MyBatis 源码分析篇 7:Mapper 方法执行的“后果”

我们已经知道了 Mapper 方法执行的前因,即:获取语句+参数映射(MyBatis 源码分析篇 6:Mapper 方法执行的“前因”)。现在就让我们来看看其“后果”:结果集映射。

参数映射是通过 TypeHandler 实现的,那么同理,结果集映射也应该是通过 TypeHandler 实现。

我们还是按照之前的方式,使用 debug 在入口代码上打断点,步入源码。入口代码为:

List<Author> list = mapper.selectByName("Sylvia");

对应的 SQL:

   <resultMap id="AuthorMap" type="Author">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="sex" property="sex" />
        <result column="phone" property="phone" />
    </resultMap>

    <select id="selectByName" resultMap="AuthorMap" >
        select
          id, name, sex, phone
        from author
        where name = #{name}
    </select>

我们接着 MyBatis 源码分析篇 5:Mapper 方法执行之 Executor 一文最后我们找到的 JDBC 代码位置 org.apache.ibatis.executor.statement.PreparedStatementHandler 的 query(...) 方法来看:

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

在执行完 PreparedStatement 的 execute() 后,自然就要进行结果的处理,我们现在就进入最后一行结果处理的代码看看:
org.apache.ibatis.executor.resultset.DefaultResultSetHandler:

  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    final List<Object> multipleResults = new ArrayList<Object>();
    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    //忽略 resultSets 部分...
    return collapseSingleResultList(multipleResults);
  }

这个方法就是在获取结果集并处理成我们最终想要的 List<Author> 类型的结果。我们提取一下关键步骤:

  • 第一步:获取 PreparedStatement 执行数据库操作后返回的 ResultSet。
    ResultSetWrapper rsw = getFirstResultSet(stmt);(方法的第 4 行代码)
  • 第二步:获取 ResultMap。
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();(方法的第 5 行代码)
    ResultMap resultMap = resultMaps.get(resultSetCount);(方法的第 9 行代码)
  • 第三步:通过前两步获得的 ResultSet 和 ResultMap 处理获得最终的 List<Author> 类型的结果。
    handleResultSet(rsw, resultMap, multipleResults, null);(方法的第 10 行代码)

现在,我们就来对每一步进行跟踪分析。

1 获取 ResultSet

ResultSetWrapper rsw = getFirstResultSet(stmt);

步入这行代码,进入本类的 getFirstResultSet(...) 方法:

  private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
    ResultSet rs = stmt.getResultSet();
    while (rs == null) {
      // move forward to get the first resultset in case the driver
      // doesn't return the resultset as the first result (HSQLDB 2.1)
      if (stmt.getMoreResults()) {
        rs = stmt.getResultSet();
      } else {
        if (stmt.getUpdateCount() == -1) {
          // no more results. Must be no resultset
          break;
        }
      }
    }
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
  }

在上一篇关于获取语句+参数映射的分析我们知道,这个方法传入的参数 stmt 是通过动态代理方法生成的,那么上面这个方法中的第一行代码:ResultSet rs = stmt.getResultSet(); 自然就会进入动态代理类 PreparedStatementLogger 中的 invoke(...) 方法来获取 ResultSet:

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }          
      if (EXECUTE_METHODS.contains(method.getName())) {
        //这里省略了几个 else if
      } else if ("getResultSet".equals(method.getName())) {
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      }
    //这里还有其他代码,继续省略...
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

