iBatis源码解读-SqlMapConfig.xml配置解析

一、前言

​ 最近在看 iBatis 源码,发现之前很多的细节已经忘记的差不多了,正所谓好记性不如烂笔头,于是决定将看源码的过程用博客记录下来,希望自己可以坚持下来。

iBatis 算是一个退休的框架了,现在用的比较多的一般是 MyBatis,但是之前的老项目一直在用,所以自己工作中也算是频繁与之打交道,所以我决定从最基础的开始研究一下其具体的实现逻辑。框架一般都是前辈们历经千辛万苦打磨出来的,所以要理解其实现有谈何容易,所以我决定从最原始的方式开始学习,一步一步的深入,最好是能读懂前辈的设计思路以及技巧,帮助自己在日后的开发工作中能用上。

该文章假设你对 iBatis 已经熟练使用,并且不抗拒阅读其源码。因为很多的文字会在源码片段上注释,对于源码解析的文章,我暂时也找不到更好的表述方法了。

二、示例

首先我们配置一下SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE sqlMapConfig        
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"        
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">  
<sqlMapConfig>  
    <properties resource="jdbc.properties" />  
    <transactionManager type="JDBC">  
        <dataSource type="SIMPLE">  
            <property name="JDBC.Driver" value="${jdbc.driverClassName}" />  
            <property name="JDBC.ConnectionURL" value="${jdbc.url}" />  
            <property name="JDBC.Username" value="${jdbc.username}" />  
            <property name="JDBC.Password" value="${jdbc.password}" />  
        </dataSource>  
    </transactionManager>  
    <sqlMap resource="User.xml" />
</sqlMapConfig>

jdbc.properties:

jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=root

因为这里我们不会展开 sqlMap 节点配置的解析,所以这里 User.xml 就不贴出来了。

运行主类 StartMain

public class StartMain {
    public static void main(String[] args) {
        String config = "SqlMapConfig.xml";
        Reader reader = Resources.getResourceAsReader(config);
        SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
         System.out.println(sqlMap);
    }
}

三、源码解析

通过上面的代码,我们可以知道首先通过 Resources 这个工具类将给定的配置文件获取文件字符流,具体位置为:com.ibatis.common.resources.Resources#getResourceAsReader,该工具类提供多种方式获取配置文件,所以我们这里看下getResourceAsReader 即可:

public static Reader getResourceAsReader(String resource) throws IOException {
    Reader reader;
    if (charset == null) {
      reader = new InputStreamReader(getResourceAsStream(resource));
    } else {
      reader = new InputStreamReader(getResourceAsStream(resource), charset);
    }
    
    return reader;
}

因为这个不是重点,所以暂时我们不用太关注,暂时我们只关注 SqlMapClientBuilder.buildSqlMapClient ,通过该类的命名方式我们知道这是采用了23种设计模式之一的构建者模式(有兴趣的小伙伴可自行查阅相关资源进行了解)。

public static SqlMapClient buildSqlMapClient(Reader reader) {
    return new SqlMapConfigParser().parse(reader);
}

这里可以看到通过实例化 SqlMapConfigParser 对象然后调用该实例的 parse 方法获取 SqlMapClient 对象,我们继续跟进 SqlMapConfigParser 的默认构造方法:

// 节点解析器
protected final NodeletParser parser = new NodeletParser();
// 解析数据存放仓库
private XmlParserState state = new XmlParserState();

public SqlMapConfigParser() {
    // 开启文档校验
    parser.setValidation(true);
    // 设置本地DTD文档解析器
    parser.setEntityResolver(new SqlMapClasspathEntityResolver());

    // 注册SqlMapConfig.xml中sqlMapConfig节点的解析
    addSqlMapConfigNodelets();
    // 注册SqlMapConfig.xml中properties节点的解析
    addGlobalPropNodelets();
    // 注册SqlMapConfig.xml中settings节点的解析
    addSettingsNodelets();
    // 注册SqlMapConfig.xml中typeAlias节点的解析
    addTypeAliasNodelets();
    // 注册SqlMapConfig.xml中typeHandler节点的解析
    addTypeHandlerNodelets();
    // 注册SqlMapConfig.xml中transactionManager节点的解析
    addTransactionManagerNodelets();
    // 注册SqlMapConfig.xml中sqlMap节点的解析
    addSqlMapNodelets();
    // 注册SqlMapConfig.xml中resultObjectFactory节点的解析
    addResultObjectFactoryNodelets();
 }

在标签节点注册之前,我们先看一个接口 com.ibatis.common.xml.Nodelet:

