Mybatis原理--动态生成SQL

本文将带你分析Mybatis是如何动态生成SQL。
首先,会根据源码分析框架初始化时xml文件的加载、解析、缓存过程。着重介绍 xml的解析过程 和 使用解析的结果,最后列举实例和对照源码DeBug分析:当DAO接口调用时标签的解析、参数的创建、SQL的生成过程,并总结整个流程。

  • 数据的处理

Mybatis对数据的处理可以分为 用入参动态的拼装sql对sql执行的结果封装成 JavaBean

这里包括两个过程:1. 查询阶段我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值 2. 另一个就是对resultset查询结果集的jdbcType 数据转换成java 数据类型,本文只介绍第一个过程。

  • 根据传入的参数动态的拼装sql

Mybatis中,需要根据xml标签的语法编写出动态SQL,在执行的时候会根据标签进行解析,这里使用的是 Ognl 来解析标签动态地构造SQL语句

  • 分析parseDynamicTags 的解析过程:

SpringMybatis整合的时候需要配置SqlSessionFactoryBean,该配置会加入数据源和Mybatis xml配置文件路径等信息

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatisConfig.xml"/>
    <property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>
</bean>

其中SqlSessionFactoryBean实现了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中会调用buildSqlSessionFactory方法 该方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件,在启动的时候,会根据xml文件的配置路径来解析xml文件,下面我们就看看加载时候的部分源码,并做简单的分析,读者可重点关注加注解的部分代码:

XMLMapperBuilder:

/* 读者可重点关注 加注解 的部分代码即可  */
public class XMLMapperBuilder extends BaseBuilder {
 
  public void parse() {
        if(!this.configuration.isResourceLoaded(this.resource)) {
            //根据xpath解析mapper节点 
            this.configurationElement(this.parser.evalNode("/mapper"));
            this.configuration.addLoadedResource(this.resource);
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingChacheRefs();
        this.parsePendingStatements();
    }
    
    /**
     * 根据xpath解析mapper节点
     */
    private void configurationElement(XNode context) {
        try {
            String namespace = context.getStringAttribute("namespace");
            if(namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            } else {
                //赋值当前处理的mapper的namespace
                this.builderAssistant.setCurrentNamespace(namespace);
                //处理二级缓存
                this.cacheRefElement(context.evalNode("cache-ref"));
                this.cacheElement(context.evalNode("cache"));
                //处理parameterMap节点
              this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                //处理resultMap节点
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                //处理sql节点
                this.sqlElement(context.evalNodes("/mapper/sql"));
                //处理select|insert|update|delete节点
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
        }
    }

    /**
     * 处理select|insert|update|delete节点
     */
    private void buildStatementFromContext(List<XNode> list) {
        if(this.configuration.getDatabaseId() != null) {
            this.buildStatementFromContext(list, this.configuration.getDatabaseId());
        }

        this.buildStatementFromContext(list, (String)null);
    }

    /**
     * 处理select|insert|update|delete节点
     */
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        Iterator i$ = list.iterator();

        while(i$.hasNext()) {
            XNode context = (XNode)i$.next();
            //对每个节点都用XMLStatementBuilder进行解析
            XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
            try {
                //解析每个节点
                statementParser.parseStatementNode();
            } catch (IncompleteElementException var7) {
                this.configuration.addIncompleteStatement(statementParser);
            }
        }
    }
}

XMLStatementBuilder :

public class XMLStatementBuilder extends BaseBuilder {

   /**
    * 解析节点
    */
  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进行解析SQL
            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", Boolean.valueOf(!isSelect)).booleanValue();
            boolean useCache = this.context.getBooleanAttribute("useCache", Boolean.valueOf(isSelect)).booleanValue();
            boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", Boolean.valueOf(false)).booleanValue();
            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
            includeParser.applyIncludes(this.context.getNode());
            this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
          
            //解析创建SQL
            SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);
          
            //此处省略其他代码
    }  
}

默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。

XMLLanguageDriver 创建SqlSource

public class XMLLanguageDriver implements LanguageDriver {
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        //使用XMLScriptBuilder的parseScriptNode方法解析节点的SQL部分
        return builder.parseScriptNode();
    }
}

XMLScriptBuilder解析:

public class XMLScriptBuilder extends BaseBuilder {
    public SqlSource parseScriptNode() {
        //解析节点,若有子节点,就会递归的调用解析
        List<SqlNode> contents = this.parseDynamicTags(this.context);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        SqlSource sqlSource = null;
        if(this.isDynamic) {
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }

        return (SqlSource)sqlSource;
    }
}

XMLScriptBuilder 递归的解析所有节点:

