核心操作包是 MyBatis 进行数据库查询和对象关系映射等工作的包。该包中的类能完成参数解析、数据库查询、结果映射等主要功能。在主要功能的执行过程中还会涉及缓存、懒加载、鉴别器处理、主键自增、插件支持等众多其他功能。

1. jdbc

jdbc包是 MyBatis中一个十分独立的包,该包提供了数据库操作语句的执行能力和脚本运行能力。jdbc包看起来非常简单,除即将废弃的 SelectBuilder和 SqlBuilder类外,只剩下六个类。但是,整个包的源码有很多地方值得揣摩。我们首先给出以下两点疑问,然后带着这两点疑问继续后面的源码分析。

  • AbstractSQL类中的很多方法名是大写。例如,UPDATE应该写作 update才对,为什么会出现这样的情况?
  • 整个 jdbc包中的所有类都没有被外部引用过,那该包有什么存在的意义?

1.1 AbstractSQL类与SQL类

AbstractSQL 类是一个抽象类,它含有一个抽象方法 getSelf。而 SQL 类作为AbstractSQL类的子类实现了该抽象方法。AbstractSQL类包含两个静态内部类:SafeAppendable类和 SQLStatement类。本着自下而上的分析原则,我们先分析 AbstractSQL中的这两个内部类。

1.1.1 SafeAppendable内部类

SafeAppendable是一个拼接器,它的 append方法也能实现串的拼接功能。其功能实现比较简单,源码如下所示。

  // Appendable接口:StringBuilder/StringBuffer等都是它的子类,表征具有可拼接性,字符等可以拼接在后面。// 一个安全的拼接器,安全是因为外部调用不到。而内部又通过实例化调用private static class SafeAppendable {// 主串private final Appendable a;// 主串是否为空private boolean empty = true;public SafeAppendable(Appendable a) {super();this.a = a;}/*** 向主串拼接一段字符串* @param s 被拼接的字符串* @return SafeAppendable内部类自身*/public SafeAppendable append(CharSequence s) {try {// 要拼接的串长度不为零,则拼完后主串也不是空了if (empty && s.length() > 0) {empty = false;}// 拼接a.append(s);} catch (IOException e) {throw new RuntimeException(e);}return this;}/*** 判断当前主串是否为空* @return 当前主串是否为空*/public boolean isEmpty() {return empty;}}

1.1.2 SQLStatement内部类

SQLStatement内部类可以完整地表述出一条 SQL语句,它的主要属性如下所示。可以看出,这些属性完整地表述了一条 SQL语句所需要的各种片段信息。

    // 当前语句的语句类型StatementType statementType;// 语句片段信息List<String> sets = new ArrayList<>();List<String> select = new ArrayList<>();List<String> tables = new ArrayList<>();List<String> join = new ArrayList<>();List<String> innerJoin = new ArrayList<>();List<String> outerJoin = new ArrayList<>();List<String> leftOuterJoin = new ArrayList<>();List<String> rightOuterJoin = new ArrayList<>();List<String> where = new ArrayList<>();List<String> having = new ArrayList<>();List<String> groupBy = new ArrayList<>();List<String> orderBy = new ArrayList<>();List<String> lastList = new ArrayList<>();List<String> columns = new ArrayList<>();List<List<String>> valuesList = new ArrayList<>();// 表征是否去重,该字段仅仅对于SELECT操作有效,它决定是SELECT还是SELECT DISTINCTboolean distinct;// 结果偏移量String offset;// 结果总数约束String limit;// 结果约束策略LimitingRowsStrategy limitingRowsStrategy = LimitingRowsStrategy.NOP;

并且 SQLStatement中确实存在一个 sql方法,能够根据不同的语句类型调用相应的子方法将语句片段信息拼接成一个完整的 SQL语句。该 sql方法的源码如下所示。

    /*** 根据语句类型,调用不同的语句拼接器拼接SQL语句* @param a 起始字符串* @return 拼接完成后的结果*/public String sql(Appendable a) {SafeAppendable builder = new SafeAppendable(a);if (statementType == null) {return null;}String answer;switch (statementType) {case DELETE:answer = deleteSQL(builder);break;case INSERT:answer = insertSQL(builder);break;case SELECT:answer = selectSQL(builder);break;case UPDATE:answer = updateSQL(builder);break;default:answer = null;}return answer;}

如下代码则展示了 selectSQL方法,它能完成 SELECT语句的拼接工作。

    // SELECT 操作的拼接// 因为SELECT操作(其他操作也是)的字符拼接是固定的,因此只要给定各个keyword的list即可按照顺序完成拼接/*** 将SQL语句片段信息拼接为一个完整的SELECT语句* @param builder 语句拼接器* @return 拼接完成的SQL语句字符串*/private String selectSQL(SafeAppendable builder) {if (distinct) {sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");} else {sqlClause(builder, "SELECT", select, "", "", ", ");}sqlClause(builder, "FROM", tables, "", "", ", ");joins(builder); // JOIN操作相对复杂,调用单独的joins子方法进行操作sqlClause(builder, "WHERE", where, "(", ")", " AND ");sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");sqlClause(builder, "HAVING", having, "(", ")", " AND ");sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");limitingRowsStrategy.appendClause(builder, offset, limit);return builder.toString();}

1.1.3 AbstractSQL类

有了 SQLStatement和 SafeAppendable这两个内部类之后,外部类 AbstractSQL就能不依赖其他类而实现 SQL语句的拼接工作了。例如,之前我们介绍@*Provider注解时给出了如下代码例子,其实就是基于 AbstractSQL类的子类 SQL类进行的将字符串片段拼接为 SQL语句的工作。

而且这也解答了我们本章开始时给出的一个疑问:AbstractSQL类中存在大量的全大写字母命名的方法,如 UPDATE、SET等,这是为了照顾用户的使用习惯。因为通常我们在书写 SQL语句时会将 UPDATE、SET等关键字大写。知道了 AbstractSQL类的结构后,就可以分析整个 AbstractSQL类的使用了。

首先,用户使用类似“SELECT("*").FROM("user").WHERE("schoolName=#{schoolName}")”的语句设置 SQL 语句片段,这些片段被保存在 AbstractSQL 中SQLStatement内部类的 ArrayList中。

用户调用 toString()操作时,触发了 SQL片段的拼接工作。在SQLStatement内部类中按照一定规则拼接成完整的 SQL语句。我们发现拼接函数的整个拼接操作的模板是固定的,例如,代码所示的insertSQL方法中是按照 tables、columns、values的顺序拼接的。

    /*** 将SQL语句片段信息拼接为一个完整的INSERT语句* @param builder 语句拼接器* @return 拼接完成的SQL语句字符串*/private String insertSQL(SafeAppendable builder) {sqlClause(builder, "INSERT INTO", tables, "", "", "");sqlClause(builder, "", columns, "(", ")", ", ");for (int i = 0; i < valuesList.size(); i++) {sqlClause(builder, i > 0 ? "," : "VALUES", valuesList.get(i), "(", ")", ", ");}return builder.toString();}

那这是不是意味着用户在组建 SQL语句时的语句顺序是可以打乱的呢?事实上,确实如此,我们调整代码,完全不会影响代码的执行。

1.1.4 SQL类

SQL类是 AbstractSQL的子类,仅仅重写了其中的一个 getSelf方法。整个 SQL类如下代码所示。

那 AbstractSQL为什么要留存一个抽象方法,然后再创建一个 SQL类来实现呢?这一切的意义是什么呢?将AbstractSQL作为抽象方法独立出来,使得我们可以继承AbstractSQL实现其他的子类,保证了 AbstractSQL类更容易被扩展。例如,我们可以创建一个继承了 AbstractSQL类的 ExplainSQL类。然后在ExplainSQL类中增加一个行为,例如,在所有的操作前都增加 EXPLAIN前缀以实现 SQL运行性能的分析。

1.2 SqlRunner类

SqlRunner类是 MyBatis提供的可以直接执行 SQL语句的工具类。可以通过代码直接调用SqlRunner 执行 SQL语句。

在使用 SqlRunner时有一点要注意,那就是如果参数为 null,则需要引用枚举类型Null中的枚举值。这是因为 Null中的枚举类型包含了类型信息和类型处理器信息,如代码所示。

public enum Null {BOOLEAN(new BooleanTypeHandler(), JdbcType.BOOLEAN),BYTE(new ByteTypeHandler(), JdbcType.TINYINT),SHORT(new ShortTypeHandler(), JdbcType.SMALLINT),INTEGER(new IntegerTypeHandler(), JdbcType.INTEGER),LONG(new LongTypeHandler(), JdbcType.BIGINT),FLOAT(new FloatTypeHandler(), JdbcType.FLOAT),DOUBLE(new DoubleTypeHandler(), JdbcType.DOUBLE),BIGDECIMAL(new BigDecimalTypeHandler(), JdbcType.DECIMAL),STRING(new StringTypeHandler(), JdbcType.VARCHAR),CLOB(new ClobTypeHandler(), JdbcType.CLOB),LONGVARCHAR(new ClobTypeHandler(), JdbcType.LONGVARCHAR),BYTEARRAY(new ByteArrayTypeHandler(), JdbcType.LONGVARBINARY),BLOB(new BlobTypeHandler(), JdbcType.BLOB),LONGVARBINARY(new BlobTypeHandler(), JdbcType.LONGVARBINARY),OBJECT(new ObjectTypeHandler(), JdbcType.OTHER),OTHER(new ObjectTypeHandler(), JdbcType.OTHER),TIMESTAMP(new DateTypeHandler(), JdbcType.TIMESTAMP),DATE(new DateOnlyTypeHandler(), JdbcType.DATE),TIME(new TimeOnlyTypeHandler(), JdbcType.TIME),SQLTIMESTAMP(new SqlTimestampTypeHandler(), JdbcType.TIMESTAMP),SQLDATE(new SqlDateTypeHandler(), JdbcType.DATE),SQLTIME(new SqlTimeTypeHandler(), JdbcType.TIME);// 参数的类型处理器private TypeHandler<?> typeHandler;// 参数的JDBC类型private JdbcType jdbcType;Null(TypeHandler<?> typeHandler, JdbcType jdbcType) {this.typeHandler = typeHandler;this.jdbcType = jdbcType;}public TypeHandler<?> getTypeHandler() {return typeHandler;}public JdbcType getJdbcType() {return jdbcType;}
}

使用 Null的枚举值进行参数设置,确保了参数值虽然为 null,但参数的类型是明确的。而具有明确的参数类型在 PreparedStatement的 setNull函数中是必需的,SqlRunner类为参数赋 null值时最终调用了下面的 setNull函数,感兴趣的读者可以自己追踪代码分析。

    /*** Sets the designated parameter to SQL <code>NULL</code>.** <P><B>Note:</B> You must specify the parameter's SQL type.** @param parameterIndex the first parameter is 1, the second is 2, ...* @param sqlType the SQL type code defined in <code>java.sql.Types</code>* @exception SQLException if parameterIndex does not correspond to a parameter* marker in the SQL statement; if a database access error occurs or* this method is called on a closed <code>PreparedStatement</code>* @exception SQLFeatureNotSupportedException if <code>sqlType</code> is* a <code>ARRAY</code>, <code>BLOB</code>, <code>CLOB</code>,* <code>DATALINK</code>, <code>JAVA_OBJECT</code>, <code>NCHAR</code>,* <code>NCLOB</code>, <code>NVARCHAR</code>, <code>LONGNVARCHAR</code>,*  <code>REF</code>, <code>ROWID</code>, <code>SQLXML</code>* or  <code>STRUCT</code> data type and the JDBC driver does not support* this data type*/void setNull(int parameterIndex, int sqlType) throws SQLException;