/**
 * Nodelet是一种回调或事件处理程序,可以用它来向NodeParser注册的XPath事件。
 */
public interface Nodelet {
    /**
     * 当解析文档节点时如果有注册该节点对应的注册,则会回调该方法进行后期处理
     */
    void process (Node node) throws Exception;
}

通过官文的源码我们可以清楚的知道,该接口是用来处理 XPath 路径所映射节点的回调。

了解完 Nodelet 接口后,接下来我们就正式进入节点注册分析。

addSqlMapConfigNodelets():

private void addSqlMapConfigNodelets() {
    parser.addNodelet("/sqlMapConfig/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        state.getConfig().finalizeSqlMapConfig();
      }
    });
}

该方法主要是用于注册 SqlMapConfig.xml 配置文件中的 sqlMapConfig 节点的,因为是通过 XPath 路径去进行注册的,所以有关 XPath 的知识可以参阅 XPath 教程。这里使用了一个额外的/end(),该特性是用来最终完成sqlMapConfig节点之后进行才调用的一种方式。

addGlobalPropNodelets():

private void addGlobalPropNodelets() {
    parser.addNodelet("/sqlMapConfig/properties", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //如果是本地配置文件则使用resource属性配置
        String resource = attributes.getProperty("resource");
        //网络资源则使用url属性配置
        String url = attributes.getProperty("url");
        //该属性最终会存放在XmlParserState对象的全局Properties对象中
        state.setGlobalProperties(resource, url);
      }
    });
}

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的properties节点,该节点支持本地以及网络资源路径的配置方式。

addSettingsNodelets():

private void addSettingsNodelets() {
    parser.addNodelet("/sqlMapConfig/settings", new Nodelet() {
      public void process(Node node) throws Exception {
        //解析该节点上的属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        SqlMapConfiguration config = state.getConfig();

        //是否开启关闭类信息缓存
        String classInfoCacheEnabledAttr = attributes.getProperty("classInfoCacheEnabled");
        boolean classInfoCacheEnabled = (classInfoCacheEnabledAttr == null || "true".equals(classInfoCacheEnabledAttr));
        //最终会存储在SqlMapExecutorDelegate的statementCacheEnabled属性上
        config.setClassInfoCacheEnabled(classInfoCacheEnabled);

        //是否开启关闭懒加载属性
        String lazyLoadingEnabledAttr = attributes.getProperty("lazyLoadingEnabled");
        boolean lazyLoadingEnabled = (lazyLoadingEnabledAttr == null || "true".equals(lazyLoadingEnabledAttr));
        //最终会存储在SqlMapExecutorDelegate的lazyLoadingEnabled属性上
        config.setLazyLoadingEnabled(lazyLoadingEnabled);

        //是否开启关闭语句缓存属性
        String statementCachingEnabledAttr = attributes.getProperty("statementCachingEnabled");
        boolean statementCachingEnabled = (statementCachingEnabledAttr == null || "true".equals(statementCachingEnabledAttr));
        //最终会存储在SqlMapExecutorDelegate的lazyLoadingEnabled属性上
        config.setStatementCachingEnabled(statementCachingEnabled);

        //是否开启关闭SqlMapClient的缓存模型
        String cacheModelsEnabledAttr = attributes.getProperty("cacheModelsEnabled");
        boolean cacheModelsEnabled = (cacheModelsEnabledAttr == null || "true".equals(cacheModelsEnabledAttr));
        //最终会存储在SqlMapExecutorDelegate的cacheModelsEnabled属性上
        config.setCacheModelsEnabled(cacheModelsEnabled);

        //是否开启关闭字节码增强属性
        String enhancementEnabledAttr = attributes.getProperty("enhancementEnabled");
        boolean enhancementEnabled = (enhancementEnabledAttr == null || "true".equals(enhancementEnabledAttr));
        //最终会存储在SqlMapExecutorDelegate的enhancementEnabled属性上
        config.setEnhancementEnabled(enhancementEnabled);

        //是否开启关闭列标签属性
        String useColumnLabelAttr = attributes.getProperty("useColumnLabel");
        boolean useColumnLabel = (useColumnLabelAttr == null || "true".equals(useColumnLabelAttr));
        //最终会存储在SqlMapExecutorDelegate的useColumnLabel属性上
        config.setUseColumnLabel(useColumnLabel);

        //是否开启关闭多结果集支持
        String forceMultipleResultSetSupportAttr = attributes.getProperty("forceMultipleResultSetSupport");
        boolean forceMultipleResultSetSupport = "true".equals(forceMultipleResultSetSupportAttr);
        //最终会存储在SqlMapExecutorDelegate的forceMultipleResultSetSupport属性上
        config.setForceMultipleResultSetSupport(forceMultipleResultSetSupport);

        //配置语句的执行超时时间
        String defaultTimeoutAttr = attributes.getProperty("defaultStatementTimeout");
        Integer defaultTimeout = defaultTimeoutAttr == null ? null : Integer.valueOf(defaultTimeoutAttr);
        //最终会存储在SqlMapConfiguration的defaultStatementTimeout属性上
        config.setDefaultStatementTimeout(defaultTimeout);

        //是否开启关闭语句命名空间
        String useStatementNamespacesAttr = attributes.getProperty("useStatementNamespaces");
        boolean useStatementNamespaces = "true".equals(useStatementNamespacesAttr);
        //最终会存储在SqlMapConfiguration的useStatementNamespaces属性上
        state.setUseStatementNamespaces(useStatementNamespaces);
      }
    });
}

