mybatis源码分析---读取配置中sql的具体过程

一.执行流程

1. 简单的初始化代码

mybatis.cfg.xml主配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="db.properties"/>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/shop"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="userMapper.xml"></mapper>
    </mappers>
</configuration>

userMapper.xml映射配置文件

<?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE mapper
                PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
        <!-- 写Sql语句   -->
 <mapper namespace="com.testmybaits.mapper.UserMapper">
   <insert id="UserRegister" parameterType="com.testmybaits.po.User" >
       insert into user values (#{uid},#{username},#{password},#{name},#{email},#{telephone},#{birthday},#{sex},#{state},#{code});
   </insert>
</mapper>

运行的主函数

public static void main(String[] args) {//执行如下代码
        String resource= "mybatis.cfg.xml";
        InputStream in = null;
        try {
            in = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        sqlSession.close();
    }

2. 执行步骤

第一步,mybatis运行,创建SqlSessionFactoryBuilder工厂对象,调用build方法,读取配置文件信息存储在Configuration对象中,并创建SqlSessionFactory对象

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());//通过XML解析器解析,获得DefaultSqlSession对象
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();
            try {
                inputStream.close();
            } catch (IOException var13) {
                ;
            }
        }
        return var5;
    }

第二步,创建SqlSession,赋予属性。从上一步获得的Configuration对象中,得到Environment环境信息,创建事务工厂。根据传入的事务隔离级别、是否自动提交的参数,创建事务对象。再根据事务对象、配置对象中的执行器类型创建执行器对象,通过执行器对象创建SqlSession
即 事务工厂-->事务-->执行器-->SqlSession

public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;

        DefaultSqlSession var8;
        try {
            Environment environment = this.configuration.getEnvironment();//读取配置文件中的环境信息
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);//创建事务工厂
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//根据传入的TransactionIsolationLevel、autoCommit创建事务对象
            Executor executor = this.configuration.newExecutor(tx, execType);//根据事务、配置对象的执行器类型创建执行器对象,这里是SimpleExecutor
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
            ErrorContext.instance().reset();
        }

        return var8;
    }

第三步.SqlSession提交、关闭

二.疑惑

mybatis有xml、注解的使用方式
mybatis的使用方式(如果对使用方式不熟可以看这篇)

1.对于Sql的xml是怎么被读取的、为什么xml文件名要规定一致,sql的#{ }是怎么替换的?
2.假如使用注解的方式又是怎么被读取的?

三.解决

1.第一个问题

  1. 创建SqlSessionFactory对象,关键在于这个build方法读取配置文件,并将配置信息放到Configuration对象中,进入build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());//通过XML解析器解析,获得DefaultSqlSession对象
        } //...

发现其中创建了一个XML解析器,调用了parse方法,进入parse方法

  1. parse代码如下
public Configuration parse() {
        if (this.parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
            this.parsed = true;
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            return this.configuration;
        }
    }

可以看到parse方法调用parseConfiguration找到configuration标签,读取内部内容,具体又是怎么读呢? 进入parseConfiguration方法

  1. parseConfiguration方法如下,
private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.typeAliasesElement(root.evalNode("typeAliases"));
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            this.settingsElement(settings);
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

可以看到很多见过的属性,即读取主配置文件的属性进到Configuration对象中,具体怎么读,以读取mappers为例(其下有package、mapper标签)进入mapperElement方法

  1. mapperElement方法如下