我们主要来看 if ("getResultSet".equals(method.getName() 这个判断后的代码:如果现在执行的方法是 getResultSet(...),那么首先会获取一个 ResultSet 的实例,接着会通过 ResultSetLogger.newInstance() 获取的 ResultSet 覆盖掉这个 ResultSet。同获取 PreparedStatement 实例非常相似,这里采用同样的方式:动态代理来通过 ResultSetLogger 代理类获取 ResultSet 实例。此处便不再赘述。

分析到这里我们就拿到了新出炉的 ResultSet,并将它封装到了 ResultSetWrapper 类中以供接下来的使用。

2 获取 ResultMap

List<ResultMap> resultMaps = mappedStatement.getResultMaps();
ResultMap resultMap = resultMaps.get(resultSetCount);

关于获取 ResultMap,它是直接从 MappedStatement 中拿到的,而这个 MappedStatement 实例的内容是在获取 SqlSessionFactory 时填充的,关于这一点我们在 MyBatis 源码分析篇 6:Mapper 方法执行的“前因” 中已经详细分析过,此处便不再赘述,感兴趣的同学可以查看之前的篇章并结合自己实践跟踪源码来回忆一下。

这个时候获得的 ResultMap 是这个样子的:

ResultMap

可以看到 ResultMap 已经包含了每个属性的结果映射 Map、要映射的列和属性,用于接下来的结果映射。

3 获取 List<Object>

handleResultSet(rsw, resultMap, multipleResults, null);

通过前两步,我们已经拿到了 ResultSet 和 ResultMap 了,现在我们就要来通过它们处理得到最终我们想要的 List<Object> 了。

首先要说明的一点是,这行代码是在 while 循环中,即通过遍历我们拿到的 ResultSetWrapper (封装了 ResultSet),来循环处理每一条结果集。现在就让我们步入这行代码,进入本类的 handleResultSet(...) 方法:

  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

对于上面这个方法,其中第 6 、7 行代码为我们要跟踪的核心功能。其中,第 6 行代码通过 ObjectFactory 生成 DefaultResultHandler,该 DefaultResultHandler 持有生成的 List 实例。第 7 行代码则是给第 6 行代码执行生成的 List 填充值。而第 8 行代码则是将分页处理过后的 List<Object> 加入 List 类型的 multipleResults 中返回。

现在我们就来步入第 6 行核心代码看看:

  public DefaultResultHandler(ObjectFactory objectFactory) {
    list = objectFactory.create(List.class);
  }

在 DefaultResultHandler 类的构造方法中,调用了 ObjectFactory 的 create(...) 方法。想必大家对 ObjectFactory 已经不再陌生,在前面文档篇关于 ObjectFactory 的学习中我们知道:MyBatis 在获取到结果集后会通过 ObjectFactory 来创建结果对象的实例。它也是在 MyBatis 读取 Configuration XML 的时候存入的。也就是说,如果我们没有在 mybatis-config.xml 中配置自定义的 objectFactory,它会使用默认的 DefaultObjectFactory,默认只是实例化一个目标类。

为了印证这一点,我们跟踪 create(...) 方法来看一下 DefaultObjectFactory 中的实现:

  @Override
  public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    Class<?> classToCreate = resolveInterface(type);
    // we know types are assignable
    return (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
  }

上面这个方法第 1 行代码得到了 ArrayList 类型,继续步入:

  private  <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    try {
      Constructor<T> constructor;
      if (constructorArgTypes == null || constructorArgs == null) {
        constructor = type.getDeclaredConstructor();
        if (!constructor.isAccessible()) {
          constructor.setAccessible(true);
        }
        return constructor.newInstance();
      }
      constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()]));
      if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
      }
      return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
    } catch (Exception e) {
      StringBuilder argTypes = new StringBuilder();
      if (constructorArgTypes != null && !constructorArgTypes.isEmpty()) {
        for (Class<?> argType : constructorArgTypes) {
          argTypes.append(argType.getSimpleName());
          argTypes.append(",");
        }
        argTypes.deleteCharAt(argTypes.length() - 1); // remove trailing ,
      }
      StringBuilder argValues = new StringBuilder();
      if (constructorArgs != null && !constructorArgs.isEmpty()) {
        for (Object argValue : constructorArgs) {
          argValues.append(String.valueOf(argValue));
          argValues.append(",");
        }
        argValues.deleteCharAt(argValues.length() - 1); // remove trailing ,
      }
      throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e);
    }
  }

显而易见,原来 MyBatis 使用 ObjectFactory 生成目标类的实例,是通过反射实现的!对于反射,我想大家已经很熟悉了,这里就不展开分析了。