addTypeAliasNodelets():

private void addTypeAliasNodelets() {
    parser.addNodelet("/sqlMapConfig/typeAlias", new Nodelet() {
      public void process(Node node) throws Exception {
        //解析标签上的属性
        Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //别名
        String alias = prop.getProperty("alias");
        //类型
        String type = prop.getProperty("type");
        //最终会存储在SqlMapConfiguration的typeHandlerFactory属性对象typeAliases的Map集合属性中
        state.getConfig().getTypeHandlerFactory().putTypeAlias(alias, type);
      }
    });
}

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的typeAlias节点,主要作用是用于将某个类型取个别名,例如我们有一个实体类com.think.domain.User,如果不想每次都要写包名,则可以配置该标签将其给定一个别名User,后面则无需写冗长的包名了。

addTypeHandlerNodelets():

private void addTypeHandlerNodelets() {
    parser.addNodelet("/sqlMapConfig/typeHandler", new Nodelet() {
      public void process(Node node) throws Exception {
        //解析字段属性
        Properties prop = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //数据库对应的类型
        String jdbcType = prop.getProperty("jdbcType");
        //java对应的类型
        String javaType = prop.getProperty("javaType");
        //回调
        String callback = prop.getProperty("callback");
        //判断是否能定位到别名
        javaType = state.getConfig().getTypeHandlerFactory().resolveAlias(javaType);
        callback = state.getConfig().getTypeHandlerFactory().resolveAlias(callback);
        //实例化一个类型处理器并添加到TypeHandlerFactory对象的typeHandlerMap中
        state.getConfig().newTypeHandler(Resources.classForName(javaType), jdbcType, Resources.instantiate(callback));
      }
    });
}

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的typeHandler节点,主要用于将jdbcType(数据库类型)转换为javaType(java类型)

addTransactionManagerNodelets():

private void addTransactionManagerNodelets() {
    parser.addNodelet("/sqlMapConfig/transactionManager/property", new Nodelet() {
      public void process(Node node) throws Exception {
        //获取节点属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        //将节点的属性名和属性值放到XmlParserState的Properties名为txProps对象中
        state.getTxProps().setProperty(name, value);
      }
    });
    //用于解析事务管理器结束标签的回调
    parser.addNodelet("/sqlMapConfig/transactionManager/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        //获取该节点上的属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //事务管理器类型(JDBC、JTA、EXTERNAL)
        String type = attributes.getProperty("type");
        //是否强制提交事务
        boolean commitRequired = "true".equals(attributes.getProperty("commitRequired"));

        //通过别名来定位到具体的实例对象字符串,  该别名在SqlMapConfiguration的registerDefaultTypeAliases()中进行了注册
        type = state.getConfig().getTypeHandlerFactory().resolveAlias(type);
        TransactionManager txManager;
        //通过反射方式实例化该对象
        TransactionConfig config = (TransactionConfig) Resources.instantiate(type);
        //设置给定的数据源
        config.setDataSource(state.getDataSource());
        //设置事务属性
        config.setProperties(state.getTxProps());
        //设置是否强制提交事务
        config.setForceCommit(commitRequired);
        //设置给定数据源(不懂为啥这里还要设置一次)
        config.setDataSource(state.getDataSource());
        //配置完成后实例化一个事务管理器
        txManager = new TransactionManager(config);
        state.getConfig().setTransactionManager(txManager);
      }
    });
    //用于处理数据源属性
    parser.addNodelet("/sqlMapConfig/transactionManager/dataSource/property", new Nodelet() {
      public void process(Node node) throws Exception {
        //获取节点属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        //将数据源属性放入到XmlParserState的Properties名为dsProps对象中
        state.getDsProps().setProperty(name, value);
      }
    });
    //用于处理数据源结束标签逻辑
    parser.addNodelet("/sqlMapConfig/transactionManager/dataSource/end()", new Nodelet() {
      public void process(Node node) throws Exception {
        //获取节点属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //数据源类型(SIMPLE、DBCP、JNDI)
        String type = attributes.getProperty("type");
        Properties props = state.getDsProps();

        //通过别名定位到数据源的实例字符串
        type = state.getConfig().getTypeHandlerFactory().resolveAlias(type);
        //通过反射的方式实例化数据源工厂
        DataSourceFactory dsFactory = (DataSourceFactory) Resources.instantiate(type);
        //将配置的数据源属性进行工厂初始化操作
        dsFactory.initialize(props);
        //通过该工厂模式获取到数据源对象
        state.setDataSource(dsFactory.getDataSource());
      }
    });
}

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的transactionManager节点,主要用于配置iBatis的数据库事务管理器以及数据源。