private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();
            while(true) {
                while(var2.hasNext()) {
                    XNode child = (XNode)var2.next();
                    String resource;
                    if ("package".equals(child.getName())) {
                        resource = child.getStringAttribute("name");
                        this.configuration.addMappers(resource);
                    } else {
                        resource = child.getStringAttribute("resource");
                        String url = child.getStringAttribute("url");
                        String mapperClass = child.getStringAttribute("class");
                        XMLMapperBuilder mapperParser;
                        InputStream inputStream;
                        if (resource != null && url == null && mapperClass == null) {
//...没截全
    }

该方法判断是package还是mapper标签,然后读取进Configuration对象,具体怎么读呢?以packeage为例

  1. 读取package的代码如下
if ("package".equals(child.getName())) {
                        resource = child.getStringAttribute("name");
                        this.configuration.addMappers(resource);
                    } 

resource获得包名后,调用addMappers,有多个重载,最终调用到参数类型为String packageName, Class<?> superType的addMappers

public void addMappers(String packageName) {
        this.mapperRegistry.addMappers(packageName);
    }
public void addMappers(String packageName) {
        this.addMappers(packageName, Object.class);
    }
  1. 最终调用的addMappers方法
public void addMappers(String packageName, Class<?> superType) {//最终调用到这个addMapper方法
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        resolverUtil.find(new IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        Iterator var5 = mapperSet.iterator();

        while(var5.hasNext()) {
            Class<?> mapperClass = (Class)var5.next();
            this.addMapper(mapperClass);
        }
    }

其中find方法去找当前工程有无该包,若有,经过匹配后,会加入到该resolverUtil中的Set对象中,通过getClasses得到该添加完毕的set对象。调用自身参数类型为Class<T> type的addMapper方法,进入该addMapper方法

  1. 自身参数类型为Class<T> type的addMapper方法如下
  public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (this.hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                this.knownMappers.put(type, new MapperProxyFactory(type));//准备加载的Class对象的map
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
                parser.parse();
                loadCompleted = true;//加载完成,继续放在该map中
            } finally {
                if (!loadCompleted) {//加载未完成,从该map中移除出去
                    this.knownMappers.remove(type);
                }
            }
        }
    }

可以看到,最终通过MapperAnnotationBuilder.parser完成解析,从名称上Mapper注解的构建解析,解析过程肯定在这里,进去看看

  1. 具体的MapperAnnotationBuilder.parser方法,
public void parse() {
        String resource = this.type.toString();
        if (!this.configuration.isResourceLoaded(resource)) {//已加载过则不执行
            this.loadXmlResource();//加载寻找package对应的xml文件,有的话,则加载。没有则执行完向下
            this.configuration.addLoadedResource(resource);//里面是Set,表示已加载的resource
            this.assistant.setCurrentNamespace(this.type.getName());
            this.parseCache();//解析缓存
            this.parseCacheRef();
            Method[] methods = this.type.getMethods();
            Method[] var3 = methods;
            int var4 = methods.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Method method = var3[var5];

                try {
                    if (!method.isBridge()) {
                        this.parseStatement(method);//如果读取了xml文件不会进入,未读,则在该方法中去类中读取注解
                    }
                } catch (IncompleteElementException var8) {
                    this.configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        this.parsePendingMethods();
    }

其中有loadXmlResource方法,装载xml文件,装载什么配置文件呢,主配置文件装载过了,显然是映射的配置文件,进去看看

  1. loadXmlResource代码如下
private void loadXmlResource() {
        if (!this.configuration.isResourceLoaded("namespace:" + this.type.getName())) {
            String xmlResource = this.type.getName().replace('.', '/') + ".xml";//将配置文件中package配的/换成. ,得到对应的xml的文件名称(这就是为什么文件名称要统一的原因)
            InputStream inputStream = null;
            try {
                inputStream = Resources.getResourceAsStream(this.type.getClassLoader(), xmlResource);
            } catch (IOException var4) {
                ;
            }
            if (inputStream != null) {//读取到对应的配置文件
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, this.assistant.getConfiguration(), xmlResource, this.configuration.getSqlFragments(), this.type.getName());
                xmlParser.parse();//进行解析
            }
        }
    }

我们可以看到在这里会将配置文件中package配的"/"换成". ",得到对应的xml的文件名称(这就是为什么文件名称要统一的原因),读取了映射配置文件,然后调用了xmlParser.parse()方法

  1. xmlParser.parse()方法具体解析过程如下
    *1.parse进行具体解析,代码如下,可以看到有个configurationElement方法读取了xml里mapper的sql信息,怎么读取的呢?
public void parse() {
        if (!this.configuration.isResourceLoaded(this.resource)) {
            this.configurationElement(this.parser.evalNode("/mapper"));//读取到mapper标签的内容进行具体解析,这个方法将参数换成了占位符!!!重要
            this.configuration.addLoadedResource(this.resource);//加入到已读map中
            this.bindMapperForNamespace();
        }
        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }

​ *2.进入configurationElement方法,代码如下,其中的buildStatementFromContext方法中读取了具体的sql操作的标签信息,进buildStatementFromContext看看

private void configurationElement(XNode context) {
        try {//获取对应参数
            String namespace = context.getStringAttribute("namespace");//获取命名空间
            if (namespace != null && !namespace.equals("")) {
                this.builderAssistant.setCurrentNamespace(namespace);
                this.cacheRefElement(context.evalNode("cache-ref"));//获取缓存信息
                this.cacheElement(context.evalNode("cache"));
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));/
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                this.sqlElement(context.evalNodes("/mapper/sql"));//获取动态sql标签
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));//具体的读取sql!!!
            } else {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
        }
    }