现在我们拿到了 ArrayList 实例,接下来就剩下往里面塞值了。我们接着看前面提到的第 7 行代码:handleResultSet(...) 方法的 handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);

进入 org.apache.ibatis.executor.resultset.DefaultResultSetHandler 类的 handleRowValuesForSimpleResultMap(...) 方法:

  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    skipRows(rsw.getResultSet(), rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
      Object rowValue = getRowValue(rsw, discriminatedResultMap);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
    }
  }

其中,方法的第 2 行代码是要进行逻辑分页的,这一点我们已经在 MyBatis 最佳实践篇 1:分页 一文中分析过,这里便不再赘述。我们重点来看 while 循环中的代码,该 while 循环遍历 ResultSet。

循环中的第 1 行代码的作用是对 ResultMap 进行进一步的处理:判断你是否定义了 Discriminator,如果定义了,它会去取你在 Discriminator 中指定的 ResultMap。
循环中的第 2 行代码的作用是获取 Bean 对象的实例并为该 Bean 对象赋值。
循环中的第 3 行代码的作用很简单,就是将获取到的 Bean 对象加入到 List 集合中。

我们想要知道 MyBatis 是如何将数据库获取到的每一条记录变成我们想要的集合中的 Java 对象的,所以第 2 行代码是我们要关注的重点。

步入这行代码,进入 getRowValue() 方法:

  private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

我们来分析一下这个方法的主要功能:

  • 第一步:获取 Bean 对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);(方法中第 2 行)
  • 第二步:为 Bean 对象赋值,填充字段
    foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;(方法中第 9 行)

现在我们就来依次看一下具体是如何实现每一步的。

首先我们跟入第一步的代码,直到进入 org.apache.ibatis.executor.resultset.DefaultResultSetHandler 类的 createResultObject() 方法:

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      return objectFactory.create(resultType);//对!就是这一行!
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

这个方法首先要通过 ResultMap 获取要创建对象的类型,然后根据该类型判断要如何实例化。因为在该测试中,我们要获取一个普通 Bean 对象 Author 的实例,它有默认的构造方法,所以会进入方法的第 9 行代码:return objectFactory.create(resultType); 这行代码我想大家已经不陌生了,在获取 ArrayList 对象的时候也是通过 ObjectFactory 的 create() 实现的。那么也就是说,对于结果集 List 的 Bean 对象的实例化是通过 ObjectFactory 实现的,也就是通过反射实现的!

通过反射生成的 Author 对象

现在我们拿到了 Bean 对象,剩下的就是为这个 Bean 对象的每个字段根据我们查询的结果赋值了。我们进入 applyPropertyMappings 方法看一下:

  private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    for (ResultMapping propertyMapping : propertyMappings) {
      String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      //略...
      if (propertyMapping.isCompositeResult()
          || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
          || propertyMapping.getResultSet() != null) {
        Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
        //略...
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          metaObject.setValue(property, value);
        }
      }
    }
    return foundValues;
  }

上面这个方法首先通过 ResultMap 获取到要映射的列名列表 mappedColumnNames 和属性结果映射列表
propertyMappings。然后遍历 propertyMappings,从 ResultSet 中获取每个列对应的值并将属性值存储到我们之前获得的 Author 对象中。其中,方法的第 10 行:Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); 为获取值,我们跟踪一下:

  private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    if (propertyMapping.getNestedQueryId() != null) {
      //略...
    } else {
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
  }

该方法首先获取该映射的 typeHandler 和列名,用 typeHandler 来获取:

  @Override
  public Integer getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    return rs.getInt(columnName);
  }