addSqlMapNodelets():

 protected void addSqlMapNodelets() {
    parser.addNodelet("/sqlMapConfig/sqlMap", new Nodelet() {
      public void process(Node node) throws Exception {
        //解析该节点上的属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //本地配置文件选resource,远程方式选url
        String resource = attributes.getProperty("resource");
        String url = attributes.getProperty("url");

        Reader reader = null;
        if (resource != null) {
          reader = Resources.getResourceAsReader(resource);
        } else if (url != null) {
          reader = Resources.getUrlAsReader(url);
        } else {
            throw new SqlMapException("The <sqlMap> element requires either a resource or a url attribute.");
        }
        //因为本节内容只讲解了SqlMapConfig.xml文件的内容,所以这里的解析后期会讲(给自己挖个坑,后面再填吧)
        new SqlMapParser(state).parse(reader);
      }
    });
}

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的sqlMap节点,用于解析数据库表对应java实体类的映射逻辑,因为支持字符流和字节流的方式解析,因为大致逻辑相同,这里只抽取了字符流的代码。

addResultObjectFactoryNodelets():

private void addResultObjectFactoryNodelets() {
    parser.addNodelet("/sqlMapConfig/resultObjectFactory", new Nodelet() {
      public void process(Node node) throws Exception {
        //解析节点属性
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        //开发者自定义结果对象工厂类
        String type = attributes.getProperty("type");
        //通过反射实例化该对象
        ResultObjectFactory = (ResultObjectFactory) Resources.instantiate(type);
        state.getConfig().setResultObjectFactory(rof);
      }
    });
    //处理结果对象工厂的属性配置
    parser.addNodelet("/sqlMapConfig/resultObjectFactory/property", new Nodelet() {
      public void process(Node node) throws Exception {
        Properties attributes = NodeletUtils.parseAttributes(node, state.getGlobalProps());
        String name = attributes.getProperty("name");
        String value = NodeletUtils.parsePropertyTokens(attributes.getProperty("value"), state.getGlobalProps());
        state.getConfig().getDelegate().getResultObjectFactory().setProperty(name, value);
      }
    });
  }

该方法用于注册SqlMapConfig.xml配置文件中的 sqlMapConfig节点下面的resultObjectFactory节点。开发者自定义实现必须要实现ResultObjectFactory接口类,该接口源码如下:

/**
* iBATIS使用此接口的实现在语句执行后创建结果对象。要使用,请将实现类指定为SqlMapConfig
* 中“resultObjectFactory”元素的类型。此接口的任何实现都必须具有公共无参数构造函数。
* 
* 请注意,iBATIS通过ResultObjectFactoryUtil类使用此接口。
*/
public interface ResultObjectFactory {
 /**
  * 返回所请求类的新实例。
  * 在以下情况下,iBATIS将调用此方法:
  *
  * <ul>
  * <li>处理结果集时-创建结果对象的新实例</li>
  * <li>在处理存储过程的输出参数时-创建OUTPUT参数的实例</li>
  * <li>在处理嵌套选择时-创建参数实例嵌套选择上的对象</li>
  * <li>在处理带有嵌套结果图的结果图时。 iBATIS将要求工厂创建嵌套对象的实例。如果嵌套对象是<code> java.util.Collection </code>的某些实现
  * 然后iBATIS将提供通用接口的默认实现
  * 如果工厂选择不创建对象。如果嵌入式对象是<code> java.util.List </code>或<code> java.util.Collection </code>的默认行为是
  * 创建一个<code> java.util.ArrayList </code>。如果嵌入的对象是<code> java.util.Set </code>的默认行为是创建<code> java.util.HashSet </code>。</li>
  * </ul>
  *
  * 如果您通过此方法返回<code> null </code>,则iBATIS会尝试使用其正常机制在类的实例中创建。这表示
  * 您可以选择使用此界面创建哪些对象。如果您选择不创建对象,则iBATIS会翻译一些内容通用接口到其通用实现。如果要求类是列表或集合iBATIS将创建一个ArrayList。如果要求class为Set,那么iBATIS将创建一个HashSet。但是这些规则仅适用如果您选择不创建对象。所以你可以用这个工厂来如果您愿意,提供这些接口的自定义实现。
  */
  Object createInstance(String statementId, Class clazz);
  /**
  * 调用SqlMapCong文件中配置的每个属性。在对createInstance进行任何调用之前,将设置所有属性
  */
  void setProperty(String name, String value);
}