​ *3.追踪进buildStatementFromContext方法中,代码如下

private void buildStatementFromContext(List<XNode> list) {
        if (this.configuration.getDatabaseId() != null) {//获取数据库的配置信息,不为null,则从Configuration中读取
            this.buildStatementFromContext(list, this.configuration.getDatabaseId());
        }//null时,传入null
        this.buildStatementFromContext(list, (String)null);
    }

​ *4.继续进入buildStatementFromContext方法中,名字上可以看出是创建Statement对象从Context中(应该是指写在xml中的sql语句),在该方法中有一个statementParser.parseStatementNode()方法值得注意
(看到了熟悉的Statement)

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        Iterator var3 = list.iterator();
        while(var3.hasNext()) {//获得单个的XNode
            XNode context = (XNode)var3.next();
            XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
            try {
                statementParser.parseStatementNode();//进行解析!!!
            } catch (IncompleteElementException var7) {
                this.configuration.addIncompleteStatement(statementParser);
            }
        }
    }

​ *5.进入statementParser.parseStatementNode方法,很多熟悉的配方,答案不远了

public void parseStatementNode() {
        String id = this.context.getStringAttribute("id");
        String databaseId = this.context.getStringAttribute("databaseId");
        if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            Integer fetchSize = this.context.getIntAttribute("fetchSize");
            Integer timeout = this.context.getIntAttribute("timeout");
            String parameterMap = this.context.getStringAttribute("parameterMap");
            String parameterType = this.context.getStringAttribute("parameterType");
            Class<?> parameterTypeClass = this.resolveClass(parameterType);
            String resultMap = this.context.getStringAttribute("resultMap");
            String resultType = this.context.getStringAttribute("resultType");
            String lang = this.context.getStringAttribute("lang");
            LanguageDriver langDriver = this.getLanguageDriver(lang);
            Class<?> resultTypeClass = this.resolveClass(resultType);
            String resultSetType = this.context.getStringAttribute("resultSetType");
            StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
            ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);
            String nodeName = this.context.getNode().getNodeName();
            SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            boolean flushCache = this.context.getBooleanAttribute("flushCache", !isSelect);
            boolean useCache = this.context.getBooleanAttribute("useCache", isSelect);
            boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", false);
            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
            includeParser.applyIncludes(this.context.getNode());
            this.processSelectKeyNodes(id, parameterTypeClass, langDriver);


            SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);//SqlSource(sql语句来源),这里调用了一个createSqlSource方法


            String resultSets = this.context.getStringAttribute("resultSets");
            String keyProperty = this.context.getStringAttribute("keyProperty");
            String keyColumn = this.context.getStringAttribute("keyColumn");
           String keyStatementId = id + "!selectKey";
            keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
            Object keyGenerator;
            if (this.configuration.hasKeyGenerator(keyStatementId)) {
                keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
            } else {
                keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            }

            this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
        }
    }

发现有个方法createSqlSource,看名字就是创建sqlSource

当parseStatementNode方法执行完毕时,会将结果放到MappedStatement里,即配置文件中所有的mapper信息读取完毕

​ *6.通过debug,进入到一个名为XMLLanguageDriver的类中的createSqlSource

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