public class XMLScriptBuilder extends BaseBuilder {
    private XNode context;
    private boolean isDynamic;
    private Class<?> parameterType;
    private Map<String, XMLScriptBuilder.NodeHandler> nodeHandlers;

    public XMLScriptBuilder(Configuration configuration, XNode context) {
        this(configuration, context, (Class)null);
    }

    public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
        super(configuration);
        this.nodeHandlers = new HashMap<String, XMLScriptBuilder.NodeHandler>() {
            private static final long serialVersionUID = 7123056019193266281L;

            {
                //不同的标签有不同的解析类
                this.put("trim", XMLScriptBuilder.this.new TrimHandler(null));
                this.put("where", XMLScriptBuilder.this.new WhereHandler(null));
                this.put("set", XMLScriptBuilder.this.new SetHandler(null));
                this.put("foreach", XMLScriptBuilder.this.new ForEachHandler(null));
                this.put("if", XMLScriptBuilder.this.new IfHandler(null));
                this.put("choose", XMLScriptBuilder.this.new ChooseHandler(null));
                this.put("when", XMLScriptBuilder.this.new IfHandler(null));
                this.put("otherwise", XMLScriptBuilder.this.new OtherwiseHandler(null));
                this.put("bind", XMLScriptBuilder.this.new BindHandler(null));
            }
        };
        this.context = context;
        this.parameterType = parameterType;
    }
      
    /**
     * 递归的解析所有节点
     */
    private List<SqlNode> parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList();
        NodeList children = node.getNode().getChildNodes();

        for(int i = 0; i < children.getLength(); ++i) {
            XNode child = node.newXNode(children.item(i));
            String nodeName;
            if(child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) {
                if(child.getNode().getNodeType() == 1) {
                    nodeName = child.getNode().getNodeName();
                    //根据不同的标签使用不同的解析类
                    XMLScriptBuilder.NodeHandler handler = (XMLScriptBuilder.NodeHandler)this.nodeHandlers.get(nodeName);
                    if(handler == null) {
                        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                    }
                    //解析
                    handler.handleNode(child, contents);
                    this.isDynamic = true;
                }
            } else {
                nodeName = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(nodeName);
                if(textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    this.isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(nodeName));
                }
            }
        }

        return contents;
    }

    /**
     * 内部类IfHandler的实现
     */
    private class IfHandler implements XMLScriptBuilder.NodeHandler {
        private IfHandler() {
        }

        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = XMLScriptBuilder.this.parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            String test = nodeToHandle.getStringAttribute("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }
  //其他标签类型的handler就不一一举例了,感兴趣的可以看看 XMLScriptBuilder 的源码实现
}
  • XMLConfigBuilder:解析mybatis中configLocation属性中的全局xml文件,内部会使用 XMLMapperBuilder 解析各个xml文件。
  • XMLMapperBuilder:遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用 XMLStatementBuilder 处理xml中的每个节点。
  • XMLStatementBuilder:解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用 XMLScriptBuilder 处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。
  • XMLScriptBuilder:解析xml中各个节点sql部分的Builder。

至此,mapper.xml文件就已经解析加载完成了并得到SqlSourceSqlSource将会放到Configuration中,有了SqlSource,在执行的时候会根据SqlSource获取BoundSql从而得到需要的SQLConfiguration可以看做巨大的资源库,Mybatis框架执行时需要的数据都可以Configuration中获取,Configuration的源码为:

public class Configuration {
    protected Environment environment;
    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel;
    protected boolean cacheEnabled;
    protected boolean callSettersOnNulls;
    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected LocalCacheScope localCacheScope;
    protected JdbcType jdbcTypeForNull;
    protected Set<String> lazyLoadTriggerMethods;
    protected Integer defaultStatementTimeout;
    protected ExecutorType defaultExecutorType;
    protected AutoMappingBehavior autoMappingBehavior;
    protected Properties variables;
    protected ObjectFactory objectFactory;
    protected ObjectWrapperFactory objectWrapperFactory;
    protected MapperRegistry mapperRegistry;
    protected boolean lazyLoadingEnabled;
    protected ProxyFactory proxyFactory;
    protected String databaseId;
    protected Class<?> configurationFactory;
    protected final InterceptorChain interceptorChain;
    protected final TypeHandlerRegistry typeHandlerRegistry;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final LanguageDriverRegistry languageRegistry;
    protected final Map<String, MappedStatement> mappedStatements;
    protected final Map<String, Cache> caches;
    protected final Map<String, ResultMap> resultMaps;
    protected final Map<String, ParameterMap> parameterMaps;
    protected final Map<String, KeyGenerator> keyGenerators;
    protected final Set<String> loadedResources;
    protected final Map<String, XNode> sqlFragments;
    protected final Collection<XMLStatementBuilder> incompleteStatements;
    protected final Collection<CacheRefResolver> incompleteCacheRefs;
    protected final Collection<ResultMapResolver> incompleteResultMaps;
    protected final Collection<MethodResolver> incompleteMethods;
    protected final Map<String, String> cacheRefMap;
  
  //SomeMethod...
}

下面举例来说明:
实例中我们使用:

//mapper接口的方法
schoolCustomerDao.selectBySome(1l,  "2017-09-17","120706049");
此SQL使用了一个 if 标签
<select id="selectBySome"  resultMap="BaseResultMap">
        SELECT
          id,student_number
        FROM
          school_customer
        WHERE
          id = #{id}
        <if test="studentNumber!=null">
            AND 
              student_number = #{studentNumber}
        </if>
        AND 
          create_time = #{createTime}
    </select>

在执行schoolCustomerDao.selectBySome(1l, "2017-09-17","120706049");时,
mapper的代理类会先判断是否在缓存中存在此方法,若不存在则需要加载,若已存在则直接调用,然后会根据 select|insert|update|delete 的类型调用不同的SqlSession方法,在调用之前会根据入参(1l, "2017-09-17","120706049")封装参数,封装参数的源码如下:

//MapperMethod类会执行这个方法进行参数的拼装
param = this.method.convertArgsToSqlCommandParam(args);
/**
 * 拼装入参
 */
public Object convertArgsToSqlCommandParam(Object[] args) {
            int paramCount = this.params.size();
            if(args != null && paramCount != 0) {
                if(!this.hasNamedParameters && paramCount == 1) {
                    return args[((Integer)this.params.keySet().iterator().next()).intValue()];
                } else {
                    Map<String, Object> param = new MapperMethod.ParamMap();
                    int i = 0;
                    //将入参拼装成key value 形式,并用param+数字作为key对value按照入参顺序排序
                    for(Iterator i$ = this.params.entrySet().iterator(); i$.hasNext(); ++i) {
                        Entry<Integer, String> entry = (Entry)i$.next();
                        param.put(entry.getValue(), args[((Integer)entry.getKey()).intValue()]);
                        String genericParamName = "param" + String.valueOf(i + 1);
                        if(!param.containsKey(genericParamName)) {
                            param.put(genericParamName, args[((Integer)entry.getKey()).intValue()]);
                        }
                    }
                    return param;
                }
            } else {
                return null;
            }
        }

封装参数时,不仅将入参封装进去,还会根据入参顺序,用param + 数字 作为key,入参作为value 放入 Map 中,如下所示:

封装参数后的结果

封装完参数,就会将 方法的全限名也是StatementId(本例中是:com.school.dao.SchoolCustomerDao.selectBySome)和封装好的参数传入:

//调用查询 this.command.getName() 为 com.school.dao.SchoolCustomerDao.selectBySome
result = sqlSession.selectOne(this.command.getName(), param);

执行到这一步就准备开始解析并生成SQL了,
先取出Configuration中的 MappedStatement,根据入参进行拼装SQL再执行

//从configuration中获取MappedStatement
//statement 为 com.school.dao.SchoolCustomerDao.selectBySome
MappedStatement ms = this.configuration.getMappedStatement(statement);
//调用查询
List<E> result = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

/**
 * 调用查询
 */
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        //此处拼装生成BoundSql
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        //执行查询
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

getBoundSql拼装的SQL代码为:

/**
 * BoundSql boundSql = ms.getBoundSql(parameter);
 * 调用下面方法类中的方法,获取 BoundSql 
 */
public BoundSql getBoundSql(Object parameterObject) {
        //会根据不同的sqlSource类型执行不同的解析,
       //此处会调用DynamicSqlSource的解析方法,并返回解析好的BoundSql,和已经排好序,需要替换的参数如下图,在下文中会详细解释
        BoundSql boundSql = this.sqlSource.getBoundSql(parameterObject);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if(parameterMappings == null || parameterMappings.size() <= 0) {
            boundSql = new BoundSql(this.configuration, boundSql.getSql(), this.parameterMap.getParameterMappings(), parameterObject);
        }
        Iterator i$ = boundSql.getParameterMappings().iterator();

        while(i$.hasNext()) {
            ParameterMapping pm = (ParameterMapping)i$.next();
            String rmId = pm.getResultMapId();
            if(rmId != null) {
                ResultMap rm = this.configuration.getResultMap(rmId);
                if(rm != null) {
                    this.hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }

        return boundSql;
    }

替换SQL中对应的参数
//DynamicSqlSource类
public class DynamicSqlSource implements SqlSource {
    private Configuration configuration;
    private SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }
    /**
     * 解析SQL
     */
    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(this.configuration, parameterObject);
        //会根据 rootSqlNode 中每个节点的内容,会调用 MixedSqlNode 的 apply 方法,解析合并SQL
        this.rootSqlNode.apply(context);
        //用 SqlSourceBuilder 将SQL中的 #{} 换成 ?
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
        Class<?> parameterType = parameterObject == null?Object.class:parameterObject.getClass();
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        Iterator i$ = context.getBindings().entrySet().iterator();

        while(i$.hasNext()) {
            Entry<String, Object> entry = (Entry)i$.next();
            boundSql.setAdditionalParameter((String)entry.getKey(), entry.getValue());
        }
        return boundSql;
    }
}
debug一些参数的信息
/**
*  this.rootSqlNode.apply(context);
*  会调用MixedSqlNode类的方法解析拼装SQL
*/
public class MixedSqlNode implements SqlNode {
   private List<SqlNode> contents;

   public MixedSqlNode(List<SqlNode> contents) {
       this.contents = contents;
   }

   public boolean apply(DynamicContext context) {
       Iterator i$ = this.contents.iterator();

       while(i$.hasNext()) {
           SqlNode sqlNode = (SqlNode)i$.next();
           //进行解析并拼装,此处调用下文的 IfSqlNode 的 apply 方法
           sqlNode.apply(context);
       }

       return true;
   }
}

public class IfSqlNode implements SqlNode {
   private ExpressionEvaluator evaluator;
   private String test;
   private SqlNode contents;

   public IfSqlNode(SqlNode contents, String test) {
       this.test = test;
       this.contents = contents;
       this.evaluator = new ExpressionEvaluator();
   }
   //被调用的 IfSqlNode 的 apply 方法 
   public boolean apply(DynamicContext context) {
       //调用 ExpressionEvaluator 的 evaluateBoolean 方法
       //判断执行结果是否为true
       if(this.evaluator.evaluateBoolean(this.test, context.getBindings())) {
           this.contents.apply(context);
           return true;
       } else {
           return false;
       }
   }
}

public class ExpressionEvaluator {
   public ExpressionEvaluator() {
   }
    //被调用的 ExpressionEvaluator 的 evaluateBoolean 方法
   public boolean evaluateBoolean(String expression, Object parameterObject) {
       //调用 Ognl 进行解析,实现细节不再细梳,感兴趣的读者可以DeBug查看
       Object value = OgnlCache.getValue(expression, parameterObject);
       return value instanceof Boolean?((Boolean)value).booleanValue():(value instanceof Number?!(new BigDecimal(String.valueOf(value))).equals(BigDecimal.ZERO):value != null);
   }
}
sqlNode.apply(context);方法的调用在此处使用Ognl进行解析

解析完成后,会生成一个已经将参数替换为 ? 的SQL,在执行的时候只需要调用preparedStatement.setXXX()将List<ParameterMapping> parameterMappings 中的参数按照顺序替换,就可以生成一个SQL。至此,动态解析Mybatis标签生成SQL,已经完成。

总结:

  • Mybatis中mapper.xml文件会再加载的时候全部解析为rootSqlNode节点
  • 调用mapper的DAO接口时会代理其方法,封装参数,并传入StatementId (方法全限名)
  • 根据StatementId获取到rootSqlNode节点,循环调用 MixedSqlNode方法,使用Ognl 解析的结果进行拼装返回
  • 使用SqlSourceBuilder将#{} 中的内容解析,生成已经排序的参数List<ParameterMapping> parameterMappings并将 #{} 替换成 ?
  • 最终调用JDBC中的preparedStatement.setXXX()方法,将已经排序的参数List<ParameterMapping> parameterMappings 中的参数按照顺序将?替换,便得到一个完整的SQL

以上就是《Mybatis原理--动态生成SQL》的全部内容,如有不正确的地方,请读者指正,互相学习,共同进步,谢谢。

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

推荐阅读更多精彩内容

  • 1 引言# 本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为什么要将JDBC封装成Mybait...
    七寸知架构阅读 76,424评论 36 980
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,423评论 0 4
  • 1 动态SQL# 那么,问题来了: 什么是动态SQL? 动态SQL有什么作用? 传统的使用JDBC的方法,相信大家...
    七寸知架构阅读 18,624评论 2 58
  • 风声清冽。 最愁秋深处, 遍堆残叶。 院庭初冬, 埋下姑苏一坛雪。 欲待新成酒酿, 且共饮,落花时节。 又落雪,薄...
    来路远方阅读 279评论 2 2
  • 十六年前,我在成都。一个四面环山的小镇,镇子上生活着的人都是慵懒的样子,跟我来这个地方的初衷相同,用现在的话来说,...
    林叟阅读 266评论 6 0