这是我们熟悉的 JDBC 代码,但是需要提醒一下,我们前面分析过,这里的 ResultSet 是通过动态代理生成的,所以调用其方法不是直接从我们熟知的 ResultSet 获取,而是通过代理类 ResultSetLogger 来实现,那么接着它会进入到 ResultSetLogger 的 invoke():

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      Object o = method.invoke(rs, params);
      if ("next".equals(method.getName())) {
        if (((Boolean) o)) {
          rows++;
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              printColumnHeaders(rsmd, columnCount);
            }
            printColumnValues(columnCount);
          }
        } else {
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

在该方法的第 5 行代码我们就得到了最终的值。好,现在我们拿到了字段的值,就差把值塞进 Author 里了。我们回到 DefaultResultSetHandler 的 applyPropertyMappings(...) 方法中,跟入 metaObject.setValue(property, value); 这行代码,直到进入 BeanWrapper 类的 setBeanProperty(...):

  private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
    try {
      Invoker method = metaClass.getSetInvoker(prop.getName());
      Object[] params = {value};
      try {
        method.invoke(object, params);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    } catch (Throwable t) {
      throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
    }
  }

很明显,这里也用的是反射,try 中的第一行就是在获取要调用的方法。MyBatis 中用到了大量的反射,源码中专门有一个 reflection 模块,就是封装 Java 反射的。我们先跟进去看一下:

  public Invoker getSetInvoker(String propertyName) {
    Invoker method = setMethods.get(propertyName);
    if (method == null) {
      throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'");
    }
    return method;
  }

我们看方法的第 1 行代码:Invoker method = setMethods.get(propertyName);,传入的参数是属性名,setMethods 是个 HashMap<String, Invoker>,key 即为属性名。setMethods 预先存储了 Bean 中所有属性及其对应的包含 set 方法的 MethodInvoker。

setMethods

那么现在我们就可以判断出来,第 1 行代码通过属性名获取到对应的包含了该属性的 set 方法的 MethodInvoker。然后返回到 BeanWrapper 的 setBeanProperty(...) 方法中,我们继续跟入 try 中的代码:method.invoke(object, params);:

  @Override
  public Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException {
    return method.invoke(target, args);
  }

其中,这时的 method 为 public void com.zhaoxueer.learn.entity.Author.setName(java.lang.String),target 为 Author 对象,args 即为属性对应的值。那么接下来它就会进入我们定义的 Bean,即 Author 中的 setName(...) 方法:

    public void setName(String name) {
        this.name = name;
    }

这样,循环完所有的属性结果映射后,我们就会获得一个完美的已经被赋了数据库查询结果的值的 Author 对象。当我们执行完所有的循环后,我们就会最终拿到我们想要的 List<Author> 的结果列表。至此,大功告成!

最后,简单总结一下 Mapper 方法执行。我们分了四篇分别来分析了 Mapper 方法是如何执行的,以及 Mapper 方法执行的前因(获取语句+参数映射)后果(结果映射)。其中,从获取 Connection、获取 PreparedStatement 到获取 ResultSet 都使用动态代理的方式来实现,分步打印日志的同时获取对象。对于参数和结果映射都使用了 TypeHandler,我们不需要为每个要映射的属性或字段显式配置 TypeHandler,MyBatis 会根据参数值和结果字段值实际的类型来自动推算 TypeHandler。对于结果映射,当我们从数据库获得 ResultSet 之后,通过使用 ObjectFactory 来反射来实例化对应的 Java 对象,并通过反射的方式将字段值存储到 Java 对象中。

附:

当前版本:mybatis-3.5.0
官网文档:MyBatis
项目实践:MyBatis Learn
手写源码:MyBatis 简易实现

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

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,398评论 0 4
  • 在前面的探索中,我们已经知道了 MyBatis 是如何 getMapper 并执行 Mapper 接口中的方法来进...
    兆雪儿阅读 451评论 0 1
  • MyBatis 理论篇 [TOC] 什么是MyBatis  MyBatis是支持普通SQL查询,存储过程和高级映射...
    有_味阅读 2,864评论 0 26
  • MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架,其主要就完成2件事情: 封装JDBC...
    慕容小伟阅读 988评论 0 2
  • 有人说,最浪漫的不是“我爱你”,而是“在一起”。 我大声地喊出对你的思念,婉转惆怅中浸透着一个女子深藏的心事。 或...
    小壹娘阅读 178评论 0 0