研究完标签节点的注册之后,我们继续跟进 SqlMapConfigParser 类的 parse方法,实际方法位置在 com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser#parse

public SqlMapClient parse(Reader reader) {
     usingStreams = false;
     //将字符流对象交给文档解析器进行解析
     parser.parse(reader);
     //最终通过解析后的数据仓库对象拿到SqlMapConfiguration对象,再通过该对象拿到SqlMapClientImpl实例
     return state.getConfig().getClient();
}

这里通过 NodeletParser 实例进行配置文件的解析工作,具体位置在:com.ibatis.common.xml.NodeletParser#parse:

public void parse(Reader reader) throws NodeletException {
     //创建文档对象
     Document doc = createDocument(reader);
     //开始解析文档节点
     parse(doc.getLastChild());
}

parse:

public void parse(Node node) {
    // 实例化一个用于构建XPath路径的对象
    Path path = new Path();
    // 处理XPath路径"/"的节点
    processNodelet(node, "/");
    // 处理已注册的节点
    process(node, path);
}

NodeletParser 实例对象中有一个 Map 集合用于存储要解析的 XPath 节点,所以这里我们接下来看 process 方法并是用于解析注册到该 Map 中的节点数据:

// 该方法用于递归解析配置文档中的节点
private void process(Node node, Path path) {
   if (node instanceof Element) {
     // Element(节点是元素)
     // 获取节点的名称
     String elementName = node.getNodeName();
     // 将节点名称添加到路径对象中
     path.add(elementName);
     // 如果该节点有注册则进行该节点的回调处理
     processNodelet(node, path.toString());
     // 如果有注册该XPath路径则执行该节点的回调
     processNodelet(node, new StringBuffer("//").append(elementName).toString());

     // Attribute(节点属性)
     NamedNodeMap attributes = node.getAttributes();
     int n = attributes.getLength();
     for (int i = 0; i < n; i++) {
       Node att = attributes.item(i);
       String attrName = att.getNodeName();
       path.add("@" + attrName);
       //如果有注册XPath属性节点,则执行该注册的回调
       processNodelet(att, path.toString());
       processNodelet(node, new StringBuffer("//@").append(attrName).toString());
       //最终将其移除该属性节点
       path.remove();
     }

     // Children(子节点)
     NodeList children = node.getChildNodes();
     for (int i = 0; i < children.getLength(); i++) {
        //如果有子节点者执行该方法的回调继续解析
       process(children.item(i), path);
     }
     //解析到节点的结束标签增加end()
     path.add("end()");
     //处理该节点结束时候的回调处理
     processNodelet(node, path.toString());
     path.remove();
     path.remove();
   } else if (node instanceof Text) {
     // Text
     path.add("text()");
     //处理该节点的文本内容
     processNodelet(node, path.toString());
     processNodelet(node, "//text()");
     path.remove();
   }
}

processNodelet:

   private void processNodelet(Node node, String pathString) {
       Nodelet nodelet = (Nodelet) letMap.get(pathString);
       if (nodelet != null) {
           nodelet.process(node);
       }
   }

processNodelet方法用于映射到已经注册的节点,并执行之前添加到该集合上的回调。

总结

通过本文的分析,我们可以清晰的发现,iBatis的解析还是很简单的,首先读取配置文件,然后通过注册一系列的XPath路径注册到Map集合上,然后通过解析到对应的节点然后获取对应的节点数据以及属性并设置到对应的对象上,最终构建好SqlMapClient对象返回给开发者。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271