SqlRunner中的相关方法都比较简单,如下代码展示了查询多条记录的selectAll方法。

  /*** 执行多个数据的查询操作,即SELECT操作* @param sql 要查询的SQL语句* @param args SQL语句的参数* @return 查询结果* @throws SQLException*/public List<Map<String, Object>> selectAll(String sql, Object... args) throws SQLException {PreparedStatement ps = connection.prepareStatement(sql);try {setParameters(ps, args);ResultSet rs = ps.executeQuery();return getResults(rs);} finally {try {ps.close();} catch (SQLException e) {//ignore}}}

在获得查询结果之后,SqlRunner还使用结果处理函数 getResults对结果进行进一步的处理。该函数负责将数据库操作返回的结果提取出来,用列表的形式来返回。getResults的源码如代码所示。

  /*** 处理数据库操作的返回结果* @param rs 返回的结果* @return 处理后的结果列表* @throws SQLException*/private List<Map<String, Object>> getResults(ResultSet rs) throws SQLException {try {List<Map<String, Object>> list = new ArrayList<>();// 返回结果的字段名列表,按照字段顺序排列List<String> columns = new ArrayList<>();// 返回结果的类型处理器列表,按照字段顺序排列List<TypeHandler<?>> typeHandlers = new ArrayList<>();// 获取返回结果的表信息、字段信息等ResultSetMetaData rsmd = rs.getMetaData();for (int i = 0, n = rsmd.getColumnCount(); i < n; i++) {// 记录字段名columns.add(rsmd.getColumnLabel(i + 1));// 记录字段的对应类型处理器try {Class<?> type = Resources.classForName(rsmd.getColumnClassName(i + 1));TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(type);if (typeHandler == null) {typeHandler = typeHandlerRegistry.getTypeHandler(Object.class);}typeHandlers.add(typeHandler);} catch (Exception e) {// 默认的类型处理器是Object处理器typeHandlers.add(typeHandlerRegistry.getTypeHandler(Object.class));}}// 循环处理结果while (rs.next()) {Map<String, Object> row = new HashMap<>();for (int i = 0, n = columns.size(); i < n; i++) {// 字段名String name = columns.get(i);// 对应处理器TypeHandler<?> handler = typeHandlers.get(i);// 放入结果中,key为字段名大写,value为取出的结果值row.put(name.toUpperCase(Locale.ENGLISH), handler.getResult(rs, name));}list.add(row);}return list;} finally {if (rs != null) {try {rs.close();} catch (Exception e) {// ignore}}}}

可见,SqlRunner类能接受 SQL语句和参数,然后执行数据库操作。不过,SqlRunner并不能完成对象和 SQL参数的映射、SQL结果和对象的映射等复杂的操作。

1.3 ScriptRunner类

ScriptRunner是 MyBatis提供的直接执行 SQL脚本的工具类,这使得开发者可以直接将整个脚本文件提交给 MyBatis 执行。例如,代码所示便直接将demoScript.sql中的 SQL脚本全部执行了。

ScriptRunner处理的是 SQL脚本,不涉及变量赋值问题,相比 SqlRunner而言更为简单。ScriptRunner还提供了全脚本执行和逐行执行两种模式,如下所示。

  /*** 执行脚本* @param reader 脚本*/public void runScript(Reader reader) {// 设置为自动提交setAutoCommit();try {if (sendFullScript) {// 全脚本执行executeFullScript(reader);} else {// 逐行执行executeLineByLine(reader);}} finally {rollbackConnection();}}/*** 全脚本执行* @param reader 脚本*/private void executeFullScript(Reader reader) {// 脚本全文StringBuilder script = new StringBuilder();try {BufferedReader lineReader = new BufferedReader(reader);String line;while ((line = lineReader.readLine()) != null) {// 逐行读入脚本全文script.append(line);script.append(LINE_SEPARATOR);}// 拼接为一条命令String command = script.toString();println(command);// 执行命令executeStatement(command);// 如果没有启用自动提交,则进行提交操作(脚本中可能修改了自动提交设置)commitConnection();} catch (Exception e) {String message = "Error executing: " + script + ".  Cause: " + e;printlnError(message);throw new RuntimeSqlException(message, e);}}

可见,仅仅依靠 ScriptRunner这一个类,我们就能实现 SQL脚本的执行操作。

1.4 jdbc包的独立性

现在,我们的心头还有一个疑问:整个 jdbc包中的所有类都没有被外部引用过,那该包有什么存在的意义?那是因为 jdbc包是 MyBatis提供的一个功能独立的工具包,留给用户自行使用而不是由 MyBatis调用。例如,在很多场合下,用户可以选择自行拼接SQL语句,也可以选择借助 jdbc包的工具拼接SQL语句。

SqlRunner类和 ScriptRunner类则为用户提供了执行 SQL语句和脚本的能力。有些情况下,我们要对数据库进行一些设置操作(如运行一些 D D L操作),这时并不需要通过MyBatis提供 ORM功能,那么 SqlRunner类和 ScriptRunner类将是非常好的选择。其实,该包还有一个特点:对外界依赖极小。jdbc 包除了 SqlRunner 类之外,其他类都没有依赖 jdbc包外的类。甚至 RuntimeSqlException成了唯一一个没有继承 exception包中的 PersistenceException类的异常类。SqlRunner类依赖的 jdbc包外的类的 import语句如代码所示,这也是整个jdbc包中唯一依赖 MyBatis其他包的地方。

2. cache

MyBatis 每秒可能要处理数万条数据库查询请求,而这些请求可能是重复的。缓存能够显著降低数据库查询次数,提升整个MyBatis的性能。MyBatis 缓存使得每次数据库查询请求都会先经过缓存系统的过滤,只有在没有命中缓存的情况下才会去查询物理数据库。cache包就是 MyBatis缓存能力的提供者。不过要注意的是,cache包只是提供了缓存能力,不涉及具体缓存功能的使用。因此在本章的最后,我们将从缓存功能的角度出发对各个包中与缓存机制相关的源码进行阅读与汇总。

2.1 背景知识

2.1.1 Java对象的引用级别

在 Java程序的运行过程中,JVM会自动地帮我们进行垃圾回收操作以避免无用的对象占用内存空间。这个过程主要分为两步:

  1. 找出所有的垃圾对象;
  2. 清理找出的垃圾对象。

我们重点关注第一步,即如何找出垃圾对象。这里的关键问题在于如何判断一个对象是否为垃圾对象。判断一个对象是否为垃圾对象的方法主要有引用计数法和可达性分析法,JVM采用的是可达性分析法。可达性分析法是指 JVM会以从垃圾回收的根对象(Garbage Collection Root,简称 GC Root)为起点,沿着对象之间的引用关系不断遍历。最终能够遍历的对象都是有用的对象,而无法遍历的对象便是垃圾对象。

我们举一个例子。如果图19-1中的对象 c不再引用对象 d,则通过 GC Root便无法到达对象 d和对象 f,那么对象 d和 f便成了垃圾对象。有一点要说明,在图19-1中我们只绘制了一个 GC Root,实际在 JVM中有多个 GC Root。当一个对象无法通过任何一个 GC Root遍历时,它才是垃圾对象。不过图19-1展示的这种引用关系是有局限性的。试想存在一个非必需的大对象,我们希望系统在内存不紧张时可以保留它,而在内存紧张时释放它以为更重要的对象让渡内存空间。这时应该怎么做呢?Java已经考虑到了这种情况,Java的引用中并不是只有“引用”“不引用”这两种情况,而是有四种情况:

  • 强引用(SoftReference):即我们平时所说的引用。只要一个对象能够被 GCRoot强引用到,那它就不是垃圾对象。当内存不足时,JVM 会抛出OutOfMemoryError错误而不是清除被强引用的对象。
  • 软引用(SoftReference):如果一个对象只能被 GC Root软引用到,则说明它是非必需的。当内存空间不足时,JVM会回收该对象。
  • 弱引用(WeakReference):如果一个对象只能被 GC Root弱引用到,则说明它是多余的。JVM只要发现它,不管内存空间是否充足都会回收该对象。与软引用相比,弱引用的引用强度更低,被弱引用的对象存在时间相对更短。
  • 虚引用(PhantomReference):如果一个对象只能被 GC Root虚引用到,则和无法被GC Root引用到时一样。因此,就垃圾回收过程而言,虚引用就像不存在一样,并不会决定对象的生命周期。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

2.1.2 ReferenceQueue类

如果一个对象只有软引用或者弱引用,则它随时可能会被 JVM垃圾回收掉。于是它就成了薛定谔的猫,在我们读取它之前,根本无法知道它是否还存在。可是,有时我们需要知道被软引用或者弱引用的对象在何时被回收,以便进行一些后续的处理工作。

ReferenceQueue类便提供了这样的功能。ReferenceQueue本身是一个列表,我们可以在创建软引用或者弱引用的包装对象时传入该列表。这样,当 JVM 回收被包装的对象时,会将其包装类加入 ReferenceQueue类中。

我们可以通过一个可能并不恰当的例子来理解这些概念。假设我们的目标对象是雪糕,软引用或者弱引用的包装对象就是雪糕棒。我们虽然持有了雪糕棒,但是雪糕棒上的雪糕却随时可能融化后掉在地上(也可能是被我们偷吃了,总之是没有了,相当于被 JVM销毁了)。ReferenceQueue是我们收集雪糕棒的小木桶,当我们发现某根雪糕棒上的雪糕消失时,就会把雪糕棒放到小木桶中。这样一来,我们只要观察小木桶,就能知道哪些雪糕已经消失了。

下面用示例来展示 ReferenceQueue 类的作用。代码给出了目标对象 User的源码,它的 toString方法会返回包含自身 id的字符串。我们对目标对象 User建立弱引用包装,在建立包装的构造方法中传入 ReferenceQueue。这样,当 User对象被清理后,它对应的包装对象WeakReference会被放入 ReferenceQueue中。

/*** @author Shawn* @date 2022/4/1 13:54* @title Function*/
public class User {private long id;public User() {}public User(long id) {this.id = id;}@Overridepublic String toString() {return "User{" +"id=" + id +'}';}public static void main(String[] args) {// 创建ReferenceQueue,即我们的小木桶ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();// 用来存储弱引用的对象,即我们用来抓雪糕棒的手List<WeakReference<User>> weakRefUserList = new ArrayList<>();// 创建大量的做引用对象,交给 weakRefUserList 引用,即创建了许多带有雪糕的雪糕棒,并且拿在手里for (int i = 0; i < 1000000; i++) { // 创建这么多是为了让内存紧张// 创建弱引用对象,并在此过程中传入 ReferenceQueue// 即将雪糕放在雪糕棒上,并且确定用来收集雪糕棒的小木桶WeakReference<User> weakReference = new WeakReference<>(new User(Math.round(Math.random() * 1000)), referenceQueue);// 引用这个弱引用对象,即抓起这个带有雪糕的雪糕棒weakRefUserList.add(weakReference);}WeakReference weakReference;Integer count = 0;// 处理被回收的弱引用,即通过检查小木桶,处理没有了雪糕的雪糕棒while((weakReference = (WeakReference) referenceQueue.poll()) != null) {// 虽然弱引用存在,但是弱引用上面的目标对象已经为空// 即雪糕棒在木桶中,但是雪糕棒上没有雪糕了System.out.println("JVM清理了:" + weakReference + ", 从WeakReference中取出对象值为:" + weakReference.get());count++;}// 被回收的弱引用总数,即小木桶中雪糕棒的数目System.out.println("weakReference 中的元素数目为:" + count);// 在弱引用的目标对象不被清理时,可以引用目标对象,即雪糕棒上还有雪糕System.out.println("在不被清理的情况下,可以从weakReference中取出对象值");}
}

被清理的 User对象(相当于雪糕)的包装对象 WeakReference(相当于雪糕棒)都被写入了ReferenceQueue(相当于小木桶)中,也正因为它们包装的 User对象已经被清理,因此从 ReferenceQueue取出的结果必定是 null。 ReferenceQueue 也可以用在 SoftReference 中,与在 WeakReference 中的使用情况类似,不再单独介绍。

2.2 cache包结构与Cache接口

cache包是典型的装饰器模式应用案例,在 imple子包中存放了实现类,在decorators子包中存放了众多装饰器类。而 Cache接口是实现类和装饰器类的共同接口。下图给出了 Cache接口及其子类的类图。在 Cache接口的子类中,只有一个实现类,但却有十个装饰器类。通过使用不同的装饰器装饰实现类可以让实现类有着不同的功能。

2.3 缓存键

2.3.1 缓存键的原理

MyBatis 每秒过滤众多数据库查询操作,这对 MyBatis 缓存键的设计提出了很高的要求。MyBatis缓存键要满足以下几点。

  • 无碰撞:必须保证两条不同的查询请求生成的键不一致,这是最重要也是必须满足的要求。否则会引发查询操作命中错误的缓存,并返回错误的结果。
  • 高效比较:每次缓存查询操作都可能会引发键之间的多次比较,因此该操作必须是高效的。
  • 高效生成:每次缓存查询和写入操作前都需要生成缓存的键,因此该操作也必须是高效的。

在编程中,我们常使用数值、字符串等简单类型作为键,然而,这类键容易产生碰撞。为了防止碰撞的发生,需要将键的生成机制设计得非常复杂,这又降低了键的比较效率和生成效率。因此,准确度和效率之间往往是相互制约的。为了解决以上问题,MyBatis设计了一个 CacheKey类作为缓存键。整个CacheKey设计得并不复杂,但又非常精巧。CacheKey的主要属性如代码所示。

  // 乘数,用来计算hashcode时使用private final int multiplier;// 哈希值,整个CacheKey的哈希值。如果两个CacheKey该值不同,则两个CacheKey一定不同private int hashcode;// 求和校验值,整个CacheKey的求和校验值。如果两个CacheKey该值不同,则两个CacheKey一定不同private long checksum;// 更新次数,整个CacheKey的更新次数private int count;// 更新历史private List<Object> updateList;

我们可以配合代码所示的 update 方法了解以上几个属性的作用。每一次update操作都会引发 count、checksum、hashcode值的变化,并把更新值放入updateList。

  /*** 更新CacheKey* @param object 此次更新的参数*/public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;updateList.add(object);}

在比较 CacheKey 对象是否相等时,会先进行类型判断,然后进行 hashcode、checksum、count的比较,只要有一项不相同则表明两个对象不同。以上操作都比较简单,能在很短的时间内完成。如果上面的各项属性完全一致,则会详细比较两个 CacheKey 对象的变动历史 updateList,这一步操作相对复杂,但是能保证绝对不会出现碰撞问题。代码展示了 CacheKey对象的 equals方法。

  /*** 比较当前对象和入参对象(通常也是CacheKey对象)是否相等* @param object 入参对象* @return 是否相等*/@Overridepublic boolean equals(Object object) {// 如果地址一样,是一个对象,肯定相等if (this == object) {return true;}// 如果入参不是CacheKey对象,肯定不相等if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;// 依次通过hashcode、checksum、count判断。必须完全一致才相等if (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}// 详细比较变更历史中的每次变更for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;}

这样,通过 count、checksum、hashcode这三个值实现了快速比较,而通过updateList值又确保了不会发生碰撞。这种设计较好地在准确度和效率之间取得了平衡。MyBatis 还准备了一个 NullCacheKey,该类用来充当一个空键使用。在缓存查询中,如果发现某个 CacheKey信息不全,则会返回 NullCacheKey对象,类似于返回一个 null值。但是 NullCacheKey毕竟是 CacheKey的子类,在接下来的处理中不会引发空指针异常。这种设计方式也非常值得我们借鉴。

2.3.2 缓存键的生成

在数据库查询时会先根据当前的查询条件生成一个 CacheKey,在 BaseExecutor中我们可以看到这一过程,如代码所示。

  /*** 执行查询操作* @param ms 映射语句对象* @param parameter 参数对象* @param rowBounds 翻页限制* @param resultHandler 结果处理器* @param <E> 输出结果类型* @return 查询结果* @throws SQLException*/@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);// 生成缓存的键CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}

我们通过代码所示的 createCacheKey方法探究 CacheKey对象是如何生成的。

  /*** 生成查询的缓存的键* @param ms 映射语句对象* @param parameterObject 参数对象* @param rowBounds 翻页限制* @param boundSql 解析结束后的SQL语句* @return 生成的键值*/@Overridepublic CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}// 创建CacheKey,并将所有查询参数依次更新写入CacheKey cacheKey = new CacheKey();cacheKey.update(ms.getId());cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}cacheKey.update(value);}}if (configuration.getEnvironment() != null) {// issue #176cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}

可见,生成的 CacheKey 对象中包含了这次查询的所有信息,包括查询语句的id、查询的翻页限制、数据总量、完整的 SQL语句,这些信息一致就保证了两次查询的一致。结合 CacheKey的 equals方法,我们知道只要通过 equals方法判断两个 CacheKey对象相等,则两次查询操作的条件必定是完全一致的。

2.4 缓存的实现类

impl子包中 Cache接口的实现类是 PerpetualCache。PerpetualCache的实现非常简单,只有两个属性。

  • id:用来唯一标识一个缓存。一般使用映射文件的 namespace值作为缓存的id,这样就能保证不同的映射文件的缓存是不同的。
  • cache:是一个 HashMap,采用键值对的形式来存储数据。
  // Cache的id,一般为所在的namespaceprivate final String id;// 用来存储要缓存的信息private Map<Object, Object> cache = new HashMap<>();

所以缓存的实现类就是一个附带 id的 HashMap,并没有什么特别之处。

2.5 缓存装饰器

缓存实现类 PerpetualCache 的实现非常简单,但可以通过装饰器来为其增加更多的功能。decorators子包中存在许多装饰器,根据装饰器的功能可以将它们分为以下几个大类。

  • 同步装饰器:为缓存增加同步功能,如 SynchronizedCache类。
  • 日志装饰器:为缓存增加日志功能,如 LoggingCache类。
  • 清理装饰器:为缓存中的数据增加清理功能,如 FifoCache 类、LruCache 类、WeakCache类、SoftCache类。
  • 阻塞装饰器:为缓存增加阻塞功能,如 BlockingCache类。
  • 定时清理装饰器:为缓存增加定时刷新功能,如 ScheduledCache类。
  • 序列化装饰器:为缓存增加序列化功能,如 SerializedCache类。
  • 事务装饰器:用于支持事务操作的装饰器,如 TransactionalCache类。

在以上各个装饰器中,事务装饰器会留在 2.7节同 TransactionalCacheManager类一并介绍,其余各个装饰器我们将会在本节中依次介绍。

2.5.1 同步装饰器

在使用 MyBatis的过程中,可能会出现多个线程同时访问一个缓存的情况。如果多个线程同时调用 某个dao方法,则这两个线程会同时访问 某个缓存。而缓存实现类 PerpetualCache 并没有增加任何保证多线程安全的措施,这会引发多线程安全问题。MyBatis 将保证缓存多线程安全这项工作交给了 SynchronizedCache 装饰器来完成。SynchronizedCache 装饰器的实现非常简单,它直接在被包装对象的操作方法外围增加了synchronized关键字,将被包装对象的方法转变为了同步方法。

2.5.2 日志装饰器

为数据库操作增加缓存的目的是减少数据库的查询操作从而提高运行效率。而缓存的配置也非常重要,如果配置过大则浪费内存空间,如果配置过小则无法更好地发挥作用。因此,需要依据一些运行指标来设置合适的缓存大小。日志装饰器可以为缓存增加日志统计的功能,而需要统计的数据主要是缓存命中率。所谓缓存命中率是指在多次访问缓存的过程中,能够在缓存中查询到数据的比率。日志装饰器的实现非常简单,即在缓存查询时记录查询的总次数与命中次数,如下代码给出了该部分操作的源码。

  /*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {// 请求缓存次数+1requests++;final Object value = delegate.getObject(key);if (value != null) { // 命中缓存// 命中缓存次数+1hits++;}if (log.isDebugEnabled()) {log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());}return value;}

2.5.3 清理装饰器

虽然缓存能够极大地提升数据查询的效率,但这是以消耗内存空间为代价的。缓存空间总是有限的,因此为缓存增加合适的清理策略以最大化地利用这些缓存空间十分重要。缓存装饰器中有四种清理装饰器可以完成缓存清理功能,这四种清理装饰器也对应了MyBatis的四种缓存清理策略。

1.FifoCache装饰器

FifoCache装饰器采用先进先出的策略来清理缓存,它内部使用了 keyList属性存储了缓存数据的写入顺序,并且使用 size属性存储了缓存数据的数量限制。当缓存中的数据达到限制时,FifoCache装饰器会将最先放入缓存中的数据删除。代码展示了 FifoCache类的属性。

  // 被装饰对象private final Cache delegate;// 按照写入顺序保存了缓存数据的键private final Deque<Object> keyList;// 缓存空间的大小private int size;

当向缓存中存入数据时,FifoCache 类会判断数据数量是否已经超过限制。如果超过,则会将最先写入缓存的数据删除,代码展示了相关操作的源码。

  /*** 向缓存写入一条数据* @param key 数据的键* @param value 数据的值*/@Overridepublic void putObject(Object key, Object value) {cycleKeyList(key);delegate.putObject(key, value);}/*** 记录当前放入的数据的键,同时根据空间设置清除超出的数据* @param key 当前放入的数据的键*/private void cycleKeyList(Object key) {keyList.addLast(key);if (keyList.size() > size) {Object oldestKey = keyList.removeFirst();delegate.removeObject(oldestKey);}}

2.LruCache装饰器

LRU(Least Recently Used)即近期最少使用算法,该算法会在缓存数据数量达到设置的上限时将近期未使用的数据删除。LruCache 装饰器便可以为缓存增加这些功能。代码展示了 LruCache类的属性。

  // 被装饰对象private final Cache delegate;// 使用LinkedHashMap保存的缓存数据的键private Map<Object, Object> keyMap;// 最近最少使用的数据的键private Object eldestKey;

在 LruCache类的构造方法中,会调用 setSize方法来设置缓存的空间大小。在setSize方法中创建了用以存储缓存数据键的 LinkedHashMap 对象,并重写了LinkedHashMap 的removeEldestEntry方法。代码19-16展示了相关操作的源码。removeEldestEntry是 LinkedHashMap的方法,该方法会在每次向LinkedHashMap中放入数据(put方法和 putAll方法)后被自动触发。其输入参数为最久未访问的元素。通过代码可以看出,LruCache 会在超出缓存空间的情况下将最久未访问的键放入eldestKey属性中。

  /*** LruCache构造方法* @param delegate 被装饰对象*/public LruCache(Cache delegate) {this.delegate = delegate;setSize(1024);}/*** 设置缓存空间大小* @param size 缓存空间大小*/public void setSize(final int size) {keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {private static final long serialVersionUID = 4267176411845948333L;/*** 每次向LinkedHashMap放入数据时触发* @param eldest 最久未被访问的数据* @return 最久未必访问的元素是否应该被删除*/@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {eldestKey = eldest.getKey();}return tooBig;}};}

为了删除最久未使用的数据,LruCache类还做了以下两项工作。

  • 一是在每次进行缓存查询操作时更新 keyMap中键的排序,将当前被查询的键排到最前面;
  • 二是在每次进行缓存写入操作时向 keyMap写入新的键,并且在当前缓存中数据量超过设置的数据量时删除最久未访问的数据。
  /*** 向缓存写入一条信息* @param key 信息的键* @param value 信息的值*/@Overridepublic void putObject(Object key, Object value) {// 真正的查询操作delegate.putObject(key, value);// 向keyMap中也放入该键,并根据空间情况决定是否要删除最久未访问的数据cycleKeyList(key);}/*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {// 触及一下当前被访问的键,表明它被访问了keyMap.get(key);// 真正的查询操作return delegate.getObject(key);}/*** 向keyMap中存入当前的键,并删除最久未被访问的数据* @param key 当前的键*/private void cycleKeyList(Object key) {keyMap.put(key, key);if (eldestKey != null) {delegate.removeObject(eldestKey);eldestKey = null;}}

通过上述代码可以看出,真正的缓存数据都存储在被装饰对象中。LruCache类中的keyMap虽是一个 LinkedHashMap,但是它内部存储的键和值都是缓存数据的键,而没有存储缓存数据的值。这是因为引入 LinkedHashMap的目的仅仅是用它来保存缓存数据被访问的情况,而不是参与具体数据的保存。

3.WeakCache装饰器

WeakCache装饰器通过将缓存数据包装成弱引用的数据,从而使得 JVM可以清理掉缓存数据。如下代码给出了 WeakCache类的属性。WeakCache类也准备了一个 hardLinksToAvoidGarbageCollection属性来对缓存对象进行强引用,只不过该属性提供的空间是有限的。

  // 强引用的对象列表private final Deque<Object> hardLinksToAvoidGarbageCollection;// 弱引用的对象列表private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;// 被装饰对象private final Cache delegate;// 强引用对象的数目限制private int numberOfHardLinks;

经过 WeakCache 类包装后,在向缓存中存入数据时,存入的是该数据的弱引用包装类。如下代码展示了这一过程。

  /*** 向缓存写入一条信息* @param key 信息的键* @param value 信息的值*/@Overridepublic void putObject(Object key, Object value) {// 清除垃圾回收队列中的元素removeGarbageCollectedItems();// 向被装饰对象中放入的值是弱引用的句柄delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries));}/*** 将值已经被JVM清理掉的缓存数据从缓存中删除*/private void removeGarbageCollectedItems() {WeakEntry sv;while ((sv = (WeakEntry) queueOfGarbageCollectedEntries.poll()) != null) { // 轮询该垃圾回收队列// 将该队列中涉及的键删除delegate.removeObject(sv.key);}}

而从缓存中取出数据时,取出的也是数据的弱引用包装类。数据本身可能已经被 JVM清理掉了,因此在取出数据时要对这种情况进行判断。如下代码展示了这一过程。

  /*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {Object result = null;// 假定被装饰对象只被该装饰器完全控制WeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key);if (weakReference != null) { // 取到了弱引用的句柄// 读取弱引用的对象result = weakReference.get();if (result == null) { // 弱引用的对象已经被清理// 直接删除该缓存delegate.removeObject(key);} else { // 弱引用的对象还存在// 将缓存的信息写入到强引用列表中,防止其被清理hardLinksToAvoidGarbageCollection.addFirst(result);if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { // 强引用的对象数目超出限制// 从强引用的列表中删除该数据hardLinksToAvoidGarbageCollection.removeLast();}}}return result;}

缓存中存储的数据是“数据键:数据值”的形式,而经过 WeakCache包装后,缓存中存储的数据是“数据键:弱引用包装<数据值>”的形式。那么当弱引用的数据值被 JVM回收后,缓存中的数据会变成“数据键:弱引用包装<null>”的形式。如果缓存数据值被 JVM回收了,则整个缓存数据“数据键:弱引用包装<null>”也便没有了意义,应该直接清理掉。可上述过程中有一个问题:如果数据值已经被清理,那我们便无法计算出数据的键。不知道数据的键又该怎样去调用缓存的 Object removeObject(Object key)方法去删除缓存中的“数据键:弱引用包装<null>”这条数据呢?为此,WeakCache设计了 WeakEntry内部类,如代码19-21所示。WeakEntry类作为弱引用包装类直接增加了 key属性并在其中保存了数据的键,而这个属性是强引用的,不会被 JVM随意清理掉。

  private static class WeakEntry extends WeakReference<Object> {// 该变量不会被JVM清理掉,这里存储了目标对象的键private final Object key;private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {super(value, garbageCollectionQueue);this.key = key;}}

在该类的 removeGarbageCollectedItems方法中,我们可以看到当 WeakEntry中的弱引用对象被清理时,属性 key被用来删除“数据键:弱引用包装<null>”这条数据。

4.SoftCache装饰器

SoftCache 装饰器和 WeakCache 装饰器在结构、功能上高度一致,只是从弱引用变成了软引用。

2.5.4 阻塞装饰器

当 MyBatis 接收到一条数据库查询请求,而对应的查询结果在缓存中不存在时,MyBatis会通过数据库进行查询。试想如果在数据库查询尚未结束时,MyBatis又收到一条完全相同的数据库查询请求,那应该怎样处理呢?常见的有以下两种方案。

  • 因为缓存中没有对应的缓存结果,因此再发起一条数据库查询请求,这会导致数据库短时间内收到两条完全相同的查询请求。
  • 虽然缓存中没有对应的缓存结果,但是已经向数据库发起过一次请求,因此缓存应该先阻塞住第二次查询请求。等待数据库查询结束后,将数据库的查询结果返回给两次查询请求即可。

阻塞装饰器 BlockingCache 为缓存提供了上述功能,阻塞装饰器工作示意图所示。在使用阻塞装饰器装饰缓存后,缓存在收到多条相同的查询请求时会暂时阻塞住后面的查询,等待数据库结果返回时将所有的请求一并返回。

如下代码给出了 BlockingCache类的属性。其中在 locks属性中用 ConcurrentHashMap存储了所有缓存的键与对应的锁,这样,只有当取得对应的锁后才能进行相应数据的查询操作,否则就会被阻塞。

  // 获取锁时的运行等待时间private long timeout;// 被装饰对象private final Cache delegate;// 锁的映射表。键为缓存记录的键,值为对应的锁。private final ConcurrentHashMap<Object, ReentrantLock> locks;

如下代码展示了与锁的获取和释放相关的方法。要注意的是,每一条记录的键都有一个对应的锁,所以阻塞装饰器锁住的不是整个缓存,而是缓存中的某条记录。

  /*** 找出指定键的锁* @param key 指定的键* @return 该键对应的锁*/private ReentrantLock getLockForKey(Object key) {return locks.computeIfAbsent(key, k -> new ReentrantLock());}/*** 获取某个键的锁* @param key 数据的键*/private void acquireLock(Object key) {// 找出指定对象的锁Lock lock = getLockForKey(key);if (timeout > 0) {try {boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);if (!acquired) {throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());}} catch (InterruptedException e) {throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);}} else {// 锁住lock.lock();}}/*** 释放某个对象的锁* @param key 被锁的对象*/private void releaseLock(Object key) {// 找出指定对象的锁ReentrantLock lock = locks.get(key);if (lock.isHeldByCurrentThread()) {// 解锁lock.unlock();}}

如下代码展示了 BlockingCache中的缓存数据读写方法。在读取缓存中的数据前需要获取该数据对应的锁,如果从缓存中读取到了对应的数据,则立刻释放该锁;如果从缓存中没有读取到对应的数据,则意味着接下来会进行数据库查询,直到数据库查询结束向缓存中写入该数据时,才会释放该数据的锁。

  /*** 向缓存写入一条信息* @param key 信息的键* @param value 信息的值*/@Overridepublic void putObject(Object key, Object value) {try {// 向缓存中放入数据delegate.putObject(key, value);} finally {// 因为已经放入了数据,因此释放锁releaseLock(key);}}/*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {// 获取锁acquireLock(key);// 读取结果Object value = delegate.getObject(key);if (value != null) {// 读取到结果后释放锁releaseLock(key);}// 如果缓存中没有读到结果,则不会释放锁。对应的锁会在从数据库读取了结果并写入到缓存后,在putObject中释放。// 返回查询到的缓存结果return value;}

2.5.5 定时清理装饰器

当调用缓存的 clear方法时,会清理缓存中的数据。但是该操作不会自动执行。定时清理装饰器 ScheduledCache则可以按照一定的时间间隔来清理缓存中的数据,即按照一定的时间间隔调用 clear方法。如下代码给出了 ScheduledCache类的属性。

  // 被装饰的对象private final Cache delegate;// 清理的时间间隔protected long clearInterval;// 上次清理的时刻protected long lastClear;

如下代码给出了ScheduledCache类的清理方法clearWhenStale,该方法会在getSize、putObject、getObject、removeObject中被调用。

  /*** 根据清理时间间隔设置清理缓存* @return 是否发生了缓存清理*/private boolean clearWhenStale() {if (System.currentTimeMillis() - lastClear > clearInterval) {clear();return true;}return false;}

我们要知道,ScheduledCache 提供的定时清理功能并非是实时的。也就是说,即使已经满足了清理时间间隔的要求,只要 getSize、putObject、getObject、removeObject 这四个方法没有被调用,则 clearWhenStale方法也不会被触发,也就不会发生缓存清理操作。这种非实时的设计方式也是值得参考的,因为实时操作需要增加单独的计时线程,会消耗大量的资源;而这种非实时的方式节约了资源,但同时也不会造成太大的误差。

2.5.6 序列化装饰器

对象(也就是数据)放入缓存后,如果被多次读取出来,则多次读取的是同一个对象的引用。也就是说,缓存中的对象是在多个引用之间共享的。这意味着,如果读取后修改了该对象的属性,会直接导致缓存中的对象也发生变化。

有些场景下,我们不想让外部的引用污染缓存中的对象。这时必须保证外部读取缓存中的对象时,每次读取的都是一个全新的拷贝而不是引用。序列化装饰器 SerializedCache为缓存增加了这一功能。在使用 SerializedCache后,每次向缓存中写入对象时,实际写入的是对象的序列化串;而每次读取对象时,会将序列化串反序列化后再返回。通过序列化和反序列化的过程保证了每一次缓存给出的对象都是一个全新的对象,对该对象的修改不会影响缓存中的对象。当然,这要求被缓存的数据必须是可序列化的,否则 SerializedCache会抛出异常。如下代码展示了 SerializedCache类的数据写入和读取操作的源码。

  /*** 向缓存写入一条信息* @param key 信息的键* @param object 信息的值*/@Overridepublic void putObject(Object key, Object object) {if (object == null || object instanceof Serializable) { // 要缓存的数据必须是可以序列化的// 将数据序列化后写入缓存delegate.putObject(key, serialize((Serializable) object));} else { // 要缓存的数据不可序列化// 抛出异常throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);}}/*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {// 读取缓存中的序列化串Object object = delegate.getObject(key);// 反序列化后返回return object == null ? null : deserialize((byte[]) object);}

2.6 缓存的组建

组建缓存的过程就是根据需求为缓存的基本实现增加各种装饰的过程,该过程在CacheBuilder中完成。下面通过 CacheBuilder的源码了解 MyBatis如何组建缓存。组建缓存的入口方法是 CacheBuilder中的 build方法,其源码如下所示。

  /*** 组建缓存* @return 缓存对象*/public Cache build() {// 设置缓存的默认实现、默认装饰器(仅设置,并未装配)setDefaultImplementations();// 创建默认的缓存Cache cache = newBaseCacheInstance(implementation, id);// 设置缓存的属性setCacheProperties(cache);if (PerpetualCache.class.equals(cache.getClass())) { // 缓存实现是PerpetualCache,即不是用户自定义的缓存实现// 为缓存逐级嵌套自定义的装饰器for (Class<? extends Cache> decorator : decorators) {// 生成装饰器实例,并装配。入参依次是装饰器类、被装饰的缓存cache = newCacheDecoratorInstance(decorator, cache);// 为装饰器设置属性setCacheProperties(cache);}// 为缓存增加标准的装饰器cache = setStandardDecorators(cache);} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {// 增加日志装饰器cache = new LoggingCache(cache);}// 返回被包装好的缓存return cache;}

其中的setDefaultImplementations子方法负责设置缓存的默认实现和默认装饰器。可以看出在外部没有指定实现类的情况下,缓存默认的实现类是PerpetualCache 类,默认的清理装饰器是 LruCache。要注意的是,该方法只是把默认的实现类放入了 implementation属性,把LruCache放入了 decorators属性,并没有实际生产和装配缓存。

  /*** 设置缓存的默认实现和默认装饰器*/private void setDefaultImplementations() {if (implementation == null) {implementation = PerpetualCache.class;if (decorators.isEmpty()) {decorators.add(LruCache.class);}}}

接下来会通过 newBaseCacheInstance 方法生成缓存的实现,并逐级包装用户自定义的装饰器。最后还会通过 setStandardDecorators方法为缓存增加标准的装饰器。在映射文件中,我们可以通过如下代码所示的片段指定缓存的特性。

setStandardDecorators方法就是根据上述片段中设置的缓存特性来确定对缓存增加哪些装饰器的。如下代码展示了 setStandardDecorators方法的源码。

  /*** 为缓存增加标准的装饰器* @param cache 被装饰的缓存* @return 装饰结束的缓存*/private Cache setStandardDecorators(Cache cache) {try {MetaObject metaCache = SystemMetaObject.forObject(cache);// 设置缓存大小if (size != null && metaCache.hasSetter("size")) {metaCache.setValue("size", size);}// 如果定义了清理间隔,则使用定时清理装饰器装饰缓存if (clearInterval != null) {cache = new ScheduledCache(cache);((ScheduledCache) cache).setClearInterval(clearInterval);}// 如果允许读写,则使用序列化装饰器装饰缓存if (readWrite) {cache = new SerializedCache(cache);}// 使用日志装饰器装饰缓存cache = new LoggingCache(cache);// 使用同步装饰器装饰缓存cache = new SynchronizedCache(cache);// 如果启用了阻塞功能,则使用阻塞装饰器装饰缓存if (blocking) {cache = new BlockingCache(cache);}// 返回被层层装饰的缓存return cache;} catch (Exception e) {throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);}}

setStandardDecorators方法中的各项配置与代码19-30中的设置项对应。只是有一点要注意,配置中的 readOnly在源码中变成了 readWrite。这两者在 XMLMapperBuilder类中存在下面的转化关系。

boolean readWrite = !context.getBooleanAttribute("readOnly", false);

通过阅读 CacheBuilder类的源码,我们知道为缓存增加功能的过程就是增加装饰器的过程。同时,也能感受到装饰器模式的强大与灵活。

2.7 事务缓存

在数据库操作中,如果没有显式地声明事务,则一条语句本身就是一个事务。在查询语句进行数据库查询操作之后,相应的查询结果可以立刻放入缓存中备用。那么,事务中的语句进行数据库查询操作之后,相应的查询结果可以立刻放入缓存备用吗?显然不可以。

例如,插入一条新纪录后查询所有记录,SELECT 操作得到的查询结果中其实包含了前面INSERT语句插入的信息。如果 SELECT查询结束后立刻将查询结果放入缓存,则在事务提交前缓存中就包含了事务中的信息,这是违背事务定义的。而如果之后该事务进行了回滚,则缓存中的数据就会和数据库中的数据不一致。

因此,事务操作中产生的数据需要在事务提交时写入缓存,而在事务回滚时直接销毁。TransactionalCache装饰器就为缓存提供了这一功能。

TransactionalCache类的属性如下所示,它使用 entriesToAddOnCommit属性将事务中产生的数据暂时保存起来,在事务提交时一并提交给缓存,而在事务回滚时直接销毁。

TransactionalCache类也支持将缓存的范围限制在事务以内,只要将 clearOnCommit属性置为 true即可。这样,只要事务结束,就会直接将暂时保存的数据销毁掉,而不是写入缓存中。

  // 被装饰的对象private final Cache delegate;// 事务提交后是否直接清理缓存private boolean clearOnCommit;// 事务提交时需要写入缓存的数据private final Map<Object, Object> entriesToAddOnCommit;// 缓存查询未命中的数据private final Set<Object> entriesMissedInCache;

如下代码展示了 TransactionalCache类中的缓存读取和写入操作。可见读取缓存时是真正从缓存中读取,而写入缓存时却只是暂存在 TransactionalCache对象内部。

  /*** 从缓存中读取一条信息* @param key 信息的键* @return 信息的值*/@Overridepublic Object getObject(Object key) {// 从缓存中读取对应的数据Object object = delegate.getObject(key);if (object == null) { // 缓存未命中// 记录该缓存未命中entriesMissedInCache.add(key);}if (clearOnCommit) { // 如果设置了提交时立马清除,则直接返回nullreturn null;} else {// 返回查询的结果return object;}}/*** 向缓存写入一条信息* @param key 信息的键* @param object 信息的值*/@Overridepublic void putObject(Object key, Object object) {// 先放入到entriesToAddOnCommit列表中暂存entriesToAddOnCommit.put(key, object);}

而在事务进行提交或回滚时,TransactionalCache会根据设置将自身保存的数据写入缓存或者直接销毁,如下代码展示了相关的源码。

  /*** 提交事务*/public void commit() {if (clearOnCommit) { // 如果设置了事务提交后清理缓存// 清理缓存delegate.clear();}// 将为写入缓存的操作写入缓存flushPendingEntries();// 清理环境reset();}/*** 回滚事务*/public void rollback() {// 删除缓存未命中的数据unlockMissedEntries();reset();}/*** 清理环境*/private void reset() {clearOnCommit = false;entriesToAddOnCommit.clear();entriesMissedInCache.clear();}/*** 将未写入缓存的数据写入缓存*/private void flushPendingEntries() {// 将entriesToAddOnCommit中的数据写入缓存for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}// 将entriesMissedInCache中的数据写入缓存for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}/*** 删除缓存未命中的数据*/private void unlockMissedEntries() {for (Object entry : entriesMissedInCache) {try {delegate.removeObject(entry);} catch (Exception e) {log.warn("Unexpected exception while notifiying a rollback to the cache adapter."+ "Consider upgrading your cache adapter to the latest version.  Cause: " + e);}}}

至此,大家对事务缓存 TransactionalCache,尤其是其中用来暂存事务内数据的entriesToAddOnCommit属性有了清晰的认识。然而,entriesMissedInCache属性的作用是什么?为什么要在其中保存查询缓存未命中的数据?

这就要结合阻塞装饰器 BlockingCache 来思考了。事务缓存中使用的缓存可能是被BlockingCache装饰过的,这意味着,如果缓存查询得到的结果为 null,会导致对该数据上锁,从而阻塞后续对该数据的查询。而事务提交或者回滚后,应该对缓存中的这些数据全部解锁才对。entriesMissedInCache就保存了这些数据的键,在事务结束时对这些数据进行解锁。

在一个事务中,可能会涉及多个缓存。TransactionalCacheManager 就是用来管理一个事务中的多个缓存的,其中的 transactionalCaches属性中保存了多个缓存和对应的经过缓存装饰器装饰后的缓存。如下代码展示了 transactionalCaches属性。

  // 管理多个缓存的映射private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

TransactionalCacheManager 会在事务提交和回滚时触发所有相关事务缓存的提交和回滚,如下代码所示。

  /*** 事务提交*/public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();}}/*** 事务回滚*/public void rollback() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.rollback();}}

2.8 MyBatis缓存机制

在进行源码阅读时,通常可以以包为单位进行,因为包本身就是具有一定结构、功能的类的集合。但是,也总会有一些功能相对复杂,会横跨多个包。因此,以功能为主线一次阅读多个包中的源码也是必要的,它能帮助我们厘清一个功能实现的前因后果。

这一次,我们将横跨多个包,详细了解 MyBatis的缓存机制。

之前已经详细介绍了 cache包的全部源码,了解了 MyBatis如何使用不同的装饰器装饰以得到不同功能的缓存。但是,cache包中却没有涉及缓存的具体使用。

在 executor包中,MyBatis基于 cache包中提供的缓存实现了两级缓存。在这一节中,我们将详细了解 MyBatis的缓存机制。在介绍 MyBatis的缓存机制之前,先提前了解 Executor接口的概况。Executor 接口是执行器接口,它负责进行数据库查询等操作。它有两个直接子类,CachingExecutor类和 BaseExecutor类。

  • CachingExecutor是一个装饰器类,它能够为执行器实现类增加缓存功能。
  • BaseExecutor类是所有实际执行器类的基类,它有 SimpleExecutor、BatchExecutor、ReuseExecutor、ClosedExecutor 四个子类。而其中的ClosedExecutor子类本身没有实际功能,我们暂时忽略它。

Executor接口的简化类图。

2.8.1 一级缓存

MyBatis 的一级缓存又叫本地缓存,其结构和使用都比较简单,与它相关的配置项有两个。

一个是在配置文件的 settings节点下,我们可以增加如代码所示的配置语句来改变一级缓存的作用范围。配置值的可选项有 SESSION与 STATEMENT,分别对应了一次会话和一条语句。一级缓存的默认作用范围是 SESSION。

二是可以在映射文件的数据库操作节点内增加 flushCache属性项,该属性可以设置为 true或 false。当设置为 true时,MyBatis会在该数据库操作执行前清空一、二级缓存。该属性的默认值为 false。

了解了 MyBatis一级缓存的配置后,我们查看一级缓存的源码。一级缓存功能由 BaseExecutor类实现。BaseExecutor类作为实际执行器的基类,为所有实际执行器提供一些通用的基本功能,在这里增加缓存也就意味着每个实际执行器都具有这一级缓存。在 BaseExecutor 内,可以看到与一级缓存相关的两个属性,分别是 localCache 和localOutputParameterCache,如下代码所示。这两个属性使用的都是没有经过任何装饰器装饰的 PerpetualCache对象。

  // 查询操作的结果缓存protected PerpetualCache localCache;// Callable查询的输出参数缓存protected PerpetualCache localOutputParameterCache;

这两个变量中,localCache缓存的是数据库查询操作的结果。对于CALLABLE形式的语句,因为最终向上返回的是输出参数,便使用 localOutputParameterCache 直接缓存的输出参数。

因为 localCache 和 localOutputParameterCache 都是 Executor 的属性,不可能超出Executor 的作用范围。而 Executor 归属 SqlSession,因此第一级缓存的最大作用范围便是SqlSession,即一次会话。

如下代码给出了 BaseExecutor中 query操作的源码,通过它我们可以详细了解一级缓存的作用原理,以及 localCacheScope配置、flushCache配置如何生效。

  /*** 查询数据库中的数据* @param ms 映射语句* @param parameter 参数对象* @param rowBounds 翻页限制条件* @param resultHandler 结果处理器* @param key 缓存的键* @param boundSql 查询语句* @param <E> 结果类型* @return 结果列表* @throws SQLException*/@SuppressWarnings("unchecked")@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {// 执行器已经关闭throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) { // 新的查询栈且要求清除缓存// 清除一级缓存clearLocalCache();}List<E> list;try {queryStack++;// 尝试从本地缓存获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 本地缓存没有结果,故需要查询数据库list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {// 懒加载操作的处理for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}deferredLoads.clear();// 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();}}return list;}

数据库操作中的 INSERT、UPDATE、DELETE操作都对应了 BaseExecutor中的 update方法。在update方法中,会引发一级缓存的更新。如下代码展示了BaseExecutor中update方法的源码。

  /*** 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法* @param ms 映射语句* @param parameter 参数对象* @return 数据库操作结果* @throws SQLException*/@Overridepublic int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {// 执行器已经关闭throw new ExecutorException("Executor was closed.");}// 清理本地缓存clearLocalCache();// 返回调用子类进行操作return doUpdate(ms, parameter);}

可见一级缓存就是 BaseExecutor中的两个 PerpetualCache类型的属性,其作用范围很有限,不支持各种装饰器的修饰,因此不能进行容量配置、清理策略设置及阻塞设置等。

2.8.2 二级缓存

二级缓存的作用范围是一个命名空间(即一个映射文件),而且可以实现多个命名空间共享一个缓存。因此与一级缓存相比其作用范围更广,且选择更为灵活。与二级缓存相关的配置项有四项。

第一个配置项在配置文件的 settings节点下,我们可以增加如下代码所示的配置语句来启用和关闭二级缓存。该配置项的默认值为 true,即默认启用二级缓存。

第二个配置项在映射文件内。可以使用如下代码所示的 cache标签来开启并配置本命名空间的缓存,也可以使用<cache-ref />标签来声明本命名空间使用其他命名空间的缓存,如果两项都不配置则表示命名空间没有缓存。该项配置只有在第一项配置中选择启用二级缓存时才有效。

第三个配置项为数据库操作节点内的 useCache属性,通过它可以配置该数据库操作节点是否使用二级缓存。只有当第一、二项配置均启用了缓存时,该项配置才有效。对于SELECT类型的语句而言,useCache属性的默认值为 true,对于其他类型的语句而言则没有意义。

第四个配置项为数据库操作节点内的 flushCache 属性项,该配置属性与一级缓存共用,表示是否要在该语句执行前清除一、二级缓存。

了解了二级缓存的配置项之后,我们通过源码来了解二级缓存的详细原理。二级缓存功能由 CachingExecutor类实现,它是一个装饰器类,能通过装饰实际执行器为它们增加二级缓存功能。在 Configuration的 newExecutor方法中,MyBatis会根据配置文件中的二级缓存开关配置用 CachingExecutor类装饰实际执行器。

  /*** 创建一个执行器* @param transaction 事务* @param executorType 数据库操作类型* @return 执行器*/public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;// 根据数据操作类型创建实际执行器if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}// 根据配置文件中settings节点cacheEnabled配置项确定是否启用缓存if (cacheEnabled) { // 如果配置启用缓存// 使用CachingExecutor装饰实际执行器executor = new CachingExecutor(executor);}// 为执行器增加拦截器(插件),以启用各个拦截器的功能executor = (Executor) interceptorChain.pluginAll(executor);return executor;}

在阅读 CachingExecutor类的源码之前,先讨论另外一个概念:事务。我们知道,在数据库操作中,可以将多条语句封装为一个事务;而在我们没有显式地声明事务时,数据库会为每条语句开启一个事务。于是,事务不仅可以代指封装在一起的多条语句,也可以用来代指一条普通的语句。CachingExecutor类中有两个属性,如下代码所示。其中 delegate是被装饰的实际执行器,tcm是事务缓存管理器。既然一条语句也是一个事务,那事务缓存管理器可以应用在有事务的场景,也可以应用在无事务的场景。

  // 被装饰的执行器private final Executor delegate;// 事务缓存管理器private final TransactionalCacheManager tcm = new TransactionalCacheManager();

了解了这些之后,我们查看 CachingExecutor这一装饰器类的 query核心方法,如下代码详细注释了整个工作的过程。

  /*** 查询数据库中的数据* @param ms 映射语句* @param parameterObject 参数对象* @param rowBounds 翻页限制条件* @param resultHandler 结果处理器* @param key 缓存的键* @param boundSql 查询语句* @param <E> 结果类型* @return 结果列表* @throws SQLException*/@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存Cache cache = ms.getCache();// 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为nullif (cache != null) { // 存在缓存// 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器// 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断ensureNoOutParams(ms, boundSql);// 从缓存中读取结果@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) { // 缓存中没有结果// 交给被包装的执行器执行list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 缓存被包装执行器返回的结果tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}// 交由被包装的实际执行器执行return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

其中的 flushCacheIfRequired 子方法是用来判断并清除二级缓存的方法,源码如下所示。

  /*** 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存* 注意:默认情况下,非SELECT语句的isFlushCacheRequired方法会返回true* @param ms MappedStatement*/private void flushCacheIfRequired(MappedStatement ms) {// 获取MappedStatement对应的缓存Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) { // 存在缓存且该操作语句要求执行前清除缓存// 清除事务中的缓存tcm.clear(cache);}}

而 CachingExecutor的 update方法(对应 INSERT、UPDATE、DELETE三种数据库操作)也会调用 flushCacheIfRequired方法,而对于这些语句 isFlushCacheRequired子方法恒返回 true。因此,总会导致二级缓存的清除。

2.8.3 两级缓存机制

现在我们已经清楚 MyBatis 存在两级缓存,其中一级缓存由 BaseExecutor 通过两个PerpetualCache类型的属性提供,而二级缓存由 CachingExecutor包装类提供。那么在数据库查询操作中,是先访问一级缓存还是先访问二级缓存呢?

答案并不复杂,CachingExecutor作为装饰器会先运行,然后才会调用实际执行器,这时BaseExecutor 中的方法才会执行。因此,在数据库查询操作中,MyBatis 会先访问二级缓存再访问一级缓存。这样,我们便可以得到下图所示的 MyBatis两级缓存示意图。

3. transaction

MyBatis的transaction包是负责进行事务管理的包,该包内包含两个子包:

  • jdbc子包中包含基于 JDBC进行事务管理的类;
  • managed子包中包含基于容器进行事务管理的类。

3.1 事务概述

事务即数据库事务,是数据库执行过程中的一个逻辑单位。事务有以下四个特性。

  • Atomicity(原子性):事务必须被作为一个整体来执行,要么全部执行,要么全部不执行。不允许只执行其中的一部分。
  • Consistency(一致性):事务应该保证数据库从一个一致性状态转换到另一个一致性状态。一致性状态是指数据库中数据的完整性约束。
  • Isolation(隔离性):多个事务并发执行时,事务不会互相干扰。
  • Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。

事务功能是由数据库提供的。以 MySQL 数据库为例,MySQL 主要有两种引擎:MyISAM和InnoDB。其中 MyISAM引擎是不支持事务也不支持外键的,其特点是访问速度快,非常适合用来设计日志表等不需要事务操作的表。InnoDB支持事务,但是与 MyISAM相比写操作的速度会慢一些,并且占用的磁盘空间也会稍多。

接下来我们在使用 InnoDB引擎的情况下介绍 MySQL的事务操作。MySQL默认操作模式就是自动提交模式。在这种模式下,除非显式地开始一个事务,否则每个查询都被当作一个单独的事务自动提交执行。可以通过设置 AUTOCOMMIT的值来进行修改,例如设置“SET AUTOCOMMIT=0”将关闭自动提交模式,需要对每个数据库操作都进行显示的提交。不过,通常情况下,我们会使用自动提交模式。实现 MySQL数据库事务操作的 SQL语句有下面三个。

  • BEGIN:开始事务;
  • ROLLBACK:回滚事务;
  • COMMIT:提交事务。

例如,在如下代码所示的操作中,可以看到关于“小明”的数据插入是被回滚掉的,而关于“小华”的数据插入是成功的。

在使用 Java 进行数据库操作时也可以通过数据库连接对事务进行控制。例如,如下代码所示的代码片段就实现了事务操作。

3.2 事务接口及工厂

整个 transaction包采用了工厂方法模式实现,transaction包的类图如下图所示。

TransactionFactory是所有事务工厂的接口,该接口的源码如下所示。

public interface TransactionFactory {/*** Sets transaction factory custom properties.* @param props*//*** 配置工厂的属性* @param props 工厂的属性*/default void setProperties(Properties props) {// NOP}/*** Creates a {@link Transaction} out of an existing connection.* @param conn Existing database connection* @return Transaction* @since 3.1.0*//*** 从给定的连接中获取一个事务* @param conn 给定的连接* @return 获取的事务对象*/Transaction newTransaction(Connection conn);/*** Creates a {@link Transaction} out of a datasource.* @param dataSource DataSource to take the connection from* @param level Desired isolation level* @param autoCommit Desired autocommit* @return Transaction* @since 3.1.0*//*** 从给定的数据源中获取事务,并对事务进行一些配置* @param dataSource 数据源* @param level 数据隔离级别* @param autoCommit 是否自动提交事务* @return 获取的事务对象*/Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);}

Transaction是所有事务的接口,该接口的源码如下所示。

public interface Transaction {/*** Retrieve inner database connection.* @return DataBase connection* @throws SQLException*//*** 获取该事务对应的数据库连接* @return 数据库连接* @throws SQLException*/Connection getConnection() throws SQLException;/*** Commit inner database connection.* @throws SQLException*//*** 提交事务* @throws SQLException*/void commit() throws SQLException;/*** Rollback inner database connection.* @throws SQLException*//*** 回滚事务* @throws SQLException*/void rollback() throws SQLException;/*** Close inner database connection.* @throws SQLException*//*** 关闭对应的数据连接* @throws SQLException*/void close() throws SQLException;/*** Get transaction timeout if set.* @throws SQLException*//*** 读取设置的事务超时时间* @return 事务超时时间* @throws SQLException*/Integer getTimeout() throws SQLException;}

TransactionFactory接口与 Transaction接口均有两套实现,分别在 jdbc子包和 managed子包中。

3.3 JDBC事务

jdbc子包中存放的是实现 JDBC事务的 JdbcTransaction类及其对应的工厂类。JdbcTransaction类是 JDBC事务的管理类,其属性如代码所示。

  // 数据库连接protected Connection connection;// 数据源protected DataSource dataSource;// 事务隔离级别protected TransactionIsolationLevel level;// 是否自动提交事务protected boolean autoCommit;

而具体的事务操作是由 JdbcTransaction类直接调用 Connection类提供的事务操作方法来完成的。如下代码展示了 JdbcTransaction类中事务提交和回滚的相关源码。

  /*** 提交事务* @throws SQLException*/@Overridepublic void commit() throws SQLException {// 连接存在且不会自动提交事务if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Committing JDBC Connection [" + connection + "]");}// 调用connection对象的方法提交事务connection.commit();}}/*** 回滚事务* @throws SQLException*/@Overridepublic void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Rolling back JDBC Connection [" + connection + "]");}connection.rollback();}}

JdbcTransactionFactory负责生产 JdbcTransaction对象,其实现非常简单,不再赘述。

3.4 容器事务

managed子包中存放的是实现容器事务的 ManagedTransaction类及其对应的工厂类。在 ManagedTransaction类中,可以看到 commit、rollback等方法内都没有任何逻辑操作,如下述代码所示。

  /*** 提交事务* @throws SQLException*/@Overridepublic void commit() throws SQLException {// Does nothing}/*** 回滚事务* @throws SQLException*/@Overridepublic void rollback() throws SQLException {// Does nothing}

那么这些方法是空的,又如何实现事务管理呢?这是因为相关的事务操作都委托给了容器进行管理。以 Spring容器为例。当 MyBatis和 Spring集成时,MyBatis中拿到的数据库连接对象是 Spring给出的。Spring可以通过 XML配置、注解等多种方式来管理事务(即决定事务何时开启、回滚、提交)。当然,这种情况下,事务的最终实现也是通过 Connection对象的相关方法进行的。整个过程中,MyBatis 不需要处理任何事务操作,全都委托给 Spring即可。ManagedTransactionFactory是 ManagedTransaction类的工厂,源码比较简单,不再展开介绍。

4. cursor

4.1 Iterable接口与Iterator接口

Iterable 接口与 Iterator 接口是大家经常接触的两个接口,它们都代表与迭代操作相关的能力。Iterator的意思是“迭代器”,Iterable的意思是“可迭代的”。

如果一个类是迭代器,则基于它可以实现迭代操作;而如果一个类能够给出一个迭代自身内元素的迭代器,则它就是可迭代的。因此,Iterable接口非常简单,主要定义了一个 Iterator<T> iterator抽象方法用于返回一个 Iterator对象(在 Jdk 1.8中增加了 forEach方法和 spliterator方法)。

Iterator接口表示一个针对集合的迭代器,Iterator接口定义了迭代器最重要的方法。

  • boolean hasNext:判断当前迭代中是否还有未迭代的元素。
  • E next:返回迭代中的下一个元素。
  • default void remove:从迭代器指向的集合中移除迭代器返回的最后一个元素。默认情况下不支持此操作,因为很容易造成迭代混乱。

在编程开发中,Iterable接口与 Iterator接口经常要用到,我们常用的 for-each就是基于这两个接口实现的。例如,如下代码所示的 for-each操作,经过编译后在 class文件中变成了while语句。

// 编译前
List<User> userList = new ArrayList<>();
for (User user : userList) {System.out.println(user);
}// 编译后
List<User> userList = new ArrayList<>();
Iterator<User> var2 = userList.iterator();
while (var2.hasNext()) {User user = var2.next();System.out.println(user);
}

代码能在编译后转化,这是因为 for-each是一个语法糖操作,会由编译器在编译阶段帮我们转化为基本语法。于是,在我们使用 for-each操作对 List中的元素进行遍历时,List作为Iterable接口的子类先通过 iterator方法给出一个 Iterator对象,然后基于 Iterator对象实现 List中所有元素的遍历。

最后我们再总结一下,Iterable 接口表征一个类是可迭代的,Iterator 接口表征一个类是迭代器。

  • 如果一个类能够给出一个迭代器(通过 iterator方法)用来对某个集合中的元素进行迭代,那么这个类可以继承 Iterable接口。
  • 如果一个类本身就是一个迭代器,能够对某个集合展开迭代操作,那么这个类可以继承 Iterator接口。

4.2 MyBatis中游标的使用

在使用 MyBatis进行数据库查询时,经常会查询到大量的结果。例如,我们查询到了大量的 User对象,并使用 List接受这些对象。但有些时候,我们希望逐一读入和处理查询结果,而不是一次读入整个结果集。因为前者能够减少对内存的占用,这在处理大量的数据时会显得十分必要。游标就能够帮助我们实现这一目的,它支持我们每次从结果集中取出一条结果。

在 MyBatis中使用游标进行查询非常简单,映射文件不需要任何的变动,只需要在映射接口中标明返回值类型是 Cursor即可,如下代码所示。然后,便可以用代码所示的方式来接收和处理结果。

// 映射接口
Cursor<User> queryUserBySchoolName(User user);// 接收和处理结果
UserMapper userMapper = session.getMapper(UserMapper.class);
User userPattern = new User();
userPattern.setSchoolName("Sunny School");
Cursor<User> userCursor = userMapper.queryUserBySchoolName(User user);
for(User user:userCursor){System.out.println("name:"+user.getName()+"; email:"+user.getEmail());
}

4.3 游标接口

cursor包中的源码非常简单,只有一个 Cursor接口和默认的实现类 DefaultCursor。Cursor接口继承了 java.io.Closeable接口和 java.lang.Iterable接口。Closeable接口表征一个类是可以关闭的,调用 Closeable 接口中的 close 方法可释放类的对象持有的资源。Iterable接口表征一个类是可以迭代的,这样可以对该类的对象使用 for-each操作。Cursor接口的源码如下所示,它一共规定了三个方法。

// 游标能够基于迭代器遍历项目
public interface Cursor<T> extends Closeable, Iterable<T> {/*** @return true if the cursor has started to fetch items from database.*//*** 游标是否开启* @return 是否开启*/boolean isOpen();/**** @return true if the cursor is fully consumed and has returned all elements matching the query.*//*** 是否已经完成了所有遍历* @return 是否完成了所有遍历*/boolean isConsumed();/*** Get the current item index. The first item has the index 0.* @return -1 if the first cursor item has not been retrieved. The index of the current item retrieved.*//*** 返回当前元素的索引* @return 当前元素的索引*/int getCurrentIndex();
}

4.4 默认游标

DefaultCursor类是默认的游标,下图给出了 DefaultCursor相关类的类图。通过类图可以看出,DefaultCursor类直接或者间接继承了 Cursor、Closeable、Iterable三个接口,这意味着它必须实现这三个接口定义的所有方法。

它含有三个内部类。我们仍然按照自下而上的原则对这三个内部类分别进行介绍,最后再介绍它们的外部类 DefaultCursor。

4.4.1 CursorStatus内部类

CursorStatus 内部类非常简单,是一个表征游标状态的枚举类。如下代码直接给出了它的源码,并注明了各个枚举值的含义。

  private enum CursorStatus {/*** A freshly created cursor, database ResultSet consuming has not started.*/CREATED, // 表征游标新创建,结果集尚未消费/*** A cursor currently in use, database ResultSet consuming has started.*/OPEN, // 表征游标正在被使用中,结果集正在被消费/*** A closed cursor, not fully consumed.*/CLOSED, // 表征游标已经被关闭,但其中的结果集未被完全消费/*** A fully consumed cursor, a consumed cursor is always closed.*/CONSUMED // 表征游标已经被关闭,其中的结果集已经被完全消费}

4.4.2 ObjectWrapperResultHandler内部类

ObjectWrapperResultHandler类继承了ResultHandler接口,是一个简单的结果处理器。ResultHandler接口在 session包中,其源码如下所示。ResultHandler接口中定义了一个处理单条结果的 handleResult 方法。该方法的输入参数是一个 ResultContext 对象。ResultContext类是结果上下文,从中可以取出一条结果。

public interface ResultHandler<T> {void handleResult(ResultContext<? extends T> resultContext);}private static class ObjectWrapperResultHandler<T> implements ResultHandler<T> {private T result;/*** 从结果上下文中取出并处理结果* @param context 结果上下文*/@Overridepublic void handleResult(ResultContext<? extends T> context) {// 取出结果上下文中的一条结果this.result = context.getResultObject();// 关闭结果上下文context.stop();}}

通过代码可以看出,ObjectWrapperResultHandler内部类只是将结果上下文中的一条结果取出然后放入了自身的 result属性中,并未做进一步的处理。

4.4.3 CursorIterator内部类

CursorIterator类继承了 Iterator接口,是一个迭代器类。DefaultCursor类间接继承了 Iterable接口,这意味着它必须通过 iterator方法返回一个Iterator对象。DefaultCursor类返回的 Iterator对象就是 CursorIterator对象。CursorIterator 类作为一个迭代器,实现了判断是否存在下一个元素的 hasNext 方法和返回下一个元素的 next方法。CursorIterator类的源码如下所示。

  private class CursorIterator implements Iterator<T> {// 缓存下一个要返回的对象,在next操作中完成写入T object;// next方法中返回的对象的索引int iteratorIndex = -1;/*** 判断是否还有下一个元素,如果有则顺便写入object中* @return 是否还有下一个元素*/@Overridepublic boolean hasNext() {// 如果object!=null,则显然有下一个对象,就是object本身if (object == null) {// 判断是否还能获取到新的,顺便放到object中object = fetchNextUsingRowBound();}return object != null;}/*** 返回下一个元素* @return 下一个元素*/@Overridepublic T next() {T next = object;if (next == null) { // object中无对象// 尝试去获取一个next = fetchNextUsingRowBound();}if (next != null) {// 此时,next中是这次要返回的对象。object要么本来为null,要么已经取到next中。故清空object = null;iteratorIndex++;// 返回next中的对象return next;}throw new NoSuchElementException();}/*** 删除当前的元素。不允许该操作,故直接抛出异常*/@Overridepublic void remove() {throw new UnsupportedOperationException("Cannot remove element from Cursor");}}

在 CursorIterator类中,无论是判断是否还有下一个元素的 hasNext方法还是获取下一个元素的 next方法,都调用了 fetchNextUsingRowBound 方法。该方法是外部类DefaultCursor中的一个非常重要的方法。介绍完三个内部类之后,接下来介绍 DefaultCursor外部类。

4.4.4 DefaultCursor外部类

DefaultCursor类作为默认的游标,其属性如下所示。

  // 结果集处理器private final DefaultResultSetHandler resultSetHandler;// 该结果集对应的ResultMap信息,来源于Mapper中的<ResultMap>节点private final ResultMap resultMap;// 返回结果的详细信息private final ResultSetWrapper rsw;// 结果的起止信息private final RowBounds rowBounds;// ResultHandler的子类,起到暂存结果的作用private final ObjectWrapperResultHandler<T> objectWrapperResultHandler = new ObjectWrapperResultHandler<>();// 内部迭代器private final CursorIterator cursorIterator = new CursorIterator();// 迭代器存在标志位private boolean iteratorRetrieved;// 游标状态private CursorStatus status = CursorStatus.CREATED;// 记录已经映射的行private int indexWithRowBound = -1;

DefaultCursor 类中大多数方法是用来实现 Cursor、Closeable、Iterable 三个接口的方法。其中Iterable 接口中定义的 iterator 方法的源码如下所示,该方法内使用iteratorRetrieved变量保证了迭代器只能给出一次,防止多次给出造成的访问混乱。

  /*** 返回迭代器* @return 迭代器*/@Overridepublic Iterator<T> iterator() {if (iteratorRetrieved) { // 如果迭代器已经给出throw new IllegalStateException("Cannot open more than one iterator on a Cursor");}if (isClosed()) { // 如果游标已经关闭throw new IllegalStateException("A Cursor is already closed.");}// 表明迭代器已经给出iteratorRetrieved = true;// 返回迭代器return cursorIterator;}

此外,DefaultCursor 类中重要的方法是 fetchNextUsingRowBound 方法和其子方法fetchNextObjectFromDatabase方法。fetchNextObjectFromDatabase方法在每次调用时都会从数据库查询返回的结果集中取出一条结果,而fetchNextUsingRowBound方法则在此基础上考虑了查询时的边界限制条件。于是这两个方法共同完成了在满足边界限制的情况下,每次从结果集中取出一条结果的功能。这两个方法的源码如下所示。

  /*** 考虑边界限制(翻页限制),从数据库中获取下一个对象* @return 下一个对象*/protected T fetchNextUsingRowBound() {// 从数据库查询结果中取出下一个对象T result = fetchNextObjectFromDatabase();while (result != null && indexWithRowBound < rowBounds.getOffset()) { // 如果对象存在但不满足边界限制,则持续读取数据库结果中的下一个,直到边界起始位置result = fetchNextObjectFromDatabase();}return result;}/*** 从数据库获取下一个对象* @return 下一个对象*/protected T fetchNextObjectFromDatabase() {if (isClosed()) {return null;}try {status = CursorStatus.OPEN;if (!rsw.getResultSet().isClosed()) { // 结果集尚未关闭// 从结果集中取出一条记录,将其转化为对象,并存入到objectWrapperResultHandler中resultSetHandler.handleRowValues(rsw, resultMap, objectWrapperResultHandler, RowBounds.DEFAULT, null);}} catch (SQLException e) {throw new RuntimeException(e);}// 获得存入到objectWrapperResultHandler中的对象T next = objectWrapperResultHandler.result;if (next != null) { // 读到了新的对象// 更改索引,表明记录索引加一indexWithRowBound++;}if (next == null || getReadItemsCount() == rowBounds.getOffset() + rowBounds.getLimit()) { // 没有新对象或者已经到了rowBounds边界// 游标内的数据已经消费完毕close();status = CursorStatus.CONSUMED;}// 清除objectWrapperResultHandler中的该对象,已准备迎接下一对象objectWrapperResultHandler.result = null;return next;}

fetchNextObjectFromDatabase方法的中文含义为“从数据库获取下一个对象”,从方法名称上看,该方法似乎会从数据库中查询下一条记录。但实际上并非如此,该方法并不会引发数据库查询操作。因为,在该方法被调用之前,数据库查询的结果集已经完整地保存在了 rsw变量中。fetchNextObjectFromDatabase方法只是从结果集中取出下一条记录,而非真正地去数据库查询下一条记录。例如,我们使用 DefaultCursor 展开一次查询(该查询一共会返回四条记录),通过代码调试可以看出 rsw变量中已经完整保存了四条记录,如下图所示。

因此,对于 DefaultCursor 类而言,结果集中的所有记录都已经存储在了内存中,DefaultCursor类只负责逐一给出这些记录而已。

5. executor

如果从 MyBatis的所有包中选择一个最为重要的包,那就是executor包。executor 包,顾名思义为执行器包,它作为 MyBatis 的核心将其他各个包凝聚在了一起。在该包的工作中,会调用配置解析包解析出的配置信息,会依赖基础包中提供的基础功能。最终,executor包将所有的操作串接在了一起,通过 session包向外暴露出一套完整的服务。executor 包功能众多,每一个子包都提供一个相对独立的功能项。在该包源码的阅读过程中我们依旧遵循自下而上的原则,先分析 executor包中的各个子包的源码,然后再分析与主流程相关的源码。

5.1 背景知识

如果从 MyBatis的所有包中选择一个最为重要的包,那就是 executor包。executor 包,顾名思义为执行器包,它作为 MyBatis 的核心将其他各个包凝聚在了一起。在该包的工作中,会调用配置解析包解析出的配置信息,会依赖基础包中提供的基础功能。最终,executor包将所有的操作串接在了一起,通过 session包向外暴露出一套完整的服务。executor 包功能众多,每一个子包都提供一个相对独立的功能项。在该包源码的阅读过程中我们依旧遵循自下而上的原则,先分析 executor包中的各个子包的源码,然后再分析与主流程相关的源码。

5.1.1 基于cglib的动态代理

基于反射的动态代理的一个制约条件,即被代理的类必须有一个父接口。但是有些类确实没有父接口,对于这些类而言,基于反射的动态代理是不适用的。另一种实现动态代理的方式:基于 cglib(Code Generation Library,代码生成库)的动态代理。

我们知道一个类必须通过类加载过程将类文件加载到 JVM后才能使用。那么是否能够直接修改 JVM中的字节码信息来修改和创建类呢?答案是可以的,cglib就是基于这个原理工作的。cglib使用字节码处理框架 ASM来转换字节码并生成被代理类的子类,然后这个子类就可以作为代理类展开工作。ASM是一个底层的框架,除非你对JVM内部结构包括 class文件的格式和指令集都很熟悉,否则不要直接使用 ASM。下面我们通过示例介绍一下如何用 cglib实现动态代理。首先要在项目中引入 cglib工具包,以使用 Maven为例,在 pom文件中增加如下所示的依赖。

被代理类不需要实现任何接口,下述代码给出了一个简单的被代理类。

public class User {public String sayHello(String name) {System.out.println("hello" + name);return "OK";}
}

接下来编写一个实现了 org.springframework.cglib.proxy.MethodInterceptor 接口的类。被代理类中的方法被拦截后,会进入该类的 intercept 方法。在该类的intercept方法中,我们在被代理对象的方法执行前后各增加了一句输出语句。

public class ProxyHandler<T> implements MethodInterceptor {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) {System.out.println("before speak");Object ans = methodProxy.invoke(o, objects);System.out.println("after speak");return ans;}
}

之后就可以建立代理对象,实现动态代理操作。我们创建了一个代理对象(即变量 user 对应的对象),然后调用了其中的方法。可见,在被代理对象的方法执行前后,均输出了代理对象中增加的语句。cglib是通过给被代理类创建一个子类,从而实现在不改变被代理类的情况下创建代理类的。因此它也有一定的局限性:无法为 final类创建代理,因为 final类没有子类。

5.1.2 javassist框架的使用

cglib基于底层的 ASM框架来实现 Java字节码的修改。而 javassist和 ASM类似,它也是一个开源的用来创建、修改 Java字节码的类库,能实现类的创建、方法的修改、继承关系的设置等一系列的操作。

相比于 ASM,javassist的优势是学习成本低,可以根据 Java代码生成字节码,而不需要直接操作字节码。javassist 的使用虽然比 ASM 简单,但也并不是太容易。下面通过一个示例来展示javassist的使用,以“无中生有”的方式创建一个类,并给类设置属性和方法。整个示例如下代码所示。

    public static void main(String[] args) throws Exception {ClassPool pool = ClassPool.getDefault();// 定义一个类CtClass userCtClass = pool.makeClass("org.apache.ibatis.test.User");// 创建name属性CtField nameField = new CtField(pool.get("java.lang.String"), "name", userCtClass);userCtClass.addField(nameField);// 创建name的set方法CtMethod setMethod = CtNewMethod.make("public void setName(String name) {this.name = name;}", userCtClass);userCtClass.addMethod(setMethod);Class<?> userClass = userCtClass.toClass();Object user = userClass.newInstance();Method[] methods = userClass.getMethods();for (Method method : methods) {if ("setName".equals(method.getName())) method.invoke(user, "shawn");if ("sayHello".equals(method.getName())) {String result = (String) method.invoke(user);System.out.println(result);}}}

在代码中,我们凭空创建了一个 User对象,并为其赋予了 name属性和 setName方法、sayHello 方法;然后实例化该类的对象后,调用了对象的相关方法。这些操作都是直接针对 JVM中的字节码展开的。这充分说明了直接操作字节码这种方式的灵活与强大,但因为它涉及较多的底层操作,并不是很容易驾驭。但无论如何,javassist 这个强大的工具是可以直接修改字节码的。因此,我们可以使用它创建被代理类的子类从而实现动态代理,也可以使用它创建被代理类接口的子类从而实现动态代理。

5.1.3 序列化与反序列化中的方法

1.writeExternal方法和 readExternal方法

要表明一个类的对象是可序列化的,则必须继承 Serializable接口或者Externalizable接口,而且 Externalizable接口是 Serializable接口的子接口。然后,我们还给出了继承 Serializable接口实现序列化和反序列化的示例。继承 Serializable 接口实现序列化和反序列化是非常简单的,目标类除了继承Serializable接口外不需要任何其他的操作,整个序列化和反序列化的过程由 Java内部的机制完成。而继承 Externalizable接口实现序列化和反序列化则支持自定义序列化和反序列化的方法。Externalizable接口包含以下两个抽象方法。

  • void writeExternal(ObjectOutput out):该方法在目标对象序列化时调用。方法中可以调用DataOutput(输入参数 ObjectOutput的父类)方法来保存其基本值,或调用ObjectOutput的writeObject方法来保存对象、字符串和数组。
  • void readExternal(ObjectInput in):该方法在目标对象反序列化时调用。方法中调用DataInput(输入参数 ObjectInput的父类)方法来恢复其基础类型,或调用 readObject方法来恢复对象、字符串和数组。需要注意的是,readExternal 方法读取数据时,必须与 writeExternal方法写入数据时的顺序和类型一致。

下面通过示例来说明 writeExternal方法和 readExternal方法的作用。在代码中,我们设置了UserModel类的 writeExternal方法和 readExternal方法。

public class UserModel implements Externalizable {private static final long serialVerisionUID = 1L;private Integer id;private String name;private String description;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {System.out.println("writeExternal doing ...");out.write(id); // DataOutput中的方法out.writeObject(name + "(from writeExternal)");}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {System.out.println("readExternal doing ...");id = in.read();name = (String) in.readObject();System.out.println("name in file is:" + name);name = name + "(from readExternal)";}
}private static void demo() throws Exception {System.out.println("run demo:");UserModel userModel = new UserModel();userModel.setId(1);userModel.setName("shawn");ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m2.tempdata"));oos.writeObject(userModel);oos.flush();oos.close();System.out.println("↑write;↓read");ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m2.tempdata"));UserModel newUser = (UserModel) ois.readObject();System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());System.out.println();}

可见,对于实现了 Externalizable 接口的类,会在对象序列化时调用 writeExternal 方法,而在对象反序列化时调用 readExternal方法。我们可以通过自定义writeExternal方法和readExternal方法的具体实现,来控制对象的序列化和反序列化行为,这也使得继承 Externalizable接口实现序列化和反序列化更为自由和强大。

2.writeReplace方法和 readResolve方法

在进行序列化和反序列化的目标类(可以继承 Serializable 接口,也可以继承Externalizable接口)中,还可以定义两个方法:writeReplace方法和 readResolve方法。

  • writeReplace:如果一个类中定义了该方法,则对该类的对象进行序列化操作前会先调用该方法。最终该方法返回的对象将被序列化。
  • readResolve:如果一个类中定义了该方法,则对该类的对象进行反序列化操作后会调用该方法。最终该方法返回的对象将作为反序列化的结果。

writeReplace 方法和 readResolve 方法实际上为对象的序列化和反序列化提供了一种“偷梁换柱”的能力:无论实际对象如何,在序列化时都以 writeReplace 方法的返回值为准;无论序列化数据如何,在反序列化时都以 readResolve方法的返回值为准。

下面以 writeReplace方法为例,演示一下这种能力。我们在 UserModel 类中定义了writeReplace方法,并在 writeReplace方法中返回一个全新的对象,如下所示。

public class UserModel implements Serializable {private static final long serialVerisionUID = 123L;private Integer id;private String name;private String description;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}private Object writeReplace() throws ObjectStreamException {System.out.println("writeReplace doing ...");UserModel userModel = new UserModel();userModel.setId(2);userModel.setName("yeecode");userModel.setDescription("description from writeReplace");return userModel;}
}private static void demo() throws Exception {System.out.println("run demo:");UserModel userModel = new UserModel();userModel.setId(1);userModel.setName("shawn");ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m3.tempdata"));oos.writeObject(userModel);oos.flush();oos.close();System.out.println("↑write;↓read");ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m3.tempdata"));UserModel newUser = (UserModel) ois.readObject();System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());System.out.println();}

可见,无论实际 UserModel对象如何,最终的序列化都是按照 writeReplace方法输出的对象展开的。writeReplace方法确实在序列化过程中起到了“偷梁换柱”的效果。readResolve也有类似的能力,只不过是在反序列化阶段生效。

3.序列化方法和反序列化方法的执行顺序

上面我们了解了 writeExternal、readExternal和 writeReplace、readResolve四个方法,那么这四个方法具体的执行顺序如何呢?下面直接通过一个示例进行演示。在示例中,我们同时定义了以上四种方法,如代码所示。

public class UserModel05 implements Externalizable {private static final long serialVerisionUID = 1L;private Integer id;private String name;private String description;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}@Overridepublic void writeExternal(ObjectOutput out) throws IOException {System.out.println("writeExternal doing ...");out.write(id);out.writeObject(name + "(from writeExternal)");}@Overridepublic void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {System.out.println("readExternal doing ...");id = in.read();name = (String) in.readObject();System.out.println("name in file is:" + name);name = name + "(from readExternal)";}private Object writeReplace() throws ObjectStreamException {System.out.println("writeReplace doing ...");UserModel05 userModel = new UserModel05();userModel.setId(2);userModel.setName(name + "(from writeReplace)");userModel.setDescription("description from writeReplace");return userModel;}private Object readResolve() throws ObjectStreamException {System.out.println("readResolve doing ...");UserModel05 userModel = new UserModel05();userModel.setId(2);userModel.setName(name + "(from readResolve)");userModel.setDescription("description from readResolve");return userModel;}
}private static void demo05() throws Exception {System.out.println("run demo05:");UserModel05 userModel05 = new UserModel05();userModel05.setId(1);userModel05.setName("shawn");ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("m5.tempdata"));oos.writeObject(userModel05);oos.flush();oos.close();System.out.println("↑write;↓read");ObjectInputStream ois = new ObjectInputStream(new FileInputStream("m5.tempdata"));UserModel05 newUser = (UserModel05) ois.readObject();System.out.println("newUser:" + newUser.getId() + "-" + newUser.getName());System.out.println("description:" + newUser.getDescription());System.out.println();}

四个方法的执行顺序依次为:writeReplace、writeExternal、readExternal、readResolve。我们可以将继承 Externalizable接口的类的序列化和反序列化流程展示出来。

继承 Serializable接口的类的序列化和反序列化流程相对简单一些,如下图所示。

5.1.4 ThreadLocal

防止一个对象被多个线程同时读写是多线程编程中非常重要的工作,通常可以使用加锁等方式来实现。那有没有一种方式可以用来彻底避免这种情况呢?有,ThreadLocal就是其中的一种。一个对象会被多个线程访问,是因为多个线程共享了这个对象。我们只要把对象转变为线程独有的,就可以避免这种情况。这是一种以“空间换时间”的思路。

  • 当一个对象被多个线程共享时,节约了存储该对象的空间;但是在访问该对象时需要多个线程排队进行,这样便浪费了时间。
  • 当我们将对象设置为线程独享时,每个线程都可以无须排队而自由访问对象,节约了时间;但是同一个对象可能在多个线程中存在拷贝,这样就浪费了存储空间。

当然,有一些数据在多个线程之间共享是多个线程之间通信的需要,这种情况不在此列。时间与空间的矛盾在程序设计中会经常出现,我们需要根据不同的场景选择不同的方案。而ThreadLocal 是典型的“时间换空间”思路的应用,每个线程都独有一个ThreadLocal,可以在其中存放独属于该线程的数据。ThreadLocal的主要方法有:

  • T get():从 ThreadLocal中读取数据;
  • void set(T value):向 ThreadLocal中写入数据;
  • void remove():从 ThreadLocal中删除数据。

下面通过示例展示ThreadLocal的使用,并证明ThreadLocal空间是归各个线程独享的。我们创建了 threadLocalNumber 和 threadLocalString 这两个ThreadLocal 变量。代码中共有三个线程,分别是 main 方法所在的主线程、执行Task01任务的 thread01线程、执行 Task02任务的 thread02线程。在每个线程中,我们都对这两个 ThreadLocal变量进行读写操作。


public class DemoApplication {// 创建了两个ThreadLocal变量private static ThreadLocal<Integer> threadLocalNumber = new ThreadLocal<>();private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();public static void main(String[] args) {try {Thread thread01 = new Thread(new Task01());Thread thread02 = new Thread(new Task02());thread01.start();thread02.start();Thread.sleep(2L);System.out.println("Main-number: " + threadLocalNumber.get());System.out.println("Main-string: " + threadLocalString.get());} catch (Exception ex) {ex.printStackTrace();}}private static class Task01 implements Runnable {@Overridepublic void run() {System.out.println("Thread01-number: " + threadLocalNumber.get());System.out.println("Set Thread01-number : 3001");threadLocalNumber.set(3001);System.out.println("Thread01-number: " + threadLocalNumber.get());System.out.println("Set Thread01-string : hello thread01");threadLocalString.set("hello thread01");System.out.println("Thread01-string: " + threadLocalString.get());}}private static class Task02 implements Runnable {@Overridepublic void run() {System.out.println("Set Thread02-number : 3002");threadLocalNumber.set(3002);System.out.println("Thread02-number: " + threadLocalNumber.get());System.out.println("Thread02-string: " + threadLocalString.get());System.out.println("Set Thread02-string : hello thread02");threadLocalString.set("hello thread02");System.out.println("Thread02-string: " + threadLocalString.get());}}
}

每个线程操作的 ThreadLocal 变量都是线程内部的变量,不会对其他线程造成任何干扰。在多线程程序中,当我们需要保存一些线程独有的数据时,可以借助 ThreadLocal来实现。

5.1.5 存储过程

存储过程(Stored Procedure)是数据库中的一段可以被重用的代码片段,可以通过外部调用完成较为复杂的操作。在调用时,可以为存储过程传入输入参数,而存储过程执行结束后也可以给出输出参数。主流的数据库都支持存储过程,MySQL也不例外。下面我们以 MySQL为例介绍存储过程的创建及使用。存储过程的创建并不复杂,创建语句格式如下所示。

其中存储过程的参数分为以下三类。

  • IN:输入参数,该参数向存储过程输入值,但是不能从存储过程中返回值。
  • OUT:输出参数,该参数可以从存储过程中返回值,但是不能向存储过程输入值。
  • INOUT:双向参数,该参数既可以向存储过程输入值,又可以从存储过程中返回值。

在过程体中可以定义具体的操作,包括自定义变量,读取参数的值,设置参数的值,执行增、删、改、查操作,进行逻辑判断等。存储过程创建之后,便可以进行存储过程的查询、调用、删除等工作。其中调用存储过程的语句格式如下所示。

相比于普通的 SQL语句,存储过程支持变量定义、逻辑判断、数据校验和多输出等,功能更为强大。并且基于存储过程还可以将操作逻辑封装到数据库中,提高了逻辑的保密性。但是将操作逻辑封装到数据库中也会带来逻辑不清晰、与数据库耦合度高等问题。在使用中,要根据具体使用场景判断是否使用存储过程。

5.1.6 Statement及其子接口

Statement 接口中定义了一些抽象方法能用来执行静态 SQL 语句并返回结果,通常返回的结果是一个结果集 ResultSet。这一节我们详细介绍 java.sql包下的 Statement接口及其在该包下的子接口。Statement 有一个子接口 PreparedStatement,而 PreparedStatement 又有一个子接口CallableStatement,其继承关系如下图所示。在继承关系中,通常子类会继承父类的方法并在此基础上进行扩充,从而使得子类的功能成为父类功能的超集。Statement 接口及其子接口就是这样的,从 Statement 接口到CallableStatement接口,功能逐渐增强。

Statement 接口、PreparedStatement 接口、CallableStatement 接口依次对应我们在设置SQL语句时的简单语句、预编译语句、存储过程语句。关于 Statement接口中的主要方法主要用来执行操作并获取操作结果。PreparedStatement 子接口除了继承 Statement 接口的全部方法外,还新定义了一些方法。这些方法主要是一些 set方法,如下面的 setInt方法。

这些新增的 set方法(setLong、setString、setObject等)使得预编译的 SQL语句具有了按照参数位置对参数赋值的功能。CallableStatement 则在 PreparedStatement 的基础上进一步增加了方法,这些方法主要包括以下四类。

  • 按照参数名称赋值方法:这一类方法能够为存储过程中指定名称的参数赋值。例如,setInt(String,int)方法就属于这一类。
  • 注册输出参数方法:这一类方法能够向存储过程注册输出参数。例如,registerOutParameter(int,int)方法就属于这一类。
  • 按照参数位置读取值方法:这一类方法能够读取存储过程指定位置的参数值。例如,getInt(int)方法就属于这一类方法。
  • 按照参数名称读取值方法:这一类方法能够读取存储过程指定名称的参数的值。例如,getInt(String)方法就属于这一类方法。

因此,从 Statement接口到 PreparedStatement接口再到 CallableStatement接口,功能越来越强大。这就意味着SQL语句中,从简单语句到预编译语句再到存储过程语句,它们支持的功能越来越多。下图给出 Statement及其子接口的功能演进图。在后续节我们会发现,MyBatis就是通过继承这些接口来完成不同语句类型的处理的。

5.2 主键自增功能

在进行数据插入操作时,经常需要一个自增生成的主键编号,这既能保证主键的唯一性,又能保证主键的连续性。许多数据库都支持主键自增功能,如 MySQL数据库、SQL Server数据库等。当然也有一些数据库不支持主键自增功能,如 Oracle数据库。MyBatis的 executor包中的 keygen子包兼容以上这两种情况。keygen子包中一共包含四个类或接口。

KeyGenerator作为接口提供了两个方法,即 processBefore方法和 processAfter方法。关于这两个方法的实现细节我们会在下面分别介绍。NoKeyGenerator不提供任何主键自增功能,其 processBefore方法和 processAfter方法均为空方法。因此,我们不再介绍该类。

5.2.1 主键自增的配置与生效

在阅读 MyBatis的主键自增相关代码之前,先了解怎么在 MyBatis中启用主键自增功能。通过 KeyGenerator 的类图我们知道,MyBatis 中的 KeyGenerator 实现类共有三种:Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator。在实际使用时,这三种实现类中只能有一种实现类生效。而如果生效的是 NoKeyGenerator,则代表不具有任何的主键自增功能。要启用 Jdbc3KeyGenerator,可以在配置文件中增加如下代码所示的配置。

或者直接在相关语句上启用 useGeneratedKeys,如下所示。

如果要启用 SelectKeyGenerator,则需要在 SQL语句前加一段 selectKey标签,如下代码所示。

如果某一条语句中同时设置了 useGeneratedKeys和 selectKey,则后者生效。以上各个配置项的作用范围、优先级等结论,均可以通过阅读代码得出。在 XMLStatementBuilder类中,在集成开发软件的帮助下,我们可以通过查找KeyGenerator的引用找到。这段代码就是主键自增功能被解析的地方。

    // 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中processSelectKeyNodes(id, parameterTypeClass, langDriver);// 此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除,开始进行SQL解析KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);// 判断是否已经有解析好的KeyGeneratorif (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {// 全局或者本语句只要启用自动key生成,则使用key生成keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}

processSelectKeyNodes 方法最终解析了 selectKey 节点的信息并在解析完成后将selectKey节点从XML中删除了,而解析出来的信息则放入了 configuration的 keyGenerators中。之后,如果没有解析好的 KeyGenerator,则会根据 useGeneratedKeys 判断是否使用Jdbc3KeyGenerator。最终,KeyGenerator信息会被保存在整个 Statement中。在 Statement执行时,直接调用KeyGenerator 中的 processBefore 方法和 processAfter 方法即可,必然会有Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator三者中的一个来实际执行这两个方法。接下来我们介绍 Jdbc3KeyGenerator和 SelectKeyGenerator的源码。

5.2.2 Jdbc3KeyGenerator类

1.Jdbc3KeyGenerator类的功能

Jdbc3KeyGenerator类是为具有主键自增功能的数据库准备的。说到这里大家可能会疑惑,既然数据库已经支持主键自增了,那 Jdbc3KeyGenerator类存在的意义是什么呢?它存在的意义是提供自增主键的回写功能。下面通过示例来说明此功能。首先对 user 表中的 id字段启用主键自增功能,其次配置XML映射文件,如下所示。这里并没有启用主键自增功能

因为在数据库中对 id字段启用了自增功能,所以在数据插入操作结束后,数据库中的user04的 id字段会被设置为一个数值。然而,Java程序中的 user04对象却不会被更新,因此输出的 user04的 id值仍然为 null。

Jdbc3KeyGenerator类提供的回写功能能够将数据库中产生的 id值回写给 Java对象本身。我们可以通过下面的设置启用 Jdbc3KeyGenerator类,如代码所示。

因此 Jdbc3KeyGenerator类所做的工作就是在 Java对象插入完成后,将数据库自增产生的 id读取出来,然后回写给 Java对象本身。其功能示意图如图所示,其中粗实线表示的工作就是Jdbc3KeyGenerator类完成的。

2.Jdbc3KeyGenerator类的原理

Jdbc3KeyGenerator类的工作是在数据库主键自增结束后,将自增出来的主键读取出来并赋给 Java对象。这些工作都是在数据插入完成后进行的,即在 processAfter 方法中进行。而 processBefore方法中不需要进行任何操作。processAfter方法直接调用了 processBatch方法。在阅读 processBatch方法前我们先复习一个小的知识点:Statement对象的 getGeneratedKeys方法能返回此语句操作自增生成的主键,如果此语句没有产生自增主键,则结果为空 ResultSet对象。

接下来我们直接阅读代码给出的带注释的 processBatch 方法源码。该方法的主要工作就是调用 Statement对象的 getGeneratedKeys方法获取数据库自增生成的主键,然后将主键赋给实参以达到回写的目的。

  public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {// 拿到主键的属性名final String[] keyProperties = ms.getKeyProperties();if (keyProperties == null || keyProperties.length == 0) {// 没有主键则无需操作return;}// 调用Statement对象的getGeneratedKeys方法获取自动生成的主键值try (ResultSet rs = stmt.getGeneratedKeys()) {// 获取输出结果的描述信息final ResultSetMetaData rsmd = rs.getMetaData();final Configuration configuration = ms.getConfiguration();if (rsmd.getColumnCount() < keyProperties.length) {// 主键数目比结果的总字段数目还多,则发生了错误。// 但因为此处是获取主键这样的附属操作,因此忽略错误,不影响主要工作} else {// 调用子方法,将主键值赋给实参assignKeys(configuration, rs, rsmd, keyProperties, parameter);}} catch (Exception e) {throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);}}

5.2.3 SelectKeyGenerator类

Jdbc3KeyGenerator类其实并没有真正地生成自增主键,而只是将数据库自增出的主键值回写到了Java对象中。因此,面对不支持主键自增功能的数据库时,Jdbc3KeyGenerator类将无能为力。这时就需要 SelectKeyGenerator类,因为它可以真正地生成自增的主键。

SelectKeyGenerator类实现了 processBefore和 processAfter这两个方法。这两个方法均直接调用了子方法 processGeneratedKeys,这可能会让看到源码的我们感到疑惑。为了解答这个疑惑,我们先介绍 SelectKeyGenerator类的功能。

  /*** 数据插入前进行的操作* @param executor 执行器* @param ms 映射语句对象* @param stmt Statement对象* @param parameter SQL语句实参对象*/@Overridepublic void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {if (executeBefore) {processGeneratedKeys(executor, ms, parameter);}}/*** 数据插入后进行的操作* @param executor 执行器* @param ms 映射语句对象* @param stmt Statement对象* @param parameter SQL语句实参对象*/@Overridepublic void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {if (!executeBefore) {processGeneratedKeys(executor, ms, parameter);}}

SelectKeyGenerator 类的功能描述起来很简单:先执行一段特定的 SQL 语句获取一个值,然后将该值赋给 Java对象的自增属性。然而,SelectKeyGenerator类这一功能的执行时机分为以下两种,这两种执行时机通过配置二选一。

  • 在数据插入之前执行。执行完特定的 SQL 语句并将值赋给对象的自增属性后,再将这个完整的对象插入数据库中。而这种操作又分为两种情况:

- 如果数据库没有设置或者不支持主键自增,则完整的对象会被完整地插入数据库中。这是SelectKeyGenerator类最常用的使用场景。

- 如果数据库设置了主键自增,则刚才特定 SQL语句生成的自增属性值会被数据库自身的自增值覆盖掉。这种情况下,Java对象的自增属性值可能会和数据库中的自增属性值不一致,因此是错误的。这种情况下,建议使用 Jdbc3KeyGenerator类的回写功能。

  • 在数据插入之后执行。对象插入数据库结束后,Java对象的自增属性被设置成特定SQL语句的执行结果。这种操作也分为两种情况:

- 如果数据库不支持主键自增,则之前被插入数据库中的对象的自增属性是没有被赋值的,而 Java对象的自增属性却被赋值了,这会导致不一致。这种操作是错误的。

- 如果数据库设置了主键自增,则数据库自增生成的值和 SQL语句执行产生的值可能不一样。不过我们一般通过设置特定的 SQL语句来保证两者一致,这其实和 Jdbc3KeyGenerator类的回写功能类似。

可见 SelectKeyGenerator类的功能描述起来简单又灵活,但是因为执行时机、数据库状况等不同可能产生多种情况,需要使用者自己把握。SelectKeyGenerator类的功能示意图如下所示。

processBefore和 processAfter这两个方法都直接调用了 processGeneratedKeys方法,所以processGeneratedKeys方法的功能就是执行一段 SQL语句后获取一个值,然后将该值赋给 Java对象的自增属性。在阅读 processGeneratedKeys方法之前,我们先对 SelectKeyGenerator类中的属性进行介绍:

  // 用户生成主键的SQL语句的特有标志,该标志会追加在用于生成主键的SQL语句的id的后方public static final String SELECT_KEY_SUFFIX = "!selectKey";// 插入前执行还是插入后执行private final boolean executeBefore;// 用户生成主键的SQL语句private final MappedStatement keyStatement;/*** 执行一段SQL语句后获取一个值,然后将该值赋给Java对象的自增属性** @param executor 执行器* @param ms 插入操作的SQL语句(不是生成主键的SQL语句)* @param parameter 插入操作的对象*/private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {try {// keyStatement为生成主键的SQL语句;keyStatement.getKeyProperties()拿到的是要自增的属性if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {// 要自增的属性String[] keyProperties = keyStatement.getKeyProperties();final Configuration configuration = ms.getConfiguration();final MetaObject metaParam = configuration.newMetaObject(parameter);if (keyProperties != null) {// 为生成主键的SQL语句创建执行器keyExecutor。// 原注释:不要关闭keyExecutor,因为它会被父级的执行器关闭Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);// 执行SQL语句,得到主键值List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);// 主键值必须唯一if (values.size() == 0) {throw new ExecutorException("SelectKey returned no data.");} else if (values.size() > 1) {throw new ExecutorException("SelectKey returned more than one value.");} else {MetaObject metaResult = configuration.newMetaObject(values.get(0));if (keyProperties.length == 1) {// 要自增的主键只有一个,为其赋值if (metaResult.hasGetter(keyProperties[0])) {// 从metaResult中用getter得到主键值setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));} else {// 可能返回的直接就是主键值本身setValue(metaParam, keyProperties[0], values.get(0));}} else {// 要把执行SQL得到的值赋给多个属性handleMultipleProperties(keyProperties, metaParam, metaResult);}}}}} catch (ExecutorException e) {throw e;} catch (Exception e) {throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);}}

这样,我们对 SelectKeyGenerator 类的功能及如何实现这些功能进行了详细的介绍。因此,我们可以将 SelectKeyGenerator类作为 Jdbc3KeyGenerator类的升级版或自由定制版。

  • SelectKeyGenerator类可以设置为插入前执行并实现主键的主动生成,而且可以通过SQL语句设置主键生成方式。这是 Jdbc3KeyGenerator类没有的功能。
  • SelectKeyGenerator类可以设置为插入后执行。通过将主键生成 SQL语句设置为类似“SELECTLAST_INSERT_ID()”的语句便可以实现主键回写功能。

5.3 懒加载功能

5.3.1 懒加载功能的使用

在进行跨表数据查询的时候,常出现先查询表A,再根据表A的输出结果查询表B的情况。而有些时候,我们从表A中查询出来的数据,只有部分需要查询表B。例如,我们需要从 user表查询用户信息并打印所有用户的姓名列表。而查询出的用户中,只有满足“user.getAge()==18”的用户才需要查询该用户在 task表中的信息。

我们可以先从 user表获取用户信息,然后再从 task表查询所有用户的任务信息。这一定是可行的,但是这样操作会查询出许多多余的结果,所有不满足“user.getAge()==18”的用户任务信息都是多余的。一种更好的方案是先从 user表获取用户信息,然后根据需要(即是否满足“user.getAge()==18”)决定是否查询该用户在 task表中的信息。这种先加载必需的信息,然后再根据需要进一步加载信息的方式叫作懒加载。MyBatis支持数据的懒加载。要想使用懒加载,需要在 MyBatis的配置文件中启用该功能,如下所示。

aggressiveLazyLoading 是激进懒加载设置,我们对该属性进行一些说明:当aggressiveLazyLoading设置为 true时,对对象任一属性的读或写操作都会触发该对象所有懒加载属性的加载;当 aggressiveLazyLoading设置为 false时,对对象某一懒加载属性的读操作会触发该属性的加载。无论 aggressiveLazyLoading的设置如何,调用对象的“equals”“clone”“hashCode”“toString”中任意一个方法都会触发该对象所有懒加载属性的加载。在后面的源码阅读中,我们会清晰地看到 aggressiveLazyLoading设置项如何生效。接下来还需要设置好映射文件。

在“id="lazyLoadQuery"”的查询中,查询 user表是必需的操作,而在结果的映射中又需要查询 task表,因此它涉及两个表的查询。而只要不访问User对象的 taskList属性,则 task表的查询操作就是可以省略的。因此,User对象的 taskList就是可以懒加载的属性。

可以看出 MyBatis 先从 user 表查询了所有的用户信息,然后仅对满足“user.getAge()==18”的记录调用了 selectTask语句从 task表查询了任务信息,而没有对不符合条件的其他记录调用selectTask语句。因此,整个过程是存在懒加载的。

5.3.2 懒加载功能的实现

1.懒加载功能框架

懒加载功能的实现还是相对复杂的,为便于理解,我们先简要给出 MyBatis中懒加载的实现原理,这对后面的源码阅读有着重要的帮助。整个懒加载过程可以简化如下。

  1. 先查询 user表,获得 User对象。
  2. 将返回的 User对象替换为 User对象的代理对象 UserProxy对象,并返回上层应用。UserProxy对象有以下特点。
  • 当属性的写方法被调用时,直接将属性值写入被代理对象。
  • 当属性的读方法被调用时,判断是否为懒加载属性。如果不是懒加载属性,则直接由被代理对象返回;如果是懒加载属性,则根据配置加载该属性,然后再返回。

上述只是一个经过抽象的简化过程,实际的懒加载原理要复杂许多。下图给出了loader子包中核心类的类图。

在了解了懒加载的基本实现原理之后,我们参照 loader子包的类图对懒加载功能中涉及的类进行源码阅读。

2.代理工厂

ProxyFactory是创建代理类的工厂接口,其中的 setProperties方法用来对工厂进行属性设置。但是MyBatis内置的两个实现类均没有实现该接口,故不支持属性设置。createProxy方法用来创建一个代理对象。

ProxyFactory接口有两个实现类,即 CglibProxyFactory类和 JavassistProxyFactory类。这两个实现类整体结构高度一致,甚至内部类、方法设置都一样,只是实现原理不同,一个基于 cglib实现,另一个基于 Javassist实现。接下来我们以 CglibProxyFactory类为例进行源码分析。

CglibProxyFactory类中提供了两个创建代理对象的方法。其中 createProxy方法重写了ProxyFactory 接口中的方法,用来创建一个普通的代理对象;createDeserializationProxy 方法用来创建一个反序列化的代理对象。

createProxy方法创建的代理对象是内部类 EnhancedResultObjectProxyImpl的实例。首先看一下EnhancedResultObjectProxyImpl内部类的属性,如下所示。

    // 被代理类private final Class<?> type;// 要懒加载的属性Mapprivate final ResultLoaderMap lazyLoader;// 是否是激进懒加载private final boolean aggressive;// 能够触发懒加载的方法名“equals”, “clone”, “hashCode”, “toString”。这四个方法名在Configuration中被初始化。private final Set<String> lazyLoadTriggerMethods;// 对象工厂private final ObjectFactory objectFactory;// 被代理类构造函数的参数类型列表private final List<Class<?>> constructorArgTypes;// 被代理类构造函数的参数列表private final List<Object> constructorArgs;

代理类中最核心的方法是 intercept方法。当被代理类的方法被调用时,都会被拦截进该方法。在介绍 intercept方法之前,我们先了解两个方法:finalize方法和 writeReplace方法。因为在 intercept方法中,对这两种方法进行了排除。

  • finalize方法:在 JVM进行垃圾回收前,允许使用 finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
  • writeReplace方法:不再赘述。

下面我们阅读 EnhancedResultObjectProxyImpl类中 intercept方法的源码。

    /*** 代理类的拦截方法* @param enhanced 代理对象本身* @param method 被调用的方法* @param args 每调用的方法的参数* @param methodProxy 用来调用父类的代理* @return 方法返回值* @throws Throwable*/@Overridepublic Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {// 取出被代理类中此次被调用的方法的名称final String methodName = method.getName();try {synchronized (lazyLoader) { // 防止属性的并发加载if (WRITE_REPLACE_METHOD.equals(methodName)) { // 被调用的是writeReplace方法// 创建一个原始对象Object original;if (constructorArgTypes.isEmpty()) {original = objectFactory.create(type);} else {original = objectFactory.create(type, constructorArgTypes, constructorArgs);}// 将被代理对象的属性拷贝进入新创建的对象PropertyCopier.copyBeanProperties(type, enhanced, original);if (lazyLoader.size() > 0) { // 存在懒加载属性// 则此时返回的信息要更多,不仅仅是原对象,还有相关的懒加载的设置等信息。因此使用CglibSerialStateHolder进行一次封装return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);} else {// 没有未懒加载的属性了,那直接返回原对象进行序列化return original;}} else {if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) { // 存在懒加载属性且被调用的不是finalize方法if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { // 设置了激进懒加载或者被调用的方法是能够触发全局懒加载的方法// 完成所有属性的懒加载lazyLoader.loadAll();} else if (PropertyNamer.isSetter(methodName)) { // 调用了属性写方法// 则先清除该属性的懒加载设置。该属性不需要被懒加载了final String property = PropertyNamer.methodToProperty(methodName);lazyLoader.remove(property);} else if (PropertyNamer.isGetter(methodName)) { // 调用了属性读方法final String property = PropertyNamer.methodToProperty(methodName);// 如果该属性是尚未加载的懒加载属性,则进行懒加载if (lazyLoader.hasLoader(property)) {lazyLoader.load(property);}}}}}// 触发被代理类的相应方法。能够进行到这里的是除去writeReplace方法外的方法,例如读写方法、toString方法等return methodProxy.invokeSuper(enhanced, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}}

接下来我们分析一下 intercept方法的实现逻辑。其中被代理对象的 writeReplace方法被调用的情况,我们会在下节单独介绍。被代理对象的 finalize方法被调用时,代理对象不需要做任何特殊处理。而被代理对象的其他方法被调用时,intercept方法的处理方式如下。

  • 如果设置了激进懒加载或者被调用的是触发全局加载的方法,则直接加载所有未加载的属性。
  • 如果被调用的是属性写方法,则将该方法从懒加载列表中删除,因为此时数据库中的数据已经不是最新的了,没有必要再去加载。然后进行属性的写入操作。
  • 如果被调用的是属性读方法,且该属性尚未被懒加载的话,则加载该属性;如果该属性已经懒加载过,则直接读取该属性。

以上整个逻辑和上述的简化逻辑基本一致,只是在细节上考虑了更多的情况。

3.ResultLoaderMap类 

被代理对象可能会有多个属性可以被懒加载,这些尚未完成加载的属性是在ResultLoaderMap 类的实例中存储的。ResultLoaderMap 类主要就是一个 HashMap 类,该HashMap类中的键为属性名的大写,值为 LoadPair对象。LoadPair 类是 ResultLoaderMap 类的内部类,它能够实现对应属性的懒加载操作。我们首先看一下 LoadPair的属性。

    /*** Name of factory method which returns database connection.*/// 用来根据反射得到数据库连接的方法名private static final String FACTORY_METHOD = "getConfiguration";/*** Object to check whether we went through serialization.*/// 判断是否经过了序列化的标志位,因为该属性被设置了transient,经过一次序列化和反序列化后会变为nullprivate final transient Object serializationCheck = new Object();/*** Meta object which sets loaded properties.*/// 输出结果对象的封装private transient MetaObject metaResultObject;/*** Result loader which loads unread properties.*/// 用以加载未加载属性的加载器private transient ResultLoader resultLoader;/*** Wow, logger.*/// 日志记录器private transient Log log;/*** Factory class through which we get database connection.*/// 用来获取数据库连接的工厂private Class<?> configurationFactory;/*** Name of the unread property.*/// 未加载的属性的属性名private String property;/*** ID of SQL statement which loads the property.**/// 能够加载未加载属性的SQL的编号private String mappedStatement;/*** Parameter of the sql statement.*/// 能够加载未加载属性的SQL的参数private Serializable mappedParameter;

指定属性的加载操作由 LoadPair中的 load方法来完成,其带注释的源码如下所示。

    /*** 进行加载操作* @param userObject 需要被懒加载的对象(只有当this.metaResultObject == null || this.resultLoader == null才生效,否则会采用属性metaResultObject对应的对象)* @throws SQLException*/public void load(final Object userObject) throws SQLException {if (this.metaResultObject == null || this.resultLoader == null) { // 输出结果对象的封装不存在或者输出结果加载器不存在// 判断用以加载属性的对应的SQL语句存在if (this.mappedParameter == null) {throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "+ "required parameter of mapped statement ["+ this.mappedStatement + "] is not serializable.");}final Configuration config = this.getConfiguration();// 取出用来加载结果的SQL语句final MappedStatement ms = config.getMappedStatement(this.mappedStatement);if (ms == null) {throw new ExecutorException("Cannot lazy load property [" + this.property+ "] of deserialized object [" + userObject.getClass()+ "] because configuration does not contain statement ["+ this.mappedStatement + "]");}// 创建结果对象的包装this.metaResultObject = config.newMetaObject(userObject);// 创建结果加载器this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,metaResultObject.getSetterType(this.property), null, null);}/* We are using a new executor because we may be (and likely are) on a new thread* and executors aren't thread safe. (Is this sufficient?)** A better approach would be making executors thread safe. */// 只要经历过持久化,则可能在别的线程中了。为这次惰性加载创建的新线程ResultLoaderif (this.serializationCheck == null) {// 取出原来的ResultLoader中的必要信息,然后创建一个新的// 这是因为load函数可能在不同的时间多次执行(第一次加载属性A,又过了好久加载属性B)。// 而该对象的各种属性是跟随对象的,加载属性B时还保留着加载属性A时的状态,即ResultLoader是加载属性A时设置的// 则此时ResultLoader中的Executor在ResultLoader中被替换成了一个能运行的Executor,而不是ClosedExecutor// 能运行的Executor的状态可能不是close,这将导致它被复用,从而引发多线程问题// 是不是被两次执行的一个关键点就是有没有经过序列化,因为执行完后会被序列化并持久化final ResultLoader old = this.resultLoader;this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,old.parameterObject, old.targetType, old.cacheKey, old.boundSql);}this.metaResultObject.setValue(property, this.resultLoader.loadResult());}

上述方法的设计包含很多非常巧妙的点,我们一一进行介绍。首先,懒加载的过程就是执行懒加载 SQL语句后,将查询结果使用输出结果加载器赋给输出结果元对象的过程。因此,load 方法首先会判断输出结果元对象 metaResultObject和输出结果加载器resultLoader是否存在。如果不存在的话,则会使用输入参数 userObject重新创建上述二者。然后,介绍 ClosedExecutor类的设计。ClosedExecutor类是 ResultLoaderMap类的内部类。该类只有一个 isClosed方法能正常工作,其他所有的方法都会抛出异常。然而就是这样的一个类,在创建ResultLoader时还是被使用。

        // 创建结果加载器this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,metaResultObject.getSetterType(this.property), null, null);

这是因为 ClosedExecutor类存在的目的就是通过 isClosed方法返回 true来表明自己是一个关闭的类,以保证让任何遇到 ClosedExecutor 对象的操作都会重新创建一个新的有实际功能的Executor。例如,在 ResultLoader中我们可以找到源码。

    // 初始化ResultLoader时传入的执行器Executor localExecutor = executor;if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {// 执行器关闭,或者执行器属于其他线程,则创建新的执行器localExecutor = newExecutor();}

可以看出,传入的 ClosedExecutor 对象总会触发 ResultLoader 创建新的 Executor 对象。所以,没有任何实际功能的 ClosedExecutor对象起到了占位符的作用。最后,我们介绍 load方法中与序列化和反序列化相关的设计。经过一次序列化和反序列化后,对象可能处在了全新的线程中;序列化和反序列化的时间间隔可能很长,原来的缓存信息也极有可能没有了意义。这些情况都需要懒加载过程进行特殊的处理。我们知道,在继承了 Serializable接口的类中,如果对某个属性使用 transient关键字修饰,就会使序列化操作忽略该属性。那么对序列化的结果进行反序列化操作时,就会导致该属性变为 null。基于此,LoadPair 中的 serializationCheck 属性被设计成了一个序列化标志位。只要 LoadPair对象经历过序列化和反序列化过程,就会使得 serializationCheck属性的值变为 null。

如果经历过序列化与反序列化,则当前的 LoadPair对象很有可能处在一个新的线程之中,因此继续使用之前的 ResultLoader 可能会引发多线程问题。所以,LoadPair 对象只要检测出自身经历过持久化,就会依赖老 ResultLoader 对象中的信息重新创建一个新ResultLoader对象。ResultLoader对象也被 transient修饰,因此真正老 ResultLoader对象也在序列化和反序列化的过程中消失了,与之一起消失的还有 MetaObject对象和 ResultLoader对象。因此这里所谓的老ResultLoader对象实际是在该 load方法中进入“(this.metaResultObject==null||this.resultLoader==null)”对应的分支后重新组建的。而重新组建的所谓的老 ResultLoader 对象与真正的老 ResultLoader 对象相比缺少了cacheKey和boundSql这两个参数。其中 cacheKey是为了加速查询而存在的,非必要并且缓存可能早已失效;而boundSql 会在后面的查询阶段重新补足,在 BaseStatementHandler的构造方法中就可以找到相关的代码片段。这样,序列化和反序列化引入的问题才被一一解决了。可见,牵涉序列化和反序列化之后,懒加载操作会变得十分复杂。

4.ResultLoader类

ResultLoader 类是一个结果加载器类,它负责完成数据的加载工作。因为懒加载只涉及查询,而不需要支持增、删、改的工作,因此它只有一个查询方法 selectList来进行数据的查询。

5.3.3 懒加载功能对序列化和反序列化的支持

在 5.3.2节中介绍的源码已经能够实现 MyBatis基本的懒加载功能,但是还有一个问题没有解决——序列化与反序列化问题。仍然以基于 cglib实现的懒加载为例。如果要对查询结果对象进行序列化,实际上是对代理对象即EnhancedResultObjectProxyImpl对象进行序列化,因为 EnhancedResultObject-ProxyImpl已经替换了被代理对象。我们查看 EnhancedResultObjectProxyImpl类的属性后会发现一个问题,即这些属性中并不包含已加载完成的属性(非懒加载的属性和已懒加载完的属性)。这意味着,只要对查询结果对象进行一次序列化和反序列化操作,则所有已加载完成的属性都会丢失。这种事情是不应该发生的。为了保证懒加载操作支持序列化和反序列化,则必须保证在序列化时将被代理对象和代理对象的所有信息全都保存。为此,load 子包中准备了一整套的机制。接下来我们就介绍这套机制。

在 CglibProxyFactory中创建代理对象时,无论是创建 EnhancedResultObjectProxyImpl类型的代理对象还是创建 EnhancedDeserializationProxyImpl 类型的代理对象,都会在它们的构造方法中调用代码所示的 createProxy方法。

  /*** 创建代理对象* @param type 被代理对象类型* @param callback 回调对象* @param constructorArgTypes 构造方法参数类型列表* @param constructorArgs 构造方法参数类型* @return 代理对象*/static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {Enhancer enhancer = new Enhancer();enhancer.setCallback(callback);// 创建的是代理对象是原对象的子类enhancer.setSuperclass(type);try {// 获取类中的writeReplace方法type.getDeclaredMethod(WRITE_REPLACE_METHOD);if (LogHolder.log.isDebugEnabled()) {LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");}} catch (NoSuchMethodException e) {// 如果没找到writeReplace方法,则设置代理类继承WriteReplaceInterface接口,该接口中有writeReplace方法enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});} catch (SecurityException e) {// 什么都不做}Object enhanced;if (constructorArgTypes.isEmpty()) {enhanced = enhancer.create();} else {Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);enhanced = enhancer.create(typesArray, valuesArray);}return enhanced;}

createProxy 方法中一个重要的操作是校验被代理类中是否含有writeReplace方法。如果被代理类没有该方法,则会让代理类继承 WriteReplaceInterface从而获得一个writeReplace方法。writeReplace方法会在对象序列化前被调用,起到“偷梁换柱”的作用。在被代理类中植入了 writeReplace 方法后,在被代理对象被序列化时,则会调用该方法。而在EnhancedResultObjectProxyImpl类的 intercept方法中,已经对 writeReplace方法进行了特殊处理,负责特殊处理的源码如下所示。

          if (WRITE_REPLACE_METHOD.equals(methodName)) { // 被调用的是writeReplace方法// 创建一个原始对象Object original;if (constructorArgTypes.isEmpty()) {original = objectFactory.create(type);} else {original = objectFactory.create(type, constructorArgTypes, constructorArgs);}// 将被代理对象的属性拷贝进入新创建的对象PropertyCopier.copyBeanProperties(type, enhanced, original);if (lazyLoader.size() > 0) { // 存在懒加载属性// 则此时返回的信息要更多,不仅仅是原对象,还有相关的懒加载的设置等信息。因此使用CglibSerialStateHolder进行一次封装return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);} else {// 没有未懒加载的属性了,那直接返回原对象进行序列化return original;}}

通过代码可以看出,对代理对象进行持久化操作时,如果被代理对象还有尚未懒加载的属性,则最终持久化的是一个 CglibSerialStateHolder 对象。这一切是基于writeReplace提供的“偷梁换柱”功能实现的。CglibSerialStateHolder是 AbstractSerialStateHolder类的子类,AbstractSerialStateHolder类的带注释的属性如下所示。可见其中既包含了被代理对象的信息,又包含了尚未加载属性的信息。而 CglibSerialStateHolder类作为其子类会继承这些属性。

  private static final long serialVersionUID = 8940388717901644661L;private static final ThreadLocal<ObjectOutputStream> stream = new ThreadLocal<>();// 序列化后的对象private byte[] userBeanBytes = new byte[0];// 原对象private Object userBean;// 未加载的属性private Map<String, ResultLoaderMap.LoadPair> unloadedProperties;// 对象工厂,创建对象时使用private ObjectFactory objectFactory;// 构造函数的属性类型列表,创建对象时使用private Class<?>[] constructorArgTypes;// 构造函数的属性列表,创建对象时使用private Object[] constructorArgs;

我们可以在 AbstractSerialStateHolder中看到 writeExternal方法。

  /*** 对对象进行序列化* @param out 序列化结果将存入的流* @throws IOException*/@Overridepublic final void writeExternal(final ObjectOutput out) throws IOException {// 判断是不是该线程的第一轮写入boolean firstRound = false;final ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream os = stream.get();if (os == null) {// 之前没有结果,所以是第一轮写入os = new ObjectOutputStream(baos);firstRound = true;stream.set(os);}os.writeObject(this.userBean);os.writeObject(this.unloadedProperties);os.writeObject(this.objectFactory);os.writeObject(this.constructorArgTypes);os.writeObject(this.constructorArgs);final byte[] bytes = baos.toByteArray();out.writeObject(bytes);if (firstRound) {stream.remove();}}

将序列化的原理研究清楚后,我们再研究反序列化的过程。反序列化时,会调用AbstractSerialStateHolder中的 readResolve方法,如下所示。

  /*** 反序列化时被调用,给出反序列化的对象* @return 最终给出的反序列化对象* @throws ObjectStreamException*/@SuppressWarnings("unchecked")protected final Object readResolve() throws ObjectStreamException {// 非第一次运行,直接输出已经解析好的被代理对象if (this.userBean != null && this.userBeanBytes.length == 0) {return this.userBean;}// 第一次运行时,反序列化输出try (ObjectInputStream in = new LookAheadObjectInputStream(new ByteArrayInputStream(this.userBeanBytes))) {this.userBean = in.readObject();this.unloadedProperties = (Map<String, ResultLoaderMap.LoadPair>) in.readObject();this.objectFactory = (ObjectFactory) in.readObject();this.constructorArgTypes = (Class<?>[]) in.readObject();this.constructorArgs = (Object[]) in.readObject();} catch (final IOException ex) {throw (ObjectStreamException) new StreamCorruptedException().initCause(ex);} catch (final ClassNotFoundException ex) {throw (ObjectStreamException) new InvalidClassException(ex.getLocalizedMessage()).initCause(ex);}final Map<String, ResultLoaderMap.LoadPair> arrayProps = new HashMap<>(this.unloadedProperties);final List<Class<?>> arrayTypes = Arrays.asList(this.constructorArgTypes);final List<Object> arrayValues = Arrays.asList(this.constructorArgs);// 创建一个反序列化的代理输出,因此还是一个代理return this.createDeserializationProxy(userBean, arrayProps, objectFactory, arrayTypes, arrayValues);}

在这里会将之前序列化的结果反序列化,最终给出一个 EnhancedDeserializationProxy-Impl对象,它也是一个代理对象。EnhancedDeserializationProxyImpl类是 AbstractEnhanced-DeserializationProxy的子类。

反序列化过程中还对结果进行了缓存。这样,对同一个对象多次反序列化时除了第一次需要进行实际的反序列化操作外,之后只需将属性中缓存的结果直接返回即可,提高了反序列化的效率。

5.4 语句处理功能

5.4.1 MyBatis对多语句类型的支持

在 MyBatis映射文件的编写中,我们常会见到“${}”和“#{}”这两种定义变量的符号,其含义如下。

  • ${}:使用这种符号的变量将会以字符串的形式直接插到 SQL片段中。
  • #{}:使用这种符号的变量将会以预编译的形式赋值到 SQL片段中。

MyBatis中支持三种语句类型,不同语句类型支持的变量符号不同。MyBatis中的三种语句类型如下。

  • STATEMENT:这种语句类型中,只会对 SQL片段进行简单的字符串拼接。因此,只支持使用“${}”定义变量。
  • PREPARED:这种语句类型中,会先对 SQL片段进行字符串拼接,然后对 SQL片段进行赋值。因此,支持使用“${}”“#{}”这两种形式定义变量。
  • CALLABLE:这种语句类型用来实现执行过程的调用,会先对 SQL 片段进行字符串拼接,然后对SQL片段进行赋值。因此,支持使用“${}”“#{}”这两种形式定义变量。

在创建 SQL语句时,语句的类型由 statementType 属性进行指定。如果不指定则默认采用PREPARED。

因为 STATEMENT、PREPARED 形式的 SQL 语句比较常用,不再单独介绍。下面详细介绍CALLABLE语句的使用。在对CALLABLE语句进行调用时,可以直接使用Map来设置输入参数。

存储过程调用后,MyBatis会根据输出参数设置直接将输出结果写回给定的 Map参数中,键为变量名,值为存储过程结果。通过程序运行结果中的日志可以看出,MyBatis先进行字符串的拼接(${ageMinLimit}变量被拼接),然后进行变量的赋值(#{ageMaxLimit}变量被赋值)。在拼接和赋值都完成之后,MyBatis执行查询并对结果进行回写。

5.4.2 MyBatis的语句处理功能

statement 子包负责提供语句处理功能,其中 StatementHandler 是语句处理功能类的父接口。StatementHandler接口及其子类的类图如下图所示。

其中 RoutingStatementHandler 类是一个代理类,它能够根据传入的 MappedStatement对象的具体类型选中一个具体的被代理对象,然后将所有实际操作都委托给被代理对象。RoutingStatementHandler 类提供的是路由功能,而路由选择的依据就是语句类型。RoutingStatementHandler类中路由选择的实现逻辑源码。

  // 根据语句类型选取出的被代理类的对象private final StatementHandler delegate;public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 根据语句类型选择被代理对象switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}

BaseStatementHandler 作为三个实现类的父类,提供了实现类的公共方法。并且BaseStatementHandler类使用的模板模式在 prepare方法中定义了整个方法的框架,然后将一些与子类相关的操作交给其三个子类处理。SimpleStatementHandler类、PreparedStatementHandler类和 CallableStatementHandler类是三个真正的 Statement 处理器,分别处理 Statement 对象、PreparedStatement 对象和CallableStatement对象。通过其中的 parameterize方法可以看出三个 Statement处理器的不同。

SimpleStatementHandler中 parameterize方法的实现为空,因为它只需完成字符串替换即可,不需要进行参数处理。

PreparedStatementHandler中 parameterize方法最终通过 ParameterHandler接口经过多级中转后调用了 java.sql.PreparedStatement类中的参数赋值方法。该中转过程我们会在下节进行介绍。

CallableStatementHandler中 parameterize方法如下代码所示。它一共完成两步工作:

  • 一是通过 registerOutputParameters方法中转后调用 java.sql.CallableStatement中的输出参数注册方法完成输出参数的注册;
  • 二是通过 ParameterHandler接口经过多级中转后调用java.sql.PreparedStatement类中的参数赋值方法。
  /*** 对语句进行参数处理* @param statement SQL语句* @throws SQLException*/@Overridepublic void parameterize(Statement statement) throws SQLException {// 输出参数的注册registerOutputParameters((CallableStatement) statement);// 输入参数的处理parameterHandler.setParameters((CallableStatement) statement);}

可见 SimpleStatementHandler类、PreparedStatementHandler类和 CallableStatement-Handler类最终是依靠 java.sql包下的 Statement接口及其子接口提供的功能完成具体参数处理操作的。三个 Statement处理器中其他方法的处理逻辑基本一致而且也比较简单,不再展开介绍。

5.5 参数处理功能

为 SQL语句中的参数赋值是 MyBatis进行语句处理时非常重要的一步,而这一步就是由 parameter子包完成的。parameter子包中其实只有一个 ParameterHandler接口,它定义了两个方法:

  • getParameterObject方法用来获取 SQL语句对应的实参对象。
  • setParameters方法用来完成 SQL语句中的变量赋值。

ParameterHandler接口有一个默认的实现类DefaultParameterHandler,DefaultParameterHandler在 scripting包的 defaults子包中。DefaultParameterHandler的属性信息如下所示。

  // 类型处理器注册表private final TypeHandlerRegistry typeHandlerRegistry;// MappedStatement对象(包含完整的增删改查节点信息)private final MappedStatement mappedStatement;// 参数对象private final Object parameterObject;// BoundSql对象(包含SQL语句、参数、实参信息)private final BoundSql boundSql;// 配置信息private final Configuration configuration;

我们重点关注其 setParameters方法,MyBatis 中支持进行参数设置的语句类型是 PreparedStatement 接口及其子接口(CallableStatement 是PreparedStatement 的子接口),所以 setParameters 的输入参数是PreparedStatement类型。

  /*** 为语句设置参数* @param ps 语句*/@Overridepublic void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());// 取出参数列表List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);// ParameterMode.OUT是CallableStatement的输出参数,已经单独注册。故忽略if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;// 取出属性名称String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {// 从附加参数中读取属性值value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 参数对象是基本类型,则参数对象即为参数值value = parameterObject;} else {// 参数对象是复杂类型,取出参数对象的该属性值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 确定该参数的处理器TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 此方法最终根据参数类型,调用java.sql.PreparedStatement类中的参数赋值方法,对SQL语句中的参数赋值typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}

setParameters 方法的实现逻辑也很简单,就是依次取出每个参数的值,然后根据参数类型调用PreparedStatement中的赋值方法完成赋值。

5.6 结果处理功能

说起 MyBatis查询结果的处理,需要完成的功能有:

  • 处理结果映射中的嵌套映射等逻辑;
  • 根据映射关系,生成结果对象;
  • 根据数据库查询记录对结果对象的属性进行赋值;
  • 将结果对象汇总为 List、Map、Cursor等形式。

executor包的 result子包只负责完成“将结果对象汇总为 List、Map、Cursor等形式”这一简单功能中的一部分:将结果对象汇总为 List或 Map的形式。在介绍 result子包之前,我们先介绍位于 session包中的两个接口:ResultContext接口和ResultHandler接口。

  • ResultContext接口表示结果上下文,其中存放了数据库操作的一个结果(对应数据库中的一条记录)。
  • ResultHandler 接口表示结果处理器,数据库操作结果会由它处理。因此说,ResultHandler会负责处理 ResultContext。

result 子包中主要有三个类:DefaultResultContext 类、DefaultResultHandler 类和DefaultMapResultHandler类。这三个类中,DefaultResultContext类是 ResultContext接口唯一的实现类,DefaultResultHandler类和 DefaultMapResultHandler类是 ResultHandler接口的实现类。ResultHandler接口及其相关类的类图如下所示。

DefaultResultContext 用来存储一个结果对象,对应数据库中的一条记录。其各个属性的含义如代码所示。

  // 结果对象private T resultObject;// 结果计数(表明这是第几个结果对象)private int resultCount;// 使用完毕(结果已经被取走)private boolean stopped;

了解了DefaultResultContext类的各个属性后,对各个ResultHandler类的分析就非常简单了。DefaultResultHandler类负责将 DefaultResultContext类中的结果对象聚合成一个 List返回;而DefaultMapResultHandler类负责将 DefaultResultContext类中的结果对象聚合成一个 Map返回。其中 DefaultMapResultHandler类稍微复杂一些,我们以它为例进行介绍。该类的属性如下所示。

  // Map形式的映射结果private final Map<K, V> mappedResults;// Map的键。由用户指定,是结果对象中的某个属性名private final String mapKey;// 对象工厂private final ObjectFactory objectFactory;// 对象包装工厂private final ObjectWrapperFactory objectWrapperFactory;// 反射工厂private final ReflectorFactory reflectorFactory;

DefaultMapResultHandler类中的 handleResult方法用来完成 Map的组装,该方法的源码如下所示。

  /*** 处理一个结果* @param context 一个结果*/@Overridepublic void handleResult(ResultContext<? extends V> context) {// 从结果上下文中取出结果对象final V value = context.getResultObject();// 获得结果对象的元对象final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);// 基于元对象取出key对应的值final K key = (K) mo.getValue(mapKey);mappedResults.put(key, value);}

这样,我们对单个结果对象如何被聚合为 List、Map、Cursor 形式返回进行了了解。DefaultResultContext作为默认的 ResultContext实现类,存储了一个结果对象,对应着数据库中的一条记录。而 ResultHandler有三个实现类能够处理 DefaultResultContext对象,这三个实现类的功能如下。

  • DefaultResultHandler类负责将多个 ResultContext聚合为一个 List返回。
  • DefaultMapResultHandler类负责将多个 ResultContext聚合为一个 Map返回。
  • DefaultCursor 类中的 ObjectWrapperResultHandler 内部类负责将多个 ResultContext聚合为一个 Cursor返回。

5.7 结果集处理功能

在上节中我们对 MyBatis将单个结果对象聚合为 List、Map、Cursor的机制进行了介绍。但那只是结果处理流程中非常小的一个环节。在结果处理流程中,尚未完成的功能还有:

  • 处理结果映射中的嵌套映射等逻辑;
  • 根据映射关系,生成结果对象;
  • 根据数据库查询记录对结果对象的属性进行赋值。

以上这些功能均由 resultset子包提供,我们将在本节对这些功能展开介绍。resultset子包提供的功能虽多,但是只有三个类。resultset子包的类图如下图所示。

ResultSetWrapper是结果封装类,ResultSetHandler和 DefaultResultSetHandler分别是结果集处理器的接口和实现类。在介绍以上三个类之前,我们先了解 MyBatis的结果集。

5.7.1 MyBatis中多结果集的处理

为了介绍 MyBatis中的结果集处理功能,我们先明确以下概念。

结果:从数据库中查询出的一条记录就是一个结果,它可以映射为一个 Java对象。

结果集:指结果的集合,从数据库中查询出的多个记录就是一个结果集。结果集可以以 List、Map、Cursor的形式返回。

多结果集:即结果集的集合,其中包含了多个结果集。

说到多结果集,大家可能会产生一个疑问:平时每一次数据库查询操作都只会返回一个结果集,怎么会有多结果集的概念?例如,代码所示的 union操作,其实就是把两个结果集进行了合并,但最终两个结果集还是会合并为一个结果集。

虽然结果集中的结果可以明显地被分为两类,但是它们仍然属于一个结果集。在这个结果集中,每条结果都包含 id和 name这两个属性。

但其实,一次数据库查询确实可以返回一个多结果集。例如,存储过程会在调用时将两个 SELECT操作的结果一并返回。最终得到的多结果集,在多结果集中,各个结果集的属性各不相同。

MyBatis也支持处理多结果集,例如,有的语句会接受两个结果集,并将两个结果集分别命名为 userRecord和 taskRecord。然后使用 userMap来对结果集 userRecord进行映射,使用taskMap来对结果集 taskRecord进行映射。

最终可以得到 result变量中存储的多结果集,其中的两个结果集均使用 List存储。第一个结果集为使用 userMap映射出的 User对象列表,第二个结果集为使用taskMap映射出的Task对象列表。

MyBatis 甚至还支持多结果集的合并。我们指定了两个结果集userRecord 和taskRecord,但是只指定了一个结果集映射 userMap。这样,userRecord 和taskRecord 这两个结果集会被合并,最终整合成一个符合 userMap 映射的结果集。在这个结果集中,每一条 userRecord结果的 taskList属性都完整地包含了 taskRecord结果集中的全部结果。

多结果集合并后我们只得到了一个由 List存储的结果集,该结果集是使用userMap映射出的 User对象列表。每一个 User对象的 taskList属性都完整地包含了taskRecord结果集的全部结果。

至此,我们对 MyBatis中结果、结果集、多结果集的概念进行了区分,并对多结果集的返回、合并做了初步的了解,这些知识对于我们读懂 resultset子包的代码十分必要。

5.7.2 结果集封装类

java.sql.Statement进行完数据库操作之后,对应的操作结果是由 java.sql.ResultSet返回的。感兴趣的读者可以阅读 java.sql.ResultSet接口中定义的方法,它主要分为几大类:

  • 切换到下一结果,读取本结果是否为第一个结果、最后一个结果等结果间切换相关的方法;
  • 读取当前结果某列的值;
  • 修改当前结果某列的值(修改不会影响数据库中的真实值);
  • 一些其他的辅助功能,如读取所有列的类型信息等。

以上这几类方法已经能够满足数据库结果的查询操作。而 MyBatis中的 ResultSetWrapper类是对 java.sql.ResultSet的进一步封装,这里用到了装饰器模式。ResultSetWrapper类在 java.sql.ResultSet接口的基础上扩展出了更多的功能,这些功能包括获取所有列名的列表、获取所有列的类型的列表、获取某列的 JDBC类型、获取某列对应的类型处理器等。ResultSetWrapper类的属性如代码所示。

  // 被装饰的resultSet对象private final ResultSet resultSet;// 类型处理器注册表private final TypeHandlerRegistry typeHandlerRegistry;// resultSet中各个列对应的列名列表private final List<String> columnNames = new ArrayList<>();// resultSet中各个列对应的Java类型名列表private final List<String> classNames = new ArrayList<>();// resultSet中各个列对应的JDBC类型列表private final List<JdbcType> jdbcTypes = new ArrayList<>();// <列名,< java类型,TypeHandler>>// 这里的数据是不断组建起来的。java类型传入,然后去全局handlerMap索引java类型的handler放入map,然后在赋给列名。// 每个列后面的java类型不应该是唯一的么?不是的//    <resultMap id="userMapFull" type="com.example.demo.UserBean">//        <result property="id" column="id"/>//        <result property="schoolName" column="id"/>//    </resultMap>// 上面就可能不唯一,同一个列可以给不同的java属性// 类型与类型处理器的映射表。结构为:Map<列名,Map<Java类型,类型处理器>>private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>();// 记录了所有的有映射关系的列。// key为resultMap的id,后面的List为该resultMap中有映射的列的列表// <resultMap的id,List<对象映射的列名>>// 记录了所有的有映射关系的列。结构为:Map<resultMap的id,List<对象映射的列名>>private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();// 记录了所有的无映射关系的列。// key为resultMap的id,后面的List为该resultMap中无映射的列的列表//  // <resultMap的id : List<对象映射的列名>>// 记录了所有的无映射关系的列。结构为:Map<resultMap的id,List<对象映射的列名>>private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();

在知道了这些属性的含义之后,ResultSetWrapper 的各个方法的源码阅读就非常简单了,我们不再展开详述。

5.7.3 结果集处理器

ResultSetHandler是结果集处理器接口,它定义了结果集处理器的三个抽象方法。

  • <E> List<E> handleResultSets(Statement stmt):将 Statement的执行结果处理为 List。
  • <E> Cursor<E> handleCursorResultSets(Statement stmt):将 Statement的执行结果处理为 Map。
  • void handleOutputParameters(CallableStatement cs):处理存储过程的输出结果。

DefaultResultSetHandler类作为 ResultSetHandler接口的默认也是唯一的实现类,实现了上述的抽象方法。下面以 DefaultResultSetHandler类中的 handleResultSets方法为例,介绍 MyBatis如何完成结果集的处理。

通过 handleResultSets方法的名称(Sets为复数形式)也能看出,它能够处理多结果集。在处理多结果集时,我们得到的是两层列表,即结果集列表和嵌套在其中的结果列表。而在处理单结果集时,我们可以直接得到结果列表。handleResultSets方法调用了许多的子方法。为了使大家更清晰地了解 handleResultSets方法的执行过程,我们先给出整个方法的简化伪代码。

当然,在实际处理过程中,handleResultSets方法的流程要比简化的伪代码复杂许多。该方法带注释的完整源码如下所示。

  /*** 处理Statement得到的多结果集(也可能是单结果集,这是多结果集的一种简化形式),最终得到结果列表* @param stmt Statement语句* @return 结果列表* @throws SQLException*/@Overridepublic List<Object> handleResultSets(Statement stmt) throws SQLException {ErrorContext.instance().activity("handling results").object(mappedStatement.getId());// 用以存储处理结果的列表final List<Object> multipleResults = new ArrayList<>();// 可能会有多个结果集,该变量用来对结果集进行计数int resultSetCount = 0;// 可能会有多个结果集,先取出第一个结果集ResultSetWrapper rsw = getFirstResultSet(stmt);// 查询语句对应的resultMap节点,可能含有多个List<ResultMap> resultMaps = mappedStatement.getResultMaps();int resultMapCount = resultMaps.size();// 合法性校验(存在输出结果集的情况下,resultMapCount不能为0)validateResultMapsCount(rsw, resultMapCount);// 循环遍历每一个设置了resultMap的结果集while (rsw != null && resultMapCount > resultSetCount) {// 获得当前结果集对应的resultMapResultMap resultMap = resultMaps.get(resultSetCount);// 进行结果集的处理handleResultSet(rsw, resultMap, multipleResults, null);// 获取下一结果集rsw = getNextResultSet(stmt);// 清理上一条结果集的环境cleanUpAfterHandlingResultSet();resultSetCount++;}// 获取多结果集中所有结果集的名称String[] resultSets = mappedStatement.getResultSets();if (resultSets != null) {// 循环遍历每一个没有设置resultMap的结果集while (rsw != null && resultSetCount < resultSets.length) {// 获取该结果集对应的父级resultMap中的resultMapping(注:resultMapping用来描述对象属性的映射关系)ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);if (parentMapping != null) {// 获取被嵌套的resultMap的编号String nestedResultMapId = parentMapping.getNestedResultMapId();ResultMap resultMap = configuration.getResultMap(nestedResultMapId);// 处理嵌套映射handleResultSet(rsw, resultMap, null, parentMapping);}rsw = getNextResultSet(stmt);cleanUpAfterHandlingResultSet();resultSetCount++;}}// 判断是否是单结果集:如果是则返回结果列表;如果否则返回结果集列表return collapseSingleResultList(multipleResults);}

handleResultSets 方法完成了对多结果集的处理。但是对于每一个结果集的处理是由handleResultSet子方法实现的。代码给出了 handleResultSet子方法的源码。

  /*** 处理单一的结果集* @param rsw ResultSet的包装* @param resultMap resultMap节点的信息* @param multipleResults 用来存储处理结果的list* @param parentMapping* @throws SQLException*/private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {try {if (parentMapping != null) { // 嵌套的结果// 向子方法传入parentMapping。处理结果中的记录。handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);} else { // 非嵌套的结果if (resultHandler == null) {// defaultResultHandler能够将结果对象聚合成一个List返回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());}}

传入 handleResultSet 方法的已经是单结果集,handleResultSet 方法调用了handleRowValues方法进行进一步的处理。handleRowValues方法代码如下所示。在 handleRowValues方法中,会以当前映射中是否存在嵌套为依据再次进行分类,分别调用 handleRowValuesForNestedResultMap方法和handleRowValuesForSimpleResultMap方法。

  /*** 处理单结果集中的属性* @param rsw 单结果集的包装* @param resultMap 结果映射* @param resultHandler 结果处理器* @param rowBounds 翻页限制条件* @param parentMapping 父级结果映射* @throws SQLException*/public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {if (resultMap.hasNestedResultMaps()) {// 前置校验ensureNoRowBounds();checkResultHandler();// 处理嵌套映射handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);} else {// 处理单层映射handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);}}

我们以 handleRowValuesForSimpleResultMap 方法为例,继续查看整体的处理流程。handleRowValuesForSimpleResultMap方法如下所示。

  /*** 处理非嵌套映射的结果集* @param rsw 结果集包装* @param resultMap 结果映射* @param resultHandler 结果处理器* @param rowBounds 翻页限制条件* @param parentMapping 父级结果映射* @throws SQLException*/private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {DefaultResultContext<Object> resultContext = new DefaultResultContext<>();// 当前要处理的结果集ResultSet resultSet = rsw.getResultSet();// 根据翻页配置,跳过指定的行skipRows(resultSet, rowBounds);// 持续处理下一条结果,判断条件为:还有结果需要处理 && 结果集没有关闭 && 还有下一条结果while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 经过鉴别器鉴别,确定经过鉴别器分析的最终要使用的resultMapResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);// 拿到了一行记录,并且将其转化为一个对象Object rowValue = getRowValue(rsw, discriminatedResultMap, null);// 把这一行记录转化出的对象存起来storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);}}

在handleRowValuesForSimpleResultMap方法中,真正完成了对结果集中结果的处理。对每一条结果进行处理时,包括以下几个功能。

  • 基于鉴别器获取该条记录对应的 resultMap,该功能调用 resolveDiscriminatedResult-Map子方法实现。
  • 根据 resultMap,将这条记录转化为一个对象,该功能调用 getRowValue子方法实现。
  • 把由这一行记录转化得到的对象存起来,该功能调用 storeObject子方法实现。

我们重点关注 getRowValue方法和 storeObject方法。getRowValue方法的源码如下所示。该方法使用反射创建了记录对应的对象,并给对象的属性进行了赋值。创建对象的操作过程大家可以通过 createResultObject子方法继续追踪,为对象属性赋值的操作过程大家可以通过 applyAutomaticMappings 子方法和applyPropertyMappings子方法继续追踪。

  /*** 将一条记录转化为一个对象* @param rsw 结果集包装* @param resultMap 结果映射* @param columnPrefix 列前缀* @return 转化得到的对象* @throws SQLException*/private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final ResultLoaderMap lazyLoader = new ResultLoaderMap();// 创建这一行记录对应的对象Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {// 根据对象得到其MetaObjectfinal MetaObject metaObject = configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 是否允许自动映射未明示的字段if (shouldApplyAutomaticMappings(resultMap, false)) {// 自动映射未明示的字段foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}// 按照明示的字段进行重新映射foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;foundValues = lazyLoader.size() > 0 || foundValues;rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;}return rowValue;}

storeObject方法的源码如下所示。在 storeObject方法中,会根据当前对象的不同分别进行处理:

  • 如果当前对象属于父级映射,则将该对象绑定到父级对象上;
  • 如果当前对象属于独立映射,则使用 ResultHandler聚合该对象。
  /*** 存储当前结果对象* @param resultHandler 结果处理器* @param resultContext 结果上下文* @param rowValue 结果对象* @param parentMapping 父级结果映射* @param rs 结果集* @throws SQLException*/private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {if (parentMapping != null) {// 存在父级,则将这一行记录对应的结果对象绑定到父级结果上linkToParents(rs, parentMapping, rowValue);} else {// 使用resultHandler聚合该对象callResultHandler(resultHandler, resultContext, rowValue);}}

这样,经过层层子方法调用后便完成了 handleResultSets 方法的源码阅读。可见在handleResultSets方法中,完成了生成结果对象、为结果对象的属性赋值、将结果对象进行聚合或绑定等重要操作。handleCursorResultSets方法和 handleOutputParameters方法的源码则要简单许多,交由大家自行阅读。

5.8 执行器

我们已经介绍了 executor包中的各个子包,每个子包都为执行器提供了一些子功能。但是最终这些子功能均由 Executor接口及其实现类串接了起来,共同向外提供服务。Executor接口及其实现类的类图如图所示。

5.8.1 执行器接口

首先看一下 Executor接口中定义的方法。方法列表如下所示。

  // 数据更新操作,其中数据的增加、删除、更新均可由该方法实现int update(MappedStatement ms, Object parameter) throws SQLException;// 数据查询操作,返回结果为列表形式<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;// 数据查询操作,返回结果为列表形式/*** 执行查询操作* @param ms 映射语句对象* @param parameter 参数对象* @param rowBounds 翻页限制* @param resultHandler 结果处理器* @param <E> 输出结果类型* @return 查询结果* @throws SQLException*/<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;// 数据查询操作,返回结果为游标形式<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;// 清理缓存List<BatchResult> flushStatements() throws SQLException;// 提交事务void commit(boolean required) throws SQLException;// 回滚事务void rollback(boolean required) throws SQLException;// 创建当前查询的缓存键值CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);// 本地缓存是否有指定值boolean isCached(MappedStatement ms, CacheKey key);// 清理本地缓存void clearLocalCache();// 懒加载void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);// 获取事务Transaction getTransaction();// 关闭执行器void close(boolean forceRollback);// 判断执行器是否关闭boolean isClosed();// 设置执行器包装void setExecutorWrapper(Executor executor);

基于以上方法可以完成数据的增、删、改、查,以及事务处理等操作。而事实上,MyBatis的所有数据库操作也确实是通过调用这些方法实现的。

5.8.2 执行器基类与实现类

Executor接口的各个实现类中,CachingExecutor已经进行了详细介绍。它并没有包含具体的数据库操作,而是在其他数据库操作的基础上封装了一层缓存,因此它没有继承 BaseExecutor。而其他的各个实现类都继承了 BaseExecutor。BaseExecutor 是一个抽象类,并用到了模板模式。它实现了其子类的一些共有的基础功能,而将与子类直接相关的操作交给子类处理。以代码所示的 query核心方法为例,介绍其具体的实现。

  /*** 查询数据库中的数据* @param ms 映射语句* @param parameter 参数对象* @param rowBounds 翻页限制条件* @param resultHandler 结果处理器* @param key 缓存的键* @param boundSql 查询语句* @param <E> 结果类型* @return 结果列表* @throws SQLException*/@SuppressWarnings("unchecked")@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {// 执行器已经关闭throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) { // 新的查询栈且要求清除缓存// 清除一级缓存clearLocalCache();}List<E> list;try {queryStack++;// 尝试从本地缓存获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 本地缓存没有结果,故需要查询数据库list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {// 懒加载操作的处理for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}deferredLoads.clear();// 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();}}return list;}

在 query方法的核心方法中,会尝试读取一级缓存,而在缓存中无结果时,则会调用queryFromDatabase方法进行数据库中结果的查询。queryFromDatabase方法的源码如下所示。

  /*** 从数据库中查询结果* @param ms 映射语句* @param parameter 参数对象* @param rowBounds 翻页限制条件* @param resultHandler 结果处理器* @param key 缓存的键* @param boundSql 查询语句* @param <E> 结果类型* @return 结果列表* @throws SQLException*/private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 向缓存中增加占位符,表示正在查询localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 删除占位符localCache.removeObject(key);}// 将查询结果写入缓存localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}

上述代码中的 doQuery子方法的具体实现则交由 BaseExecutor的子类实现,因此这是典型的模板模式。这样我们已经对执行器基类中的 query 方法的源码进行了阅读分析。接下来不妨再分析一下BaseExecutor中 update方法的源码,如下所示。

  /*** 更新数据库数据,INSERT/UPDATE/DELETE三种操作都会调用该方法* @param ms 映射语句* @param parameter 参数对象* @return 数据库操作结果* @throws SQLException*/@Overridepublic int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {// 执行器已经关闭throw new ExecutorException("Executor was closed.");}// 清理本地缓存clearLocalCache();// 返回调用子类进行操作return doUpdate(ms, parameter);}

可以很明显地看出,update 方法的源码要比 query 方法的源码简单很多。查询操作的源码往往比增加、删除、修改操作的源码复杂的原因是:

  • 查询操作的输入参数相对比较复杂,而增加、删除、修改等操作的参数通常比较简单。
  • 查询操作的输出结果相对比较复杂,结果通常会被映射成对象,甚至还会包括嵌套、结果集操作、懒加载、对象类型鉴别等复杂操作;而增加、删除、修改等操作的输出结果通常比较简单,仅包含影响的条数。
  • 查询操作的输出结果形式比较复杂,如支持列表List、映射表Map、游标 Cursor等形式的输出;而增加、删除、修改等操作的结果形式通常比较简单,仅包含一个数字。
  • 查询操作的实现比较复杂,例如需要进行缓存处理、懒加载处理、嵌套映射处理等;而增加、删除、修改等操作则往往不需要这些复杂的处理。

正因为查询操作比其他操作更为复杂,所以在本书的源码阅读中经常以查询操作为例进行源码解析。在源码阅读时,往往会遇到很多分支。这些分支在实现思路上是相似的,我们没有必要将它们的源码全部进行阅读,只需选取其中一些有代表性的分支深入阅读即可。在选择分支的过程中,有以下两个思路。

  • 可以考虑选择最为复杂的分支,这样当这个分支的代码被我们分析清楚时,其他分支的代码自然也就清楚了。
  • 也可以考虑选择最为简单的分支,这样能让我们快速读懂代码的思路,再去分析其他复杂分支也会更加容易。

具体遵循哪个思路进行分支的选择要根据具体情况来分析。通常来说,对于重要的代码选择复杂的分支;对于次要的代码选择简单的分支;对于简单的代码选择复杂的分支;对于复杂的代码选择简单的分支。对于 MyBatis而言,输入/输出参数的处理、缓存的处理、懒加载的处理等都是一些非常重要的功能。因此,我们会选择包含这些功能的查询操作分支展开源码阅读。

下面继续回到 BaseExecutor的源码阅读中。BaseExecutor有四个实现类,其功能分别如下。

  • ClosedExecutor:一个仅能表征自身已经关闭的执行器,没有其他实际功能。
  • SimpleExecutor:一个最为简单的执行器。
  • BatchExecutor:支持批量执行功能的执行器。
  • ReuseExecutor:支持 Statement对象复用的执行器。

上述 SimpleExecutor、BatchExecutor、ReuseExecutor 这三个执行器的选择是在MyBatis的配置文件中进行的,可选的值由 session包中的枚举类 ExecutorType定义。这三个执行器主要基于StatementHandler完成创建 Statement对象、绑定参数等工作。其工作流程都比较简单,我们不再展开介绍。BatchResult也是 executor中的一个类,它可以保存批量操作的参数对象列表和影响条数列表。

5.9 错误上下文

可以看到,在很多方法的开始阶段都会调用 ErrorContext 类的相关方法。例如,在代码中我们就看到了如下所示的片段。

其中的 ErrorContext 类是一个错误上下文,它能够提前将一些背景信息保存下来。这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。ErrorContext类的属性如代码所示。

  // 获得当前操作系统的换行符private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");// 将自身存储进ThreadLocal,从而进行线程间的隔离private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();// 存储上一版本的自身,从而组成错误链private ErrorContext stored;// 下面几条为错误的详细信息,可以写入一项或者多项private String resource;private String activity;private String object;private String message;private String sql;private Throwable cause;

ErrorContext 类的属性设置非常简单,但是整个类却设计得非常巧妙。首先,如代码22-64所示,ErrorContext类实现了单例模式,而它的单例是绑定到 ThreadLocal上的。这保证了每个线程都有唯一的一个错误上下文 ErrorContext。

  /*** 从ThreadLocal取出已经实例化的ErrorContext,或者实例化一个ErrorContext放入ThreadLocal* @return ErrorContext实例*/public static ErrorContext instance() {ErrorContext context = LOCAL.get();if (context == null) {context = new ErrorContext();LOCAL.set(context);}return context;}

ErrorContext 类还有一种包装机制,即每个 ErrorContext 对象内可以包装一个ErrorContext对象。这样,错误上下文就可以组成一条错误链,这和异常链十分类似。该包装功能由store方法实现,如代码所示。

  /*** 创建一个包装了原有ErrorContext的新ErrorContext* @return 新的ErrorContext*/public ErrorContext store() {ErrorContext newContext = new ErrorContext();newContext.stored = this;LOCAL.set(newContext);return LOCAL.get();}

当然,除了能够创建一个包装了原有 ErrorContext 对象的新 ErrorContext 对象外,ErrorContext类还支持这种操作的逆操作——将某个 ErrorContext对象的内部 ErrorContext对象剥离出来。该剥离功能由 recall方法实现,如代码所示。

  /*** 剥离出当前ErrorContext的内部ErrorContext* @return 剥离出的ErrorContext对象*/public ErrorContext recall() {if (stored != null) {LOCAL.set(stored);stored = null;}return LOCAL.get();}

除此之外,ErrorContext类中还有用来清除所有信息的 reset方法、用来转化为字符串输出的toString方法,以及用来设置各个详细信息的 instance、resource、activity、store等方法。这些方法的使用场景如下。

  • 当需要获得当前线程的 ErrorContext对象时,调用 instance方法。
  • 当线程执行到某一阶段产生了新的上下文信息时,调用 resource、activity等方法向ErrorContext对象补充上下文信息。
  • 当线程进入下一级操作并处于一个全新的环
  • 当线程从下一级操作返回上一级时,调用 recall方法剥离上一级的 ErrorContext对象。
  • 当线程进入一个与之前操作无关的新环境时,调用 reset方法清除 ErrorContext对象的所有信息。
  • 当线程需要打印异常信息时,调用 toString方法输出错误发生时的环境信息。

通过这些操作,线程的 ErrorContext 类中时刻保存着当前时刻的上下文信息,一旦真正发生异常便可以把这些信息提供出来。例如代码所示的 wrapException方法(该方法包含在 exceptions包的 ExceptionFactory类中),只是显式地更新了 ErrorContext对象中的 message属性和 cause属性,但 toString方法输出的结果中可能包含更为丰富的属性信息。而那些属性信息是随着线程执行环境的变化而实时更新的。

  /*** 生成一个RuntimeException异常* @param message 异常信息* @param e 异常* @return 新的RuntimeException异常*/public static RuntimeException wrapException(String message, Exception e) {return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);}

6. session

session包是整个 MyBatis应用的对外接口包,是离用户最近的包。我们进行数据库操作的代码片段如下代码所示。

代码中涉及的 SqlSessionFactory类、SqlSession类都是 session包中的类,通过这些类就可以触发 MyBatis对数据库展开操作。这也验证了 session包是整个 MyBatis的对外接口包这一结论。

6.1 SqlSession及其相关类

通过代码可以看出,在进行查询操作时,只需要和 SqlSession对象打交道即可。而 SqlSession对象是由 SqlSessionFactory 生产出来的,SqlSessionFactory 又是由SqlSessionFactoryBuilder创建的。下图给出了 SqlSession及其相关类的类图。

6.1.1 SqlSession的生成链

上图所示的 SqlSession及其相关类组成了一个生成链。SqlSessionFactoryBuilder生成了SqlSessionFactory,SqlSessionFactory生成了 SqlSession。

SqlSessionFactoryBuilder 类是 SqlSessionFactory 的建造者类,它能够根据配置文件创建出SqlSessionFactory 对象。下述代码给出了 SqlSessionFactoryBuilder 类中一个核心的build方法。

  /*** 建造一个SqlSessionFactory对象* @param reader 读取字符流的抽象类* @param environment 环境信息* @param properties 配置信息* @return SqlSessionFactory对象*/public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {// 传入配置文件,创建一个XMLConfigBuilder类XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);// 分两步:// 1、解析配置文件,得到配置文件对应的Configuration对象// 2、根据Configuration对象,获得一个DefaultSqlSessionFactoryreturn build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {reader.close();} catch (IOException e) {}}}

通过代码可以看出,创建 SqlSessionFactory对象的过程主要分为三步:

  1. 传入配置文件,创建一个 XMLConfigBuilder类准备对配置文件展开解析。
  2. 解析配置文件,得到配置文件对应的 Configuration对象。
  3. 根据 Configuration对象,获得一个 DefaultSqlSessionFactory。

SqlSessionFactoryBuilder类给出的 SqlSessionFactory对象总是 DefaultSqlSessionFactory对象,build(Reader,String,Properties)方法调用的 build(Configuration)方法可以证实这一点。build(Configuration)方法的源码如下代码所示。

  /*** 根据配置信息建造一个SqlSessionFactory对象* @param config 配置信息* @return SqlSessionFactory对象*/public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}

DefaultSqlSessionFactory对象则可以创建出 SqlSession的子类 DefaultSqlSession类的对象,该过程由 openSessionFromDataSource方法完成,该方法的源码如下所示。

  /*** 从数据源中获取SqlSession对象* @param execType 执行器类型* @param level 事务隔离级别* @param autoCommit 是否自动提交事务* @return SqlSession对象*/private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {// 找出要使用的指定环境final Environment environment = configuration.getEnvironment();// 从环境中获取事务工厂final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 从事务工厂中生产事务tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 创建执行器final Executor executor = configuration.newExecutor(tx, execType);// 创建DefaultSqlSession对象return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}

至此,SqlSession 生成链的相关源码阅读完毕,经过逐级生成后,终于得到DefaultSqlSession类对象。

6.1.2 DefaultSqlSession类

session包是整个 MyBatis应用的对外接口包,而 executor包是最为核心的执行器包。DefaultSqlSession 类做的主要工作则非常简单——把接口包的工作交给执行器包处理。DefaultSqlSession类的属性如下所示。

  // 配置信息private final Configuration configuration;// 执行器private final Executor executor;// 是否自动提交private final boolean autoCommit;// 缓存是否已经被污染private boolean dirty;// 游标列表private List<Cursor<?>> cursorList;

DefaultSqlSession类的属性中包含一个 Executor对象,DefaultSqlSession类将主要的操作都交给属性中的 Executor对象处理。以下代码所示的 selectList方法为例,相关数据库查询操作都由Executor对象的 query方法来完成。

  /*** 查询结果列表* @param <E> 返回的列表元素的类型* @param statement SQL语句* @param parameter 参数对象* @param rowBounds  翻页限制条件* @return 结果对象列表*/@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {// 获取查询语句MappedStatement ms = configuration.getMappedStatement(statement);// 交由执行器进行查询return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}

6.1.3 SqlSessionManager类

在 SqlSession的相关类中,SqlSessionManager既实现了 SqlSessionFactory接口又实现了SqlSession接口,SqlSessionManager及其相关类的类图如下所示。

这种既实现工厂接口又实现工厂产品接口的类是很少见的。因此,我们单独研究一下SqlSessionManager类是如何实现的,以及其存在的意义。以下代码给出了 SqlSessionManager类的构造方法,该构造方法是私有的,外部需要通过newInstance方法间接调用它。

  /*** SqlSessionManager构造方法* @param sqlSessionFactory SqlSession工厂*/private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {this.sqlSessionFactory = sqlSessionFactory;this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[]{SqlSession.class},new SqlSessionInterceptor());}

通过代码可以看出,SqlSessionManager在构造方法中创建了一个 SqlSession的代理对象,该代理对象可以拦截被代理对象的方法。拦截到的方法会交给SqlSessionInterceptor内部类的invoke方法进行处理。以下代码给出了invoke方法的源码。

    @Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 尝试从当前线程中取出SqlSession对象final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();if (sqlSession != null) { // 当前线程中确实取出了SqlSession对象try {// 使用取出的SqlSession对象进行操作return method.invoke(sqlSession, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}} else { // 当前线程中还没有SqlSession对象// 使用属性中的SqlSessionFactory对象创建一个SqlSession对象try (SqlSession autoSqlSession = openSession()) {try {// 使用新创建的SqlSession对象进行操作final Object result = method.invoke(autoSqlSession, args);autoSqlSession.commit();return result;} catch (Throwable t) {autoSqlSession.rollback();throw ExceptionUtil.unwrapThrowable(t);}}}}

可以看出,当 SqlSession的代理对象拦截到方法时,会尝试从当前线程的 ThreadLocal中取出一个SqlSession对象。

  • 如果 ThreadLocal中存在 SqlSession对象,代理对象则将操作交给取出的 SqlSession对象进行处理。
  • 如果 ThreadLocal中不存在 SqlSession对象,则使用属性中的 SqlSessionFactory对象创建一个SqlSession对象,然后代理对象将操作交给新创建的 SqlSession对象进行处理。

这样一来,SqlSessionManager各个属性的含义也清晰起来,如下代码所示。

  // 构造方法中传入的SqlSessionFactory对象private final SqlSessionFactory sqlSessionFactory;// 在构造方法中创建的SqlSession代理对象private final SqlSession sqlSessionProxy;// 该变量用来存储被代理的SqlSession对象private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

了解了SqlSessionManager的主要方法和属性的含义之后,其结构已经十分清晰了,那它存在的意义又是什么呢?毕竟作为工厂的 DefaultSqlSessionFactory或作为产品的 DefaultSqlSession都能实现它的功能。其实 SqlSessionManager将工厂和产品整合到一起后,提供了下面两点功能。

  • SqlSessionManager总能给出一个产品(从线程 ThreadLocal取出或者新建)并使用该产品完成相关的操作,外部使用者不需要了解细节,因此省略了调用工厂生产产品的过程。
  • 提供了产品复用的功能。工厂生产出的产品可以放入线程 ThreadLocal 保存(需要显式调用startManagedSession 方法),从而实现产品的复用。这样既保证了线程安全又提升了效率。

很多场景下,用户使用的是工厂生产出来的产品,而不关心产品是即时生产的还是之前生产后缓存的。在这种情况下,可以参考 SqlSessionManager的设计,来提供一种更为高效的给出产品的方式。

在源码阅读的过程中,我们可能无法提前得知某些类的功能。这时候需要先阅读其源码,然后在源码的基础上猜测其功能。这种源码阅读的方式比较费时费力,但有时却难以避免。我们在阅读SqlSessionManager源码时就采用了这种方式。

6.2 Configuration类

我们知道配置文件 mybatis-config.xml是 MyBatis配置的主入口,包括映射文件的路径也是通过它指明的。而配置文件的根节点就是 configuration 节点,因此该节点内保存了所有的配置信息。

configuration节点的信息经过解析后都存入了 Configuration对象中,因此 Configuration对象中就包含了 MyBatis运行的所有配置信息。并且 Configuration类还对配置信息进行了进一步的加工,为许多配置项设置了默认值,为许多实体定义了别名等。因而Configuration类是MyBatis中极为重要的一个类。以下代码给出了Configuration类的属性,我们可以感受一下其中包含内容的丰富。

  // <environment>节点的信息protected Environment environment;// 以下为<settings>节点中的配置信息protected boolean safeRowBoundsEnabled;protected boolean safeResultHandlerEnabled = true;protected boolean mapUnderscoreToCamelCase;protected boolean aggressiveLazyLoading;protected boolean multipleResultSetsEnabled = true;protected boolean useGeneratedKeys;protected boolean useColumnLabel = true;protected boolean cacheEnabled = true;protected boolean callSettersOnNulls;protected boolean useActualParamName = true;protected boolean returnInstanceForEmptyRow;protected String logPrefix;protected Class<? extends Log> logImpl;protected Class<? extends VFS> vfsImpl;protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;protected JdbcType jdbcTypeForNull = JdbcType.OTHER;protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));protected Integer defaultStatementTimeout;protected Integer defaultFetchSize;protected ResultSetType defaultResultSetType;protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;// 以上为<settings>节点中的配置信息// <properties>节点信息protected Properties variables = new Properties();// 反射工厂protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();// 对象工厂protected ObjectFactory objectFactory = new DefaultObjectFactory();// 对象包装工厂protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();// 是否启用懒加载,该配置来自<settings>节点protected boolean lazyLoadingEnabled = false;// 代理工厂protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL// 数据库编号protected String databaseId;// 配置工厂,用来创建用于加载反序列化的未读属性的配置。protected Class<?> configurationFactory;// 映射注册表protected final MapperRegistry mapperRegistry = new MapperRegistry(this);// 拦截器链(用来支持插件的插入)protected final InterceptorChain interceptorChain = new InterceptorChain();// 类型处理器注册表,内置许多,可以通过<typeHandlers>节点补充protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();// 类型别名注册表,内置许多,可以通过<typeAliases>节点补充protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();// 语言驱动注册表protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();// 映射的数据库操作语句protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection").conflictMessageProducer((savedValue, targetValue) ->". please check " + savedValue.getResource() + " and " + targetValue.getResource());// 缓存protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");// 结果映射,即所有的<resultMap>节点protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");// 参数映射,即所有的<parameterMap>节点protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");// 主键生成器映射protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");// 载入的资源,例如映射文件资源protected final Set<String> loadedResources = new HashSet<>();// SQL语句片段,即所有的<sql>节点protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");// 暂存未处理完成的一些节点protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();// 用来存储跨namespace的缓存共享设置protected final Map<String, String> cacheRefMap = new HashMap<>();

MyBatis中的 BaseBuilder、BaseExecutor、Configuration、ResultMap等近 20个类都在属性中引用了 Configuration对象,这使得 Configuration对象成了 MyBatis全局共享的配置信息中心,能为其他对象提供配置信息的查询和更新服务。Configuration 类是为了保存配置信息而设置的解析实体类,虽然成员变量众多,但成员方法却都很简单,不再展开介绍。为了便于配置信息的快速查询,Configuration 类中还设置了一个内部类 StrictMap。StrictMap是HashMap的子类,它有以下特点。

  • 不允许覆盖其中的键值。即如果要存入的键已经在 StrictMap 中存在了,则会直接抛出异常。这一点杜绝了配置信息因为覆盖发生的混乱。
  • 自动尝试使用短名称再次存入给定数据。例如,向 StrictMap 中存入键为“com.github.yeecode.clazzName”的数据,则除了存入该数据外,StrictMap 还会以“clazzName”为键再存入一份(如果短名称“clazzName”不会引发歧义的话)。这使得配置信息支持以短名称进行查询(如果短名称不会引发歧义的话)。

代码给出了 StrictMap的 put方法,这可以帮助我们更好地理解 StrictMap的以上特点。

    /*** 向Map中写入键值对* @param key 键* @param value 值* @return 旧值,如果不存在旧值则为null。因为StrictMap不允许覆盖,则只能返回null*/@Override@SuppressWarnings("unchecked")public V put(String key, V value) {if (containsKey(key)) {//如果已经存在此key了,直接报错throw new IllegalArgumentException(name + " already contains value for " + key+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));}if (key.contains(".")) {// 例如key=“com.github.yeecode.clazzName”,则shortName = “clazzName”,即获取一个短名称final String shortKey = getShortName(key);if (super.get(shortKey) == null) {// 以短名称为键,放置一次super.put(shortKey, value);} else {// 放入该对象,表示短名称会引发歧义super.put(shortKey, (V) new Ambiguity(shortKey));}}// 以长名称为键,放置一次return super.put(key, value);}

6.3 其他类

session包中还包括其他的一些类。数目最多的是一些枚举类,这在前面的章节都已经涉及过,大家可以结合它们的使用来进行分析。

  • AutoMappingBehavior:表示当启动自动映射时要如何对属性进行映射。可选项有不映射、仅单层映射和全部映射。
  • AutoMappingUnknownColumnBehavior:表示自动映射中遇到一些未知的字段该如何处理。可选项有不处理、输出报警日志和抛出异常。
  • ExecutorType:表示执行器的类型。可选项有每次都新建的简单执行器、支持复用的执行器和支持批量操作的执行器。
  • LocalCacheScope:表示本地缓存的作用范围。可选项有会话和语句。
  • TransactionIsolationLevel:表示事务隔离级别。可选项有无隔离、读已提交、读未提交、可重复读和串行化。

此外,session包中还有一个 RowBounds类。RowBounds类用来表示查询结果分页设置,即表明查询结果的起始位置和条数限制。以下代码给出了它的两个主要的属性。

  // 起始位置private final int offset;// 总长度限制private final int limit;

以下代码展示了 RowBounds 类的 offset 属性如何在 DefaultResultSetHandler 类的skipRows方法中发挥作用。

  /*** 根据翻页限制条件跳过指定的行* @param rs 结果集* @param rowBounds 翻页限制条件* @throws SQLException*/private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {// 进入该分支表示:结果的游标不是只能单步前进if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {// 直接让游标移动到起始位置rs.absolute(rowBounds.getOffset());}} else {// 进入该分支表示:结果的游标只能单步前进for (int i = 0; i < rowBounds.getOffset(); i++) {if (!rs.next()) {break;}}}}

通过代码可以看出,这种分页是通过内存分页实现的,也就是说 MyBatis会向数据库查出所有的数据,然后在内存中略过一些数据后再开始读取。虽然最终返回的是部分数据,但是向数据库请求的却是全部数据,因此这并不是一种高效的分页方式。有一些针对 MyBatis的插件,如 PageHelper插件,就可以帮助 MyBatis实现真正的数据库分页。关于 MyBatis插件支持相关的源码我们会在下一章进行介绍。

7. plugin

MyBatis还提供插件功能,允许其他开发者为 MyBatis开发插件以扩展 MyBatis的功能。与插件相关的类在 MyBatis的 plugin包中。这一章我们讲解如何阅读 plugin包的源码,学习如何开发 MyBatis插件,并通过源码分析 MyBatis实现插件插入与管理的机制。

7.1 责任链模式

在有些场景下,一个目标对象可能需要经过多个对象的处理。例如,我们要筹办一场校园晚会,需要针对演员进行如下的准备工作。

  • 给演员发送邮件,告知晚会的时间、地点,该工作由邮件发送员负责。
  • 根据演员性别为其准备衣服,该工作由物资管理员负责。
  • 如果演员未成年,则为其安排校车接送,该工作由对外联络员负责。

如下代码展示了这一过程,每个演员都要和三个工作人员打交道。

而责任链模式将多个处理器组装成一个链条,被处理对象被放置到链条的起始端后,会自动在整个链条上传递和处理。这样被处理对象不需要和每个处理器打交道,也不需要了解整个链条的传递过程,于是便实现了被处理对象和单个处理器的解耦。

为实现责任链模式,首先创建一个处理器抽象类 Handler,如下代码所示。

public abstract class Handler {// 当前处理器的下一个处理器private Handler nextHandler;/*** 当前处理器的处理逻辑,交给子类实现* @param performer 被处理对象*/public abstract void handle(Performer performer);/*** 出发当前处理器,并在处理结束后将被处理对象传给后续处理器* @param performer 被处理对象*/public void triggerProcess(Performer performer) {handle(performer);if (nextHandler != null) {nextHandler.triggerProcess(performer);}}/*** 设置当前处理器的下一个处理器* @param nextHandler 下一个处理器* @return 下一个处理器*/public Handler setNextHandler(Handler nextHandler) {this.nextHandler = nextHandler;return nextHandler;}
}

然后每个处理器需要继承该抽象类 Handler,并实现自身的 handle方法。下图所示是责任链模式的类图。

在调用时,需要先组装好整个责任链,然后将被处理对象交给责任链处理,该过程如下所示。

这样,每个演员不需要和工作人员直接打交道,也不需要关心责任链上到底有多少个工作人员。责任链模式不仅降低了被处理对象和处理器之间的耦合度,还使得我们可以更为灵活地组建处理过程。例如,我们可以很方便地向责任链中增、删处理器或者调整处理器的顺序。

7.2 MyBatis插件开发

要想了解一个功能模块的源码,一种简单的办法是先学会使用这个模块。我们开发一个功能非常简单的 MyBatis插件,来了解 MyBatis插件的开发过程。我们要开发的插件的功能是:在 MyBatis查询列表形式的结果时,打印出结果的数目。整个插件的源码非常简单,如下代码所示。

@Intercepts({@Signature(type = ResultSetHandler.class, method="handleResultSets", args={Statement.class})
})
public class PluginInterceptor implements Interceptor {private String info;@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 执行原有的方法Object result = invocation.proceed();// 打印原方法输出的结果数目System.out.println(info + ":" + ((List) result).size());return result;}@Overridepublic void setProperties(Properties properties){// 为拦截器设置属性this.info = properties.get("info").toString();}
}

MyBatis插件是一个实现了 Interceptor接口的类。Interceptor的含义是拦截器,因此我们所说的MyBatis插件真正的叫法是 MyBatis拦截器。由于 plugin包中还有一个叫 Plugin的类,为了避免混淆,在接下来的叙述中,我们用拦截器来指代我们编写的插件类。拦截器类上有注解 Intercepts,Intercepts的参数是 Signature注解数组。每个 Signature注解都声明了当前拦截器类要拦截的方法。Signature注解中参数的含义如下。

  • type:拦截器要拦截的类型。PluginInterceptor 拦截器要拦截的类型是ResultSetHandler类型。
  • method:拦截器要拦截的 type类型中的方法。PluginInterceptor拦截器要拦截的是ResultSetHandler类型中的 handleResultSets方法。
  • args:拦截器要拦截的type类型中method方法的参数类型列表。在PluginInterceptor拦截器中,ResultSetHandler类型中的 handleResultSets方法只有一个 Statement类型的参数。

当要拦截多个方法时,只需在 Intercepts数组中放入多个 Signature注解即可。Interceptor接口中有三个方法供拦截器类实现,这三个方法的含义如下。

  • intercept:拦截器类必须实现该方法。拦截器拦截到目标方法时,会将操作转接到该 intercept 方法上,其中的参数 Invocation 为拦截到的目标方法。在PluginInterceptor拦截器的 intercept方法中,会先执行原有的方法并获得原方法的输出结果,然后打印出原方法输出结果的数目,最后返回原有结果。这样PluginInterceptor拦截器便实现了打印结果数目的功能。
  • plugin:拦截器类可以选择实现该方法。该方法中可以输出一个对象来替换输入参数传入的目标对象。在 PluginInterceptor拦截器中,我们没有实现该方法,而是直接使用了 Interceptor接口中的默认实现。在默认实现中,会调用 Plugin.wrap方法给出一个原有对象的包装对象,然后用该对象来替换原有对象。
  • setProperties:拦截器类可以选择实现该方法。该方法用来为拦截器设置属性。在PluginInterceptor拦截器中,我们使用该方法为拦截器设置 info属性的值。

拦截器配置结束后,还需要将拦截器设置到 MyBatis的配置中才能生效。下述代码给出了PluginInterceptor拦截器在配置文件中的配置片段。

<plugin interceptor="com.mybatis.plugin.PluginInterceptor"><property name="preInfo" value="本次查询记录数目" />
</plugin>

拦截器配置完成后,重新启动 MyBatis 就可以使拦截器生效。在配置了PluginInterceptor 拦截器后,当我们查询列表形式的结果时,控制台会打印出当前查询结果的数目。

这样,我们已经完成了一个简单的 MyBatis拦截器的开发、配置和使用工作。MyBatis 拦截器的开发还是很容易上手的,大家可以在日常使用中根据实际需要为其开发一些拦截器以扩展 MyBatis的功能。

7.3 MyBatis拦截器平台

为了便于开发者为 MyBatis开发拦截器,MyBatis在 plugin包中搭建了一个拦截器平台。下图给出了拦截器平台的类图。

整个类图中最为核心的类便是 Plugin 类,它继承了 java.lang.reflect.InvocationHandler接口,因此是一个基于反射实现的代理类。Plugin类的属性如代码所示。

  // 被代理对象private final Object target;// 拦截器private final Interceptor interceptor;// 拦截器要拦截的所有的类,以及类中的方法private final Map<Class<?>, Set<Method>> signatureMap;

Plugin类的 signatureMap属性存储的是当前拦截器要拦截的类和方法,该信息就是通过getSignatureMap方法从拦截器的 Intercepts注解和 Signature注解中获取的。如下代码给出了getSignatureMap方法的源码。

  /*** 获取拦截器要拦截的所有类和类中的方法* @param interceptor 拦截器* @return 入参拦截器要拦截的所有类和类中的方法*/private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 获取拦截器的Intercepts注解Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// issue #251if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 将Intercepts注解的value信息取出来,是一个Signature数组Signature[] sigs = interceptsAnnotation.value();// 将Signature数组数组放入一个Map中。键为Signature注解的type类型,值为该类型下的方法集合Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());try {Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}return signatureMap;}

有了拦截器要拦截的类型信息之后,Plugin 就可以判断出当前的类型是否需要被拦截器拦截。如果一个类需要被拦截,则 Plugin 会为这个类创建一个代理类。这部分操作在wrap方法中完成。

  /*** 根据拦截器的配置来生成一个对象用来替换被代理对象* @param target 被代理对象* @param interceptor 拦截器* @return 用来替换被代理对象的对象*/public static Object wrap(Object target, Interceptor interceptor) {// 得到拦截器interceptor要拦截的类型与方法Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 被代理对象的类型Class<?> type = target.getClass();// 逐级寻找被代理对象类型的父类,将父类中需要被拦截的全部找出Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// 只要父类中有一个需要拦截,说明被代理对象是需要拦截的if (interfaces.length > 0) {// 创建并返回一个代理对象,是Plugin类的实例return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}// 直接返回原有被代理对象,这意味着被代理对象的方法不需要被拦截return target;}

因此,如果一个目标类需要被某个拦截器拦截的话,那么这个类的对象已经在 warp方法中被替换成了代理对象,即 Plugin对象。当目标类的方法被触发时,会直接进入 Plugin对象的 invoke方法。在invoke方法中,会进行方法层面的进一步判断:如果拦截器声明了要拦截此方法,则将此方法交给拦截器执行;如果拦截器未声明拦截此方法,则将此方法交给被代理对象完成。

  /*** 代理对象的拦截方法,当被代理对象中方法被触发时会进入这里* @param proxy 代理类* @param method 被触发的方法* @param args 被触发的方法的参数* @return 被触发的方法的返回结果* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 获取该类所有需要拦截的方法Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 该方法确实需要被拦截器拦截,因此交给拦截器处理return interceptor.intercept(new Invocation(target, method, args));}// 这说明该方法不需要拦截,交给被代理对象处理return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}

所以 Plugin类完成了类层级和方法层级这两个层级的过滤工作。

  • 如果目标对象所属的类被拦截器声明拦截,则 Plugin用自身实例作为代理对象替换目标对象。
  • 如果目标对象被调用的方法被拦截器声明拦截,则Plugin将该方法交给拦截器处理。否则 Plugin将该方法交给目标对象处理。

正因为 Plugin类完成了大量的工作,拦截器自身所需要做的工作就非常简单了,主要分为两项:使用Intercepts 注解和 Signature 注解声明自身要拦截的类型与方法;通过intercept方法处理被拦截的方法。当然,拦截器也可以重写 Interceptor接口中的 plugin方法,来实现更为强大的功能。重写 plugin方法后,可以在 plugin方法中给出一个其他的类来替换目标对象(而不调用 Plugin类的warp方法)。这样可以完全脱离 Plugin类去完成一些更为自由的操作。这种情况下,如何替换目标对象以及替换之后的处理逻辑完全由插件开发者自己掌控。

7.4 MyBatis拦截器链与拦截点

我们了解到拦截器的生效原理,那么 MyBatis支持配置多个拦截器吗?答案是肯定的。我们可以在 MyBatis 的配置文件中配置多个插件,这些插件会在MyBatis 的初始化阶段被依次写到 InterceptorChain 类的 interceptors 列表中。这一过程在XMLConfigBuilder类的pluginElement方法中展开,如代码所示。

  /*** 解析<plugins>节点* @param parent <plugins>节点* @throws Exception*/private void pluginElement(XNode parent) throws Exception {if (parent != null) { // <plugins>节点存在for (XNode child : parent.getChildren()) { // 依次<plugins>节点下的取出每个<plugin>节点// 读取拦截器类名String interceptor = child.getStringAttribute("interceptor");// 读取拦截器属性Properties properties = child.getChildrenAsProperties();// 实例化拦截器类Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();// 设置拦截器的属性interceptorInstance.setProperties(properties);// 将当前拦截器加入到拦截器链中configuration.addInterceptor(interceptorInstance);}}}

这些拦截器在列表中组成了一个拦截器链。在 上节我们还了解到拦截器是通过替换目标对象实现的(通常基于 Plugin类,使用动态代理对象替换目标对象),那么 MyBatis中任何对象都可以被替换吗?答案是否定的。MyBatis 中一共只有四个类的对象可以被拦截器替换,它们分别是ParameterHandler、ResultSetHandler、StatementHandler 和 Executor。而且替换只能发生在固定的地方,我们称其为拦截点。以 ParameterHandler 对象为例,代码给出了ParameterHandler对象的拦截点。

  /*** 创建参数处理器* @param mappedStatement SQL操作的信息* @param parameterObject 参数对象* @param boundSql SQL语句信息* @return 参数处理器*/public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {// 创建参数处理器ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 将参数处理器交给拦截器链进行替换,以便拦截器链中的拦截器能注入行为parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);// 返回最终的参数处理器return parameterHandler;}

在 ParameterHandler对象的拦截点,ParameterHandler对象被作为参数传递给拦截器链的pluginAll 方法,以便拦截器链中的拦截器能够将行为注入 ParameterHandler 对象中。InterceptorChain类的 pluginAll方法如代码所示。

    /*** 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象* @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例* @return 用来替换目标对象的对象*/public Object pluginAll(Object target) {// 依次交给每个拦截器完成目标对象的替换工作for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}

在 InterceptorChain类的 pluginAll方法中,会将目标对象依次交给每个拦截器进行替换处理(通常是对目标对象进行进一步的包装以注入拦截器的功能),最后得到的目标对象target汇聚了拦截器链中的每一个拦截器的功能,这其实就是责任链模式。这样,在程序运行中,拦截器链中的各个拦截器会依次发挥自身的作用。

MyBatis 源码阅读 -- 核心操作篇相关推荐

  1. mybatis源码阅读(五) ---执行器Executor

    转载自  mybatis源码阅读(五) ---执行器Executor 1. Executor接口设计与类结构图 public interface Executor {ResultHandler NO_ ...

  2. Soul网关源码阅读番外篇(一) HTTP参数请求错误

    Soul网关源码阅读番外篇(一) HTTP参数请求错误 共同作者:石立 萧 * 简介     在Soul网关2.2.1版本源码阅读中,遇到了HTTP请求加上参数返回404的错误,此篇文章基于此进行探索 ...

  3. Mybatis源码阅读之二——模板方法模式与Executor

    [系列目录] Mybatis源码阅读之一--工厂模式与SqlSessionFactory 文章目录 一. 模板方法模式 二. 同步回调与匿名函数 三. Executor BaseExecutor与其子 ...

  4. Mybatis 源码阅读环境搭建

    Mybatis源码阅读环境搭建 前言 一.下载mybatis的源码 二.编译源码 三.创建测试项目 前言     mybatis源码阅读环境搭建还是比较简单的,接下来我们讲解一下如何搭建该源码阅读环境 ...

  5. mybatis源码阅读(八) ---Interceptor了解一下

    转载自  mybatis源码阅读(八) ---Interceptor了解一下 1 Intercetor MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis允许 ...

  6. mybatis源码阅读(七) ---ResultSetHandler了解一下

    转载自  mybatis源码阅读(七) ---ResultSetHandler了解一下 1.MetaObject MetaObject用于反射创建对象.反射从对象中获取属性值.反射给对象设置属性值,参 ...

  7. mybatis源码阅读(六) ---StatementHandler了解一下

    转载自  mybatis源码阅读(六) ---StatementHandler了解一下 StatementHandler类结构图与接口设计 BaseStatementHandler:一个抽象类,只是实 ...

  8. mybatis源码阅读(四):mapper(dao)实例化

    转载自   mybatis源码阅读(四):mapper(dao)实例化 在开始分析之前,先来了解一下这个模块中的核心组件之间的关系,如图: 1.MapperRegistry&MapperPro ...

  9. mybatis源码阅读(三):mybatis初始化(下)mapper解析

    转载自 mybatis源码阅读(三):mybatis初始化(下)mapper解析 MyBatis 的真正强大在于它的映射语句,也是它的魔力所在.由于它的异常强大,映射器的 XML 文件就显得相对简单. ...

最新文章

  1. magento 多语言 ,后台手工更改方法
  2. plantuml样式_PlantUML 简明教程
  3. 【玩转Golang】 通过组合嵌入实现代码复用
  4. 实现哈希表 java,如何实现Java的哈希表?
  5. python高级语法装饰器_Python高级编程——装饰器Decorator超详细讲解上
  6. elementui表单校验原始密码_javascript 中入门数据校验
  7. 一道清华期中考试题(逃)
  8. C语言socket发送json,C++实现Socket传输json封装的Mat
  9. linux版本和特点,Linux与其他系统对比,具有哪些特点?
  10. PostgreSQL删除数据
  11. UVa 1400 (线段树) Ray, Pass me the dishes!
  12. junit测试mysql_使用Junit单元测试及操作MySQL数据库时出现错误及解决方法
  13. 14_python_练习题——excel操作
  14. 2013八大免费杀毒软件排行榜
  15. 《物联网IoT解决方案》(Unity+SteamVR+云技术+5G+AI+物联网+IoT+人机交互+万物互联+物物互联+射频识别+全球定位系统+实时采集+智能化感知+识别+管理+立钻哥哥+==)
  16. 【笔记】Ring-DVFS:基于可靠性感知强化学习的DVFS,适用于实时嵌入式系统
  17. 从12306.cn谈大网站架构与性能优化
  18. web前端开发免费教程
  19. nodejs项目mysql使用sequelize支持存储emoji
  20. 雪晴网上海沙龙+在线直播:如何成为数据科学家?

热门文章

  1. Wampserver整个下载安装教程
  2. 为windows 驱动程序签名
  3. 下厨房的内容负责人潘小月演讲
  4. H3C链路二层聚合教程
  5. Vena Network CEO朱清:寒冬中的守夜人
  6. RedisTemplate与jedis
  7. 计算机在医学应用中的不足,计算机在医学中的应用
  8. Docker harbor私有仓库部署与管理
  9. MATLAB代码:电动汽车有序充电策略 基于峰谷分时电价引导下的电动汽车充电负荷优化
  10. 学习如何编码的最佳Android应用