​ *7.打开builder.parseScriptNode()方法,可以看到在这里开始判断使用哪种解析方式,是动态还是非动态

public SqlSource parseScriptNode() {
        MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context);
        SqlSource sqlSource = null;
        if (this.isDynamic) {//判断是否为动态sql,是则调用
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }
        return (SqlSource)sqlSource;
    }

​ *8.打开RawSqlSource方法

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
        this(configuration, getSql(configuration, rootSqlNode), parameterType);
    }//getSql(configuration, rootSqlNode)从名称可以得知,从配置对象,和读取的结点信息获取String类型的sql对象
    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
    }

调用的是第一个构造方法,在该构造方法中又调用第二个构造方法,来创建RawSqlSource对象,在第二个构造方法中有sqlSourceParser.parse方法,很接近答案了
​ *9.跟进sqlSourceParser.parse方法中

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        SqlSourceBuilder.ParameterMappingTokenHandler handler = new SqlSourceBuilder.ParameterMappingTokenHandler(this.configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings());
    }

可以看到mybaits在这里把#{,}切开来进行了替换,看最后的返回值为 return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings()); 根据名称可以知道一个是配置对象,一个是替换后的sql语句,一个是原先在#{}中的参数信息的集合,可以推出ParameterMappingTokenHandler对象应该是用于参数的存放
第一个问题差不多就算是明白了,下一个问题

2.第二个问题

  1. 回到MapperAnnotationBuilder的parse方法,当没有加载到对应的xml文件时,继续向下执行,在try,catch中,有一个if判断,如果读取了xml文件不会进入,未读,则调用parseStatement方法中去类中读取注解
public void parse() {
        String resource = this.type.toString();
        if (!this.configuration.isResourceLoaded(resource)) {//已加载过则不执行
            this.loadXmlResource();//加载寻找package对应的xml文件,有的话,则加载。没有则执行完向下
            this.configuration.addLoadedResource(resource);//里面是Set,表示已加载的resource
            this.assistant.setCurrentNamespace(this.type.getName());
            this.parseCache();//解析缓存
            this.parseCacheRef();
            Method[] methods = this.type.getMethods();
            Method[] var3 = methods;
            int var4 = methods.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Method method = var3[var5];
                try {
                    if (!method.isBridge()) {
                        this.parseStatement(method);//如果读取了xml文件不会进入,未读,则在该方法中去类中读取注解
                    }
                } catch (IncompleteElementException var8) {
                    this.configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        this.parsePendingMethods();
    }
  1. 进入parseStatement方法
void parseStatement(Method method) {
        Class<?> parameterTypeClass = this.getParameterType(method);
        LanguageDriver languageDriver = this.getLanguageDriver(method);
        SqlSource sqlSource = this.getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
        if (sqlSource != null) {//没截全

仔细看有一个getSqlSourceFromAnnotations方法,获取SqlSource对象从注解中
3.进入getSqlSourceFromAnnotations方法,可以看到开始获得注解信息

private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
        try {
            Class<? extends Annotation> sqlAnnotationType = this.getSqlAnnotationType(method);
            Class<? extends Annotation> sqlProviderAnnotationType = this.getSqlProviderAnnotationType(method);
            Annotation sqlProviderAnnotation;
            if (sqlAnnotationType != null) {
                if (sqlProviderAnnotationType != null) {
                    throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
                } else {
                    sqlProviderAnnotation = method.getAnnotation(sqlAnnotationType);
                    String[] strings = (String[])((String[])sqlProviderAnnotation.getClass().getMethod("value").invoke(sqlProviderAnnotation));
                    return this.buildSqlSourceFromStrings(strings, parameterType, languageDriver);
                }
            } else if (sqlProviderAnnotationType != null) {
                sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
                return new ProviderSqlSource(this.assistant.getConfiguration(), sqlProviderAnnotation, this.type, method);
            } else {
                return null;
            }
        } catch (Exception var8) {
            throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + var8, var8);
        }
    }

搞定收工!

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

推荐阅读更多精彩内容