背景本文基于调试模式下的单步运行记录，通过分析一个查询订单协议的操作，侧面剖析 iBATIS 框架的执行动作与原理。一、核心组件简介1. DAO 层与 SqlMapClientDaoSupportDAL 层的 DAO 接口实现类通常会继承 SqlMapClientDaoSupport。Spring 容器在初始化一个 DAO Bean 实例时，通常会注入两块信息：DataSource（数据源）和 SqlMapClient（主要是 SQL 语句）。这两块信息会被封装到 SqlMapClientTemplate 中。2. 数据源配置 (DataSource)其中数据源的实例通常采用 Apache 的开源项目 DBCP。代码配置如下：<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="xxxx"/> <property name="username"><value>xxxx</value></property> <property name="password"><value>xxxxx</value></property> <property name="maxActive"><value>20</value></property> <property name="initialSize"><value>1</value></property> <property name="maxWait"><value>60000</value></property> <property name="maxIdle"><value>20</value></property> <property name="minIdle"><value>3</value></property> <property name="removeAbandoned"><value>true</value></property> <property name="removeAbandonedTimeout"><value>180</value></property> <property name="connectionProperties"><value>clientEncoding=GBK</value></property> </bean>各配置参数的具体含义可参照：dbcp 基本配置和重连配置3. SqlMapClient 配置与初始化<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation"> <value>classpath:sqlmap.xml</value> </property> </bean> <bean id="sqlMapClientTddl" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="dataSource" ref="tGroupDataSource"/> <property name="configLocation" value="classpath:sqlmap.xml"/> </bean>SqlMapClient 主要是借助于实现 FactoryBean 和 InitializingBean 两个接口，加载 sql.xml 文件资源信息，得到 SqlMapClient 组件。注：上面的 sqlMapClient 默认不配置数据源，后面的 SqlMapClientTemplate 优先从全局变量中取，如果没有再从 sqlMapClient 中查找。public DataSource getDataSource() { DataSource ds = super.getDataSource(); return (ds != null ? ds : this.sqlMapClient.getDataSource()); }构造 SqlMapClient 组件的核心代码块：public void afterPropertiesSet() throws Exception { if (this.lobHandler != null) { // Make given LobHandler available for SqlMapClient configuration. // Do early because because mapping resource might refer to custom types. configTimeLobHandlerHolder.set(this.lobHandler); } try { this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties); // Tell the SqlMapClient to use the given DataSource, if any. if (this.dataSource != null) { TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass.newInstance(); DataSource dataSourceToUse = this.dataSource; if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) { dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource); } transactionConfig.setDataSource(dataSourceToUse); transactionConfig.initialize(this.transactionConfigProperties); applyTransactionConfig(this.sqlMapClient, transactionConfig); } } finally { if (this.lobHandler != null) { // Reset LobHandler holder. configTimeLobHandlerHolder.set(null); } } }4. 核心接口与类SqlMapExecutor该接口是对 SQL 操作行为的抽象，提供了 SQL 单条执行和批处理涉及的所有操作方法。SqlMapTransactionManager该接口是对事务行为的抽象，提供了事务执行过程中涉及的所有方法。SqlMapClient该接口定位是 SQL 执行客户端，是线程安全的，用于处理多个线程的 SQL 执行。它继承了上面两个接口，这意味着该接口具有 SQL 执行和事务处理的能力，该接口的核心实现类是 SqlMapClientImpl。SqlMapSession该接口在继承关系上和 SqlMapClient 一致，但它的定位是保存单线程 SQL 执行过程的 Session 信息。该接口的核心实现类是 SqlMapSessionImpl。MappedStatement该接口定位是单条 SQL 执行时的上下文环境信息，如 SQL 标识、SQL、参数信息、返回结果、操作行为等。ParameterMap / ResultMap该接口用于在 SQL 执行的前后提供参数准备和执行结果集的处理。整体类图：二、核心调用流程分析接下来到了数据持久层的代码调用，所有的数据库 DML 操作（增、删、改、查）都是借助于 SqlMapClientTemplate 来实现的。1. 单条查询示例public OrderEnsureProtocolDO getOrderEnsureProtocolByOrderId(Long orderId) { if (orderId == null) { return null; } return (OrderEnsureProtocolDO) this.getSqlMapClientTemplate().queryForObject( "MS-FIND-ORDERENSUREPROTOCOL-BY-ORDERID", orderId); }2. 批处理示例如果一次执行的 SQL 较多，我们会采用批处理的形式：public void batchDeleteOfferSaleRecord(final List<Long> orderEntryIds) throws Exception { if (orderEntryIds == null || orderEntryIds.size() < 1 || orderEntryIds.size() > 50) { return; } this.getSqlMapClientTemplate().execute(new SqlMapClientCallback() { @Override public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { executor.startBatch(); for (Long entryId : orderEntryIds) { executor.insert(MS_DELETE_SALE_RECORD, entryId); } return executor.executeBatch(); } }); }3. 内部执行流程 (execute)不管采用上面哪种方式，查看源代码会发现，最后都是在调用 execute(SqlMapClientCallback action) 方法。queryForObject 内部封装：public Object queryForObject(final String statementName, final Object parameterObject) throws DataAccessException { return execute(new SqlMapClientCallback() { public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { return executor.queryForObject(statementName, parameterObject); } }); }核心 execute 方法：public Object execute(SqlMapClientCallback action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Assert.notNull(this.sqlMapClient, "No SqlMapClient specified"); // 获取 session 信息 (SqlMapSessionImpl 实例) SqlMapSession session = this.sqlMapClient.openSession(); if (logger.isDebugEnabled()) { logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation"); } Connection ibatisCon = null; try { Connection springCon = null; // 数据库连接 DataSource dataSource = getDataSource(); boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy); try { // 获取数据连接 ibatisCon = session.getCurrentConnection(); if (ibatisCon == null) { springCon = (transactionAware ? dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource)); // 将数据源 set 到 session 会话中 session.setUserConnection(springCon); if (logger.isDebugEnabled()) { logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation"); } } else { if (logger.isDebugEnabled()) { logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation"); } } } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } try { // 执行 SQL return action.doInSqlMapClient(session); } catch (SQLException ex) { throw getExceptionTranslator().translate("SqlMapClient operation", null, ex); } finally { // 省略。。。一系列的关闭工作 } } }4. 深层调用链 (Delegate -> MappedStatement -> SqlExecutor)SqlMapSessionImpl().queryForObject() 的方法很简单，直接交给代理对象 SqlMapExecutorDelegate 处理（里面注入了很多功能对象，负责具体的 SQL 执行）：public Object queryForObject(String id, Object paramObject) throws SQLException { return delegate.queryForObject(session, id, paramObject); }经过 N 层重载，最后调用内部的通用方法：// 入参： // id = MS-FIND-ORDERENSUREPROTOCOL-BY-ORDERID // paramObject = 26749329 public Object queryForObject(SessionScope session, String id, Object paramObject, Object resultObject) throws SQLException { Object object = null; // MappedStatement 对象集是上文中提及的初始化方法 SqlMapClientFactoryBean.afterPropertiesSet() 中，由配置文件构建而成 // 调试中的 ms 为 SelectStatement，具体的执行器 MappedStatement ms = getMappedStatement(id); // 用于事务执行 Transaction trans = getTransaction(session); boolean autoStart = trans == null; try { trans = autoStartTransaction(session, autoStart, trans); // 从 RequestScope 池中获取该次 SQL 执行中的上下文环境 RequestScope RequestScope request = popRequest(session, ms); try { // 执行 SQL object = ms.executeQueryForObject(request, trans, paramObject, resultObject); } finally { pushRequest(request); // 归还 RequestScope } autoCommitTransaction(session, autoStart); } finally { autoEndTransaction(session, autoStart); } return object; }接下来由 MappedStatement.executeQueryForObject() 来执行：public Object executeQueryForObject(RequestScope request, Transaction trans, Object parameterObject, Object resultObject) throws SQLException { try { Object object = null; DefaultRowHandler rowHandler = new DefaultRowHandler(); // 执行 SQL 语句 executeQueryWithCallback(request, trans.getConnection(), parameterObject, resultObject, rowHandler, SqlExecutor.NO_SKIPPED_RESULTS, SqlExecutor.NO_MAXIMUM_RESULTS); // 结果处理，返回结果 List list = rowHandler.getList(); if (list.size() > 1) { throw new SQLException("Error: executeQueryForObject returned too many results."); } else if (list.size() > 0) { object = list.get(0); } // 。。。。。。。。。 } }MappedStatement.executeQueryWithCallback() 方法包含了参数值映射、SQL 准备和 SQL 执行等关键过程：protected void executeQueryWithCallback(RequestScope request, Connection conn, Object parameterObject, Object resultObject, RowHandler rowHandler, int skipResults, int maxResults) throws SQLException { // 预先封装错误信息，如果报错时便于排查问题 ErrorContext errorContext = request.getErrorContext(); errorContext.setActivity("preparing the mapped statement for execution"); errorContext.setObjectId(this.getId()); errorContext.setResource(this.getResource()); try { // 验证入参 parameterObject = validateParameter(parameterObject); // 获取 SQL 对象 Sql sql = getSql(); errorContext.setMoreInfo("Check the parameter map."); // 入参映射 ParameterMap parameterMap = sql.getParameterMap(request, parameterObject); errorContext.setMoreInfo("Check the result map."); // 获取结果 ResultMap resultMap = sql.getResultMap(request, parameterObject); request.setResultMap(resultMap); request.setParameterMap(parameterMap); errorContext.setMoreInfo("Check the parameter map."); // 获取参数值 Object[] parameters = parameterMap.getParameterObjectValues(request, parameterObject); errorContext.setMoreInfo("Check the SQL statement."); // 获取拼装后的 SQL 语句 String sqlString = sql.getSql(request, parameterObject); errorContext.setActivity("executing mapped statement"); errorContext.setMoreInfo("Check the SQL statement or the result map."); RowHandlerCallback callback = new RowHandlerCallback(resultMap, resultObject, rowHandler); // SQL 执行 sqlExecuteQuery(request, conn, sqlString, parameters, skipResults, maxResults, callback); // ....省略 } }最后调用 com.ibatis.sqlmap.engine.execution.SqlExecutor.executeQuery：public void executeQuery(RequestScope request, Connection conn, String sql, Object[] parameters, int skipResults, int maxResults, RowHandlerCallback callback) throws SQLException { // ...省略 PreparedStatement ps = null; ResultSet rs = null; setupResultObjectFactory(request); try { errorContext.setMoreInfo("Check the SQL Statement (preparation failed)."); Integer rsType = request.getStatement().getResultSetType(); // 初始化 PreparedStatement，设置 SQL、参数值等 if (rsType != null) { ps = prepareStatement(request.getSession(), conn, sql, rsType); } else { ps = prepareStatement(request.getSession(), conn, sql); } setStatementTimeout(request.getStatement(), ps); Integer fetchSize = request.getStatement().getFetchSize(); if (fetchSize != null) { ps.setFetchSize(fetchSize.intValue()); } errorContext.setMoreInfo("Check the parameters (set parameters failed)."); request.getParameterMap().setParameters(request, ps, parameters); errorContext.setMoreInfo("Check the statement (query failed)."); // 执行 ps.execute(); errorContext.setMoreInfo("Check the results (failed to retrieve results)."); // 结果集处理 rs = handleMultipleResults(ps, request, skipResults, maxResults, callback); // 。。。省略 } }说明版本时效性说明：本文基于 iBATIS 框架进行分析。iBATIS 项目已于 2010 年更名为 MyBatis，且 Spring 对 iBATIS 的支持类（如 SqlMapClientDaoSupport）在现代 Spring 版本中已废弃。文中涉及的配置与源码逻辑适用于旧版 iBATIS 2.x 环境，新开发项目建议直接使用 MyBatis 或 MyBatis-Plus。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。ORM 框架的核心功能在于建立面向对象模型与关系型数据库表之间的映射关系。在这一关联过程中，必然涉及对象中的数据类型与数据库表字段类型之间的转换。MyBatis 中的 org.apache.ibatis.type 包主要便是为了实现这一功能。1. org.apache.ibatis.type 基础类在 MyBatis 官网（http://mybatis.github.io/mybatis-3/configuration.html#typeHandlers）关于类型转换有如下的描述：Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type.当 MyBatis 为 PreparedStatement 设置参数时或者从 ResultSet 中获取数据时，会根据 Java 类型使用 TypeHandler 去获取相应的值。官网中也列出了每一个 TypeHandler 用来处理对应的 JDBC 类型和 Java 类型。1.1 TypeHandler 接口这个接口主要有三个方法：一个 set 方法：用来给 PreparedStatement 对象对应的列设置参数。两个 get 方法：从 ResultSet 和 CallableStatement 获取对应列的值。不同之处在于一个是取第几个位置的值，一个是取具体列名所对应的值。set 用来将 Java 对象中的数据类型转换为 JDBC 中对应的数据类型，get 用来将 JDBC 中对应的数据类型转换为 Java 对象中的数据类型。1.2 BaseTypeHandler 抽象类在进行软件设计时提倡面向接口的设计，但接口只是一个规范，并不做任何实质性的操作，还需有一系列的实现才可以真正的达到目标。BaseTypeHandler 类便是对 TypeHandler 接口的初步实现。在实现 TypeHandler 接口的三个函数外，又引入了 3 个抽象函数用于 null 值的处理。1.3 DateTypeHandler 示例承接上文，BaseTypeHandler 类也是一个抽象类，按照 Java 的规定抽象类并不能初始化，也不能直接使用，因而还需要有具体的实现类。在 type 包中有十多个具体的类来具体处理类型转换，每一个类处理一个数据类型，像 long、int、double 等等。我们以一个稍微复杂些的 DateTypeHandler 类为例，了解下对日期是如何进行处理的。1.3.1 setNonNullParameterpublic void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { ps.setTimestamp(i, new java.sql.Timestamp(((Date) parameter).getTime())); }首先将参数 parameter 这个 Object 转换为 Date 类型，而后通过 Date 对象的 getTime() 将日期转为毫秒数，而后再将毫秒数转换为 java.sql.Timestamp 对象。即将 java.util.Date 对象转换为 java.sql.Timestamp 对象。1.3.2 getNullableResultpublic Object getNullableResult(ResultSet rs, String columnName) throws SQLException { java.sql.Timestamp sqlTimestamp = rs.getTimestamp(columnName); if (sqlTimestamp != null) { return new java.util.Date(sqlTimestamp.getTime()); } return null; } public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { java.sql.Timestamp sqlTimestamp = cs.getTimestamp(columnIndex); if (sqlTimestamp != null) { return new java.util.Date(sqlTimestamp.getTime()); } return null; }从上面的代码可以看出，这两个函数的作用就是将 java.sql.Timestamp 对象转换为 java.util.Date 对象。1.4 类图综合而言，type 包基础类的类图示例如下：2. 自定义类型处理或覆盖默认处理在 MyBatis 官网中（http://mybatis.github.io/mybatis-3/configuration.html#typeHandlers）关于有如下的描述：You can override the type handlers or create your own to deal with unsupported or non-standard types. To do so, simply extend the org.apache.ibatis.type.BaseTypeHandler class and optionally map your new TypeHandler class to a JDBC type.可以覆盖 TypeHandler 或者创建自己的 TypeHandler 去处理 MyBatis 不支持的或者非标准的数据类型。实现这个功能，只需要继承 org.apache.ibatis.type.BaseTypeHandler 类然后选择新的 TypeHandler 类和 JDBC 类型的对应关系即可。在官网中也给出了详细了示例，这里不再进行重复。其中用到了两个注解：MappedJdbcTypes 和 MappedTypes，我们可以看下这两个注解的定义文件：@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MappedJdbcTypes { public JdbcType[] value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MappedTypes { public Class[] value(); }MappedJdbcTypes 传入的是 JdbcType 类型的（JdbcType 是 type 包中的一个枚举类型），MappedTypes 传入的是 Class 类型。在声明完自己的类型转换之后，还需要让 MyBatis 知道这些新的类型转换类，这可以通过在配置文件中添加 typeHandlers 节点来实现。可以添加一个类，也可以添加一个包中所有的类。<typeHandlers> <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/> <package name="org.mybatis.example"/> </typeHandlers>3. TypeHandler 的注册：TypeHandlerRegistry前面介绍了 MyBatis 中已定义好的 TypeHandler，也介绍了如何自定义 TypeHandler，以及如何让 MyBatis 知道这次自定义的 TypeHandler。现在剩下最关键的步骤：在 MyBatis 初始化后如何将这些 TypeHandler 注册到 MyBatis 中，以便在执行数据库操作去使用这些类。这些操作是由 TypeHandlerRegistry 类实施的。我们已经知道，MyBatis 在使用时需要一个配置文件来进行各种各样的设置，与这个配置文件相对应的是 org.apache.ibatis.session.Configuration 这个类，配置文件中每一项都对应 Configuration 类中的一个属性，typeHandler 就是 Configuration 类中的一个属性。protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();我们现在来看看 TypeHandlerRegistry 类中常用的方法及其作用。3.1 TypeHandlerRegistry 类中的属性和常用方法private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() { { put(Byte.class, byte.class); put(Short.class, short.class); put(Integer.class, int.class); put(Long.class, long.class); put(Float.class, float.class); put(Double.class, double.class); put(Boolean.class, boolean.class); } }; private final Map<JdbcType, TypeHandler> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler>(JdbcType.class); private final Map<Class<?>, Map<JdbcType, TypeHandler>> TYPE_HANDLER_MAP = new HashMap<Class<?>, Map<JdbcType, TypeHandler>>(); private final TypeHandler UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);3.1.1 reversePrimitiveMap可以看出，这个 Map 就是将 Java 中的基本数据类型和他们对应的包装类一一关联起来，像 Byte 和 byte。在进行注册时会用到这个属性，详见第三小节。3.1.2 JDBC_TYPE_HANDLER_MAPJdbcType 和 TypeHandler 的对应关系，通过如下的函数进行维护：public void register(JdbcType jdbcType, TypeHandler handler) { JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler); }3.1.3 TYPE_HANDLER_MAPTYPE_HANDLER_MAP 属性是一个关键的属性，Java 类型和 JdbcType 的对应关系及处理类都存在这个属性中。从这个 Map 的定义中可以看到多个 JDBC 类型能够对应到一个 Java 类型。通过如下的函数进行维护：public void register(Class<?> type, JdbcType jdbcType, TypeHandler handler) { // 先查看这个 Java 类型是否已经绑定过了，如果没有绑定过，创建了一个 Map，否则就直接添加新的 Map<JdbcType, TypeHandler> map = TYPE_HANDLER_MAP.get(type); if (map == null) { map = new HashMap<JdbcType, TypeHandler>(); TYPE_HANDLER_MAP.put(type, map); } map.put(jdbcType, handler); // 如果当前添加的是属于 Byte、Long 等类型，将其对应的基本类型也进行注册 if (reversePrimitiveMap.containsKey(type)) { register(reversePrimitiveMap.get(type), jdbcType, handler); } }3.1.4 注册自定义 TypeHandler在注册自定义的 TypeHandler 之前需要先定位到具体的类或者包。类的处理比较简单，直接利用 Java 的反射机制就可以知道这个类的 Class 属性了。对于包就稍微复杂些，在 MyBatis 中是利用 io 包 ResolverUtil 类中的 find 函数来实现的，这里不做详细介绍，等介绍到 MyBatis 的 io 包时再详细说明。我们来看注册包的函数：public void register(String packageName) { // 先声明一个 ResolverUtil 对象 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); // 利用 find 函数找到这个包里所有的类，并且放到 resolverUtil 的 matches 属性中 resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName); Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses(); // 通过 for 循环依次将加载 for (Class<?> type : handlerSet) { // Ignore inner classes and interfaces (including package-info.java) and abstract classes // 不处理内部类、接口、抽象类以及 package-info 类 if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) { try { // 利用反射机制创建一个 TypeHandler TypeHandler handler = (TypeHandler) type.getConstructor().newInstance(); // 注册 register(handler); } catch (Exception e) { throw new RuntimeException("Unable to find a usable constructor for " + type, e); } } } }从上面的代码中可以看到，真正进行注册用到的是如下的函数，先处理 MappedTypes 这个注解：public void register(TypeHandler handler) { boolean mappedTypeFound = false; // 判断这个类是否有 MappedTypes 这个注解 MappedTypes mappedTypes = (MappedTypes) handler.getClass().getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> handledType : mappedTypes.value()) { // 进行注册并设置 mappedTypeFound 变量为 true register(handledType, handler); mappedTypeFound = true; } } // 如果 mappedTypeFound 为 false，则抛出一个异常 // 注意，这里和官网上的示例不同，官网上的示例并没有这个注解 // 但是在源代码中如果没有这个注解则不会继续下去 // 因而如果是要自定义类型转换，还是需要添加这个注解 if (!mappedTypeFound) { throw new RuntimeException("Unable to get mapped types, check @MappedTypes annotation for type handler " + handler); } }上面的函数又调用了如下的函数，去处理 MappedJdbcTypes 注解：public void register(Class<?> type, TypeHandler handler) { MappedJdbcTypes mappedJdbcTypes = (MappedJdbcTypes) handler.getClass().getAnnotation(MappedJdbcTypes.class); // 对注解进行判断，如果有 MappedJdbcTypes 这个注解，则对其对应的 JdbcType 依次进行注册，否则注册为 null if (mappedJdbcTypes != null) { for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(type, handledJdbcType, handler); } } else { register(type, null, handler); } }最后调用第三小节中提到的函数进行注册到 Map 中，供程序执行时调用。4. TypeHandlerRegistry 的初始化前面介绍了如何自定义类型转换处理类并注册到 MyBatis 中，那 type 包中有不少 MyBatis 已实现的类型转换处理类，这些类是如何及在何时注册到 MyBatis 中的呢？MyBatis 中的实现很简单，它把这些都放在了 TypeHandlerRegistry 的构造函数中了。由于构造函数比较大，也没有用到什么新方法，在此就不贴代码了，有兴趣的读者可以自己去观看。5. 小结本文对 MyBatis 中的 Type 转换做了介绍，介绍了其设计结构、常用的方法、如何自定义类型处理类、如何进行注册等内容，希望对大家理解 MyBatis 如何实现 Java 类型到 JDBC 类型转换有所帮助。当然，这里只是介绍了如何转换，但是这些转换是怎么使用的并没有涉及，这些内容将放到 MyBatis 的 Executor 中进行介绍。说明：本文基于 MyBatis 早期版本源代码分析（参考图片链接显示时间为 2013 年），部分实现细节（如注解强制要求）可能随版本迭代有所变化，请以当前官方文档及最新源码为准。
Spring Boot + MyBatis 一级缓存和二级缓存详解本文主要介绍在 Spring Boot 项目中如何使用 MyBatis 的一级缓存和二级缓存。为了演示方便，本文的数据库采用 H2 内存数据库，数据库连接池默认使用 Spring Boot 2.x 自带的 HikariCP。正确使用 MyBatis 缓存可以有效减少多余的数据库查询操作，节约 I/O 资源。接下来我们从实践出发，看一看 MyBatis 的一级缓存、二级缓存如何使用。相关代码请查阅：https://github.com/zhengxl5566/springboot-demo.git1. 概念介绍什么是一级缓存在日常开发过程中，经常会有相同的 SQL 执行多次查询的情况。MyBatis 提供了一级缓存来优化这些查询，避免多次请求数据库。一级缓存在 MyBatis 中默认是开启的，并且是 Session 级别，它的作用域为一次 SqlSession 会话。什么是二级缓存相对于一级缓存，二级缓存的作用域更广泛。它不止局限于一个 SqlSession，可以在多个 SqlSession 之间共享。事实上，它的作用域是 Namespace。MyBatis 的二级缓存全局开关默认是开启的，但由于它的作用域是 Namespace，所以还需要在 mapper.xml 中配置 <cache> 标签才能生效。缓存的优先级通过 MyBatis 发起的查询，查找顺序为：二级缓存 -> 一级缓存 -> 数据库。其中任何一个环节查到不为空的数据，都将直接返回结果。缓存失效当在一个缓存作用域中发生了 update、insert、delete 动作后，将会触发缓存失效。下一次查询将命中数据库，从而保证不会查到脏数据。2. 代码演示2.1 一级缓存默认情况下，MyBatis 开启并使用了一级缓存。单元测试用例： /** * 开启事务，测试一级缓存效果 * 缓存命中顺序：二级缓存 ---> 一级缓存 ---> 数据库 **/ @Test @Transactional(rollbackFor = Throwable.class) public void testFistCache(){ // 第一次查询，缓存到一级缓存 userMapper.selectById(1); // 第二次查询，直接读取一级缓存 userMapper.selectById(1); }执行结果：可以看到，虽然进行了两次查询，但最终只请求了一次数据库。第二次查询命中了一级缓存，直接返回了数据。这里有两点需要说明：为什么开启事务？由于使用了数据库连接池（HikariCP），默认每次查询完之后自动 commit，这就导致两次查询使用的不是同一个 SqlSession。根据一级缓存的原理，它将永远不会生效。当我们开启了事务，两次查询都在同一个 SqlSession 中，从而让第二次查询命中了一级缓存。读者可以自行关闭事务验证此结论。两种一级缓存模式一级缓存的作用域有两种：SESSION（默认）和 STATEMENT。可通过设置 local-cache-scope 的值来切换，默认为 SESSION。二者的区别在于 SESSION 会将缓存作用于同一个 SqlSession，而 STATEMENT 仅针对一次查询。所以，local-cache-scope: STATEMENT 可以理解为关闭一级缓存。2.2 二级缓存默认情况下，MyBatis 打开了二级缓存的全局开关，但它并未立即生效。因为二级缓存的作用域是 Namespace，所以还需要在 Mapper.xml 文件中配置一下才能使二级缓存生效。单表二级缓存下面对 userMapper.xml 配置一下，让其二级缓存生效，只需加入 <cache> 标签即可。<cache></cache>单元测试用例： /** * 测试二级缓存效果 * 需要 *Mapper.xml 开启二级缓存 **/ @Test public void testSecondCache(){ userMapper.selectById(1); userMapper.selectById(1); }执行结果：这里可以看到，第二次查询直接命中了缓存，日志还打印了该缓存的命中率。读者可以自行关闭二级缓存查看效果，通过注掉对应 mapper.xml 的 <cache> 标签，或者设置 cache-enabled: false 均可。多表联查二级缓存接下来演示多表联查的二级缓存，user 表 left join user_order 表 on user.id = user_order.user_id。我们考虑这样一种情况：该联查执行两次，第二次联查前更新 user_order 表。如果只使用 <cache> 配置，将会查不到更新的 user_order 数据，因为两个 mapper.xml 的作用域不同。要想合到一个作用域，就需要用到 <cache-ref>。userOrderMapper.xml：<cache></cache>`userMapper.xml`： <cache-ref namespace="com.zhengxl.mybatiscache.mapper.UserOrderMapper"/>单元测试用例： /** * 测试多表联查的二级缓存效果 * 需要 *Mapper.xml 设定引用空间 **/ @Test public void testJoin(){ System.out.println(userMapper.selectJoin()); System.out.println(userMapper.selectJoin()); UserOrder userOrder = new UserOrder(); userOrder.setGoodName("myGoods"); userOrder.setUserId(1); userOrderMapper.saveOrder(userOrder); System.out.println(userMapper.selectJoin()); }执行结果：首先查询了两次 user 表，第二次命中二级缓存；然后更新 user_order 表，使缓存失效；第三次查询时命中数据库。总结与建议综上，MyBatis 的单机缓存机制介绍完毕，读者可以自行下载样例工程验证。一级缓存：MyBatis 默认的 Session 级别一级缓存，由于 Spring Boot 中默认使用了 HikariCP，所以基本没用，需要开启事务才有用。但一级缓存作用域仅限同一 SqlSession 内，无法感知到其他 SqlSession 的增删改，所以极易产生脏数据。二级缓存：可通过 <cache-ref> 让多个 mapper.xml 共享同一 Namespace，从而实现缓存共享，但多表联查时配置略微繁琐。生产环境建议： 将一级缓存设置为 STATEMENT 级别（即关闭一级缓存），如果有必要，可以开启二级缓存。注意事项分布式部署警告：如果应用是分布式部署，由于二级缓存存储在本地内存，必然导致不同节点间查询出脏数据。所以，分布式部署的应用不建议开启 MyBatis 二级缓存，建议使用 Redis 等集中式缓存方案。参考资料聊聊 MyBatis 缓存机制说明：本文基于 Spring Boot 2.x 及 MyBatis 默认配置编写，适用于单机或非分布式场景。在微服务或分布式架构中，请谨慎评估二级缓存的使用。
为什么你用的 MyBatis 慢？一行配置让它性能翻倍！
在 Java 后端开发领域，MyBatis 凭借其灵活的 SQL 编写能力与便捷的数据库对接特性，成为了众多开发者的首选 ORM 框架。然而，在实际生产环境中，不少团队面临着 MyBatis 响应缓慢、拖慢业务整体性能的困境。面对高并发场景下的页面加载延迟或接口超时告警，往往并非代码逻辑复杂所致，而是默认配置未能适配实际业务需求。本文将深入剖析 MyBatis 性能瓶颈，并提供关键的配置优化方案。一、MyBatis 性能瓶颈现状剖析在日常开发中，基于 MyBatis 编写的业务代码在本地测试环境下往往表现正常，但一旦上线面对真实的大数据量与高并发场景，性能问题便接踵而至。究其根源，主要是 MyBatis 的默认配置在特定场景下存在“水土不服”的现象。例如，MyBatis 的一级缓存（Local Cache）机制初衷是为了减少数据库查询次数，但在某些会话管理不当的场景下，缓存可能频繁失效或重建，带来额外的开销。此外，SQL 执行过程中 Statement 对象的创建与资源管理若未优化，反复的资源申请与释放也会消耗系统资源，影响执行效率。二、核心性能瓶颈分析（一）参数设置不合理：fetchSize 默认值过小MyBatis 的 fetchSize 参数决定了每次从数据库驱动层拉取数据的行数。该参数的默认值通常较小（依赖 JDBC 驱动默认值，往往为 10 或更小）。这意味着在查询大量数据时，数据库需要多次往返传输数据，导致网络开销剧增。类比：如同去仓库搬货，若每次只拿一件，大部分时间都浪费在往返路上；若每次搬运一箱，效率将显著提升。（二）缓存策略不当一级缓存：默认开启且作用域为 SqlSession。若会话管理粒度不合理，容易导致脏读或数据不一致，且在高并发下若会话复用策略不当，可能引发性能波动。二级缓存：虽能跨会话共享数据，但配置相对繁琐。许多开发者因难以把握缓存过期与清理策略，要么直接弃用，要么开启后因配置失误导致系统响应变慢。（三）SQL 执行细节缺失MyBatis 生成的 SQL 语句在执行计划上可能并非最优。例如，关联查询时未充分利用数据库索引，导致数据库引擎进行全表扫描。在海量数据面前，这种低效查询的时间复杂度会显著增加，成为系统瓶颈。三、关键配置优化：调整 defaultFetchSize针对上述瓶颈，尤其是大数据量查询场景，可以通过调整 MyBatis 的全局配置来显著改善性能。在 MyBatis 配置文件（mybatis-config.xml）中加入以下配置：<settings> <setting name="defaultFetchSize" value="1000"/> </settings>优化原理：将 defaultFetchSize 设置为 1000，意味着 MyBatis 每次从数据库抓取数据时，会一次性拉取 1000 行记录。这大幅减少了数据库连接交互次数与网络传输频次，从而提升整体查询效率。实战效果：在某电商系统商品列表页查询场景中，优化前加载 5000 件商品信息耗时近 10 秒；添加该配置后，同等数据量下耗时锐减至 3 秒以内，性能提升显著。四、配套优化方案仅靠单一配置无法解决所有性能问题，建议结合以下手段全方位提升 MyBatis 性能：（一）缓存精细化管理一级缓存：根据业务场景合理控制 SqlSession 生命周期。二级缓存：针对只读或低频变更的业务（如商品分类信息）开启二级缓存，并设置合理的过期时间（如 30 分钟），定期清理无效缓存，平衡数据准确性与读取效率。（二）SQL 优化“组合拳”索引优化：结合数据库特性，为频繁查询字段及关联查询字段添加索引。执行计划分析：利用 EXPLAIN 语句分析 SQL 执行计划，及时调整查询语句，摒弃低效写法。连接池配置：采用高效的数据库连接池（如 HikariCP），复用连接资源，降低连接创建成本。（三）监控与调优接入性能监控工具（如 Arthas、Pinpoint 等），实时监测 MyBatis 执行 SQL 的耗时与资源占用情况。依据监控数据动态调整配置参数，实现持续迭代优化。五、实战验证与常见问题答疑实战案例在某社交平台用户动态查询模块中，初期因 MyBatis 查询效率低，用户刷新动态延迟明显。通过调整 fetchSize 配置、优化关联查询 SQL 并完善缓存策略后，页面实现秒开，用户活跃度随之提升。常见问题解答Q1：改大 defaultFetchSize 会不会导致内存溢出（OOM）？A： 合理取值是关键。需依据服务器内存大小与业务单次查询数据量权衡，一般设置在 1000 - 5000 之间较安全。同时务必做好分页处理，防止单次加载过量数据。Q2：二级缓存配置复杂，有没有简化思路？A： 优先选用整合框架自带的缓存方案。例如在 Spring Boot 搭配 MyBatis 的场景中，可利用其默认缓存配置模板，仅需微调参数即可生效，降低配置复杂度。说明：本文优化建议基于 MyBatis 通用机制。若使用 Spring Boot 集成 MyBatis，部分配置可能需在 application.yml 或 application.properties 中通过 mybatis.configuration.default-fetch-size 进行设置。具体效果取决于数据库驱动支持情况及实际业务数据量，建议在测试环境验证后上线。
深入分析 iBATIS 框架之系统架构与映射原理
iBATIS 框架主要的类层次结构总体来说，iBATIS 的系统结构相对简洁，它主要完成两件事情：根据 JDBC 规范建立与数据库的连接；通过反射打通 Java 对象与数据库参数交互之间的相互转化关系。iBATIS 的框架结构正是按照这种思想来组织类层次结构的，其实它是一种典型的交互式框架。先期准备好交互的必要条件，然后构建一个交互的环境，交互环境中还划分成会话，每次的会话也有一个环境。当这些环境都准备好以后，剩下的就是交换数据了。其实涉及到网络通信，一般都会是类似的处理方式。图 1 是 iBATIS 框架的主要类层次结构图：图 1. iBATIS 框架的主要类层次结构图上面的类图中，左边 SqlMapClient 接口主要定义了客户端的操作行为，包括 select、insert、update、delete。而右边主要是定义了当前客户端在当前线程的执行环境。SqlMapSession 可以共享使用，也可以自己创建；如果是自己创建，在结束时必须要调用关闭接口关闭。当使用者持有了 SqlMapClientImpl 对象就可以使用 iBATIS 来工作了。这里还要提到另外一个类 SqlMapExecutorDelegate，这个类从名字就可以看出它是执行代理类。这个类非常重要，因为它耦合了用户端的执行操作行为和执行的环境，它持有执行操作所需要的数据，同时提供管理着执行操作依赖的环境。所以它是一个强耦合的类，也可以看做是个工具类。iBATIS 框架的设计策略iBATIS 主要的设计目的还是为了让我们执行 SQL 时对输入输出的数据管理更加方便，所以如何方便地让我们写出 SQL 和方便地获取 SQL 的执行结果才是 iBATIS 的核心竞争力。那么 iBATIS 是怎么实现它的核心竞争力的呢？iBATIS 框架的一个重要组成部分就是其 SqlMap 配置文件，SqlMap 配置文件的核心是 Statement 语句，包括 CRUD（增删改查）。iBATIS 通过解析 SqlMap 配置文件得到所有的 Statement 执行语句，同时会形成 ParameterMap、ResultMap 两个对象用于处理参数和经过解析后交给数据库处理的 Sql 对象。这样除去数据库的连接，一条 SQL 的执行条件已经具备了。图 2 描述了 Statement 有关的类结构图：图 2. Statement 有关的类结构图图 2 给出了围绕 SQL 执行的基本结构关系，但是还有一个关键的部分就是，如何定义 SQL 语句中的参数与 Java 对象之间的关系，这其中还涉及到 Java 类型到数据库类型的转换等一系列问题。数据的映射大体的过程是这样的：根据 Statement 中定义的 SQL 语句，解析出其中的参数，按照其出现的顺序保存在 Map 集合中，并按照 Statement 中定义的 ParameterMap 对象类型解析出参数的 Java 数据类型。并根据其数据类型构建 TypeHandler 对象，参数值的复制是通过 DataExchange 对象完成的。图 3 是参数映射相关的类结构图：图 3. 参数映射相关的类结构图图 3 是输入参数的映射结构情况，返回结果 ResultMap 的映射情况也是类似的。主要就是要解决 SQL 语句中的参数与返回结果的列名与 Statement 中定义的 parameterClass 和 resultClass 中属性的对应关系。iBATIS 框架的运行原理前面大体分析了 iBATIS 框架的主要类的结构，这里主要看一下这些类是如何串联起来、如何工作的。图 4 描述了整个过程的主要执行步骤。图 4. iBATIS 运行的主要执行步骤上图中描述的 SqlMapSession 对象的创建和释放根据不同情况会有不同，因为 SqlMapSession 负责创建数据库的连接，包括对事务的管理。iBATIS 对管理事务既可以自己管理也可以由外部管理：iBATIS 自己管理是通过共享 SqlMapSession 对象实现的，多个 Statement 的执行时共享一个 SqlMapSession 实例，而且都是线程安全的；如果是外部程序管理就要自己控制 SqlMapSession 对象的生命周期。图 5 是通过 Spring 调用 iBATIS 执行一个 Statement 的一个详细的时序图：图 5. Spring 调用 iBATIS 执行一个 Statement 的时序图（查看图 5 的 清晰版本。）iBATIS 的主要工作是连接、交互，所以必须根据不同的事务场景设计不同的交易环境。示例下面我们将根据一个具体的实例解析一个 Statement 如何完成映射的，我们用一个典型的查询语句看看 Java 对象中的数据是如何赋给 SQL 中的参数的，再看看 SQL 的查询结果是如何转成 Java 对象的。先看一下示例的部分代码和配置文件，完整的代码请看附件。Spring 的 applicationContext 配置文件：清单 1. applicationContext.xml<beans> <bean id="sqlMapTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="sqlMapTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="sqlMapTransactionManager"/> </bean> <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="configLocation" value="com/mydomain/data/SqlMapConfig.xml"/> <property name="dataSource" ref="dataSource"/> </bean> <bean id="dataSource" name="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@10.1.5.11:1521:XE"/> <property name="username" value="junshan"/> <property name="password" value="junshan"/> <property name="maxActive" value="20"/> </bean> <bean id="accountDAO" class="com.mydomain.AccountDAO"> <property name="sqlMapClient" ref="sqlMapClient"/> <property name="sqlMapTransactionTemplate" ref="sqlMapTransactionTemplate"/> </bean> </beans>下面是 Account.xml 的一个 Statement：清单 2. Account.xml 中一个 Statement select ACC_ID, ACC_FIRST_NAME as firstName, ACC_LAST_NAME as lastName, ACC_EMAIL as emailAddress, ACC_DATE from ACCOUNT where ACC_ID = #id:INTEGER# and ACC_FIRST_NAME = #firstName# 下面是 Java 的测试类：清单 3. SimpleTestpublic class SimpleTest { public static void main(String[] args) { ApplicationContext factory = new ClassPathXmlApplicationContext("/com/mydomain/data/applicationContext.xml"); final AccountDAO accountDAO = (AccountDAO) factory.getBean("accountDAO"); final Account account = new Account(); account.setId(1); account.setFirstName("tao"); account.setLastName("bao"); account.setEmailAddress("junshan@taobao.com"); account.setDate(new Date()); try { accountDAO.getSqlMapTransactionTemplate().execute(new TransactionCallback(){ public Object doInTransaction(TransactionStatus status){ try{ accountDAO.deleteAccount(account.getId()); accountDAO.insertAccount(account); //account.setLastName("bobo"); //accountDAO.updateAccount(account); Account result = accountDAO.selectAccount(account); System.out.println(result); return null; } catch (Exception e) { status.setRollbackOnly(); return false; } } }); //accountDAO.getSqlMapClient().commitTransaction(); } catch (Exception e) { e.printStackTrace(); } } }iBATIS 对 SQL 语句的解析这里所说的 SQL 解析只是针对 iBATIS 配置文件中所定义的 SQL 语句，如前一节中清单 2 中所示的查询语句。和标准的 SQL 语句不同的是，参数的赋值是"#"包裹的变量名。如何解析这个变量就是 iBATIS 要完成的工作。当然 SQL 的表达形式还有很多其他的形式如动态 SQL 等。现在我们关心的是当我们执行：清单 4. 执行查询方法accountDAO.selectAccountById(account)iBATIS 将会选择清单 2 这条 Statement 来解析，最终会把它解析成一个标准的 SQL 提交给数据库执行，并且会设置两个选择条件参数。这个过程中参数映射的细节是什么样子呢？在前面的第二小节中已经说明了，iBATIS 会把 SqlMap 配置文件解析成一个个 Statement，其中包括 ParameterMap、ResultMap，以及解析后的 SQL。当 iBATIS 构建好 RequestScope 执行环境后，要做的工作就是把传过来的对象数据结合 ParameterMap 中信息提取出一个参数数组，这个数组的顺序就是对应于 SQL 中参数的顺序，然后会调用 preparedStatement.setXXX(i, parameter) 提交参数。在清单 3 中，我们给 account 对象的 id 属性和 firstName 属性分别赋值为 1 和"tao"，当执行清单 4 中的这段代码时，iBATIS 必须把这两个属性值传给清单 2 中 SQL 语句中对象的参数。这个是怎么做到的，其实很简单，在图 3 中描述了与 ParameterMap 相关的类的关系，这些类中都保存了在 SqlMap 配置文件初始化是解析清单 2 中 Statement 的所有必要的信息，具体的信息是这样的：最终的 SQL 语句是：清单 5. 解析后的 SQLselect ACC_ID, ACC_FIRST_NAME as firstName, ACC_LAST_NAME as lastName, ACC_EMAIL as emailAddress, ACC_DATE from ACCOUNT where ACC_ID = ? and ACC_FIRST_NAME = ?#id:INTEGER# 将被解析成 JDBC 类型是 INTEGER，参数值取 Account 对象的 id 属性。#firstName# 同样被解析成 Account 对象的 firstName 属性，而 parameterClass="Account" 指明了 Account 的类类型。注意到清单 5 中 #id:INTEGER# 和 #firstName# 都被替换成"？"，iBATIS 如何保证它们的顺序？在解析清单 2 过程中，iBATIS 会根据"#"分隔符取出合法的变量名构建参数对象数组，数组的顺序就是 SQL 中变量出现的顺序。接着 iBATIS 会根据这些变量和 parameterClass 指定的类型创建合适的 dataExchange 和 parameterPlan 对象。parameterPlan 对象中按照前面的顺序保存了变量的 setter 和 getter 方法列表。所以 parameter 的赋值就是根据 parameterPlan 中保存的 getter 方法列表以及传进来的 account 对象利用反射机制得到清单 5 对应的参数值数组，再将这个数组按照指定的 JDBC 类型提交给数据库。以上这些过程可以用图 6 的时序图清楚的描述：图 6. 映射参数值到数据库过程时序图上图 4 中在 8 步骤中如果 value 值为空时会设置 preparedStatement.setNull(i , jdbcType)，如果在清单 2 中的变量没有设置 jdbcType 类型时有可能会出错。数据库字段映射到 Java 对象数据库执行完 SQL 后会返回执行结果，在第 4 小节的例子中满足 id 为 1、firstName 为"tao"的信息有两条，iBATIS 如何将这两条记录设置到 account 对象中呢？和 ParameterMap 类似，填充返回信息需要的资源都已经包含在 ResultMap 中。当有了保存返回结果的 ResultSet 对象后，就是要把列名映射到 account 对象的对应属性中。这个过程大体如下：根据 ResultMap 中定义的 ResultClass 创建返回对象，这里就是 account 对象。获取这个对象的所有可写的也就是 setter 方法的属性数组，接着根据返回 ResultSet 中的列名去匹配前面的属性数组，把匹配结果构造成一个集合（resultMappingList），后面是选择 DataExchange 类型、AccessPlan 类型为后面的真正的数据交换提供支持。根据 resultMappingList 集合从 ResultSet 中取出列对应的值，构成值数组（columnValues），这个数组的顺序就是 SQL 中对应列名的顺序。最后把 columnValues 值调用 account 对象的属性的 setter 方法设置到对象中。这个过程可以用下面的时序图来表示：图 7. 映射返回对象时序图示例运行的结果前两个小节主要描述了输入参数和输出结果的映射原理，这里再结合第 4 小节的示例分析一下执行清单 3 代码的结果。执行清单 3 所示代码打印的结果为：清单 6. 示例程序的运行结果Account{id=0, firstName='tao', lastName='bobo', emailAddress='junshan@taobao.com'}上面的结果和我们预想的结果似乎有所不同，看代码我们插入数据库的 account 对象各属性值分别为 {1，"tao"，"bao"，"junshan@taobao.com"，"时间"}，后面调用清单 2 的查询，返回应该是一样的结果才对。id 的结果不对、date 属性值丢失。再仔细看看清单 2 这个 Statement 可以发现，返回结果的列名分别是 {ACC_ID, firstName, lastName, emailAddress, ACC_DATE}，其中 id 和 date 并不能映射到 Account 类的属性中。id 被赋了默认数字 0，而 date 没有被赋值。还有一个值得注意的地方是变量 id 后面跟上 JDBC 类型，这个 JDBC 类型有没有用？通常情况下都没有用，因此你可以不设，iBATIS 会自动选择默认的类型。但是如果你要这个这个值可能为空时如果没有指定 JDBC 类型可能就有问题了，在 Oracle 中虽然能正常工作但是会引起 Oracle 这当前这个 SQL 有多次编译现象，因此会影响数据库的性能。还有当同一个 Java 类型如果对应多个 JDBC 类型（如 Date 对应 JDBC 类型有 java.sql.Date、java.sql.Timestamp）就可以通过指定 JDBC 类型保存不同的值到数据库。总结如果用最简洁的话来总结 iBATIS 主要完成那些功能时，我想下面几个代码足够概括。清单 7. 典型的 Java 操作数据库代码Class.forName("oracle.jdbc.driver.OracleDriver"); Connection conn= DriverManager.getConnection(url,user,password); java.sql.PreparedStatement st = conn.prepareStatement(sql); st.setInt(0,1); st.execute(); java.sql.ResultSet rs = st.getResultSet(); while(rs.next()){ String result = rs.getString(colname); }iBATIS 就是将上面这几行代码分解包装，但是最终执行的仍然是这几行代码。前两行是对数据库的数据源的管理包括事务管理，3、4 两行 iBATIS 通过配置文件来管理 SQL 以及输入参数的映射，6、7、8 行是 iBATIS 获取返回结果到 Java 对象的映射，他也是通过配置文件管理。配置文件对应到相应代码如图所示：图 8. 配置文件与相应代码对应关系iBATIS 要达到目的就是把用户关心的和容易变化的数据放到配置文件中配置，方便用户管理。而把流程性的、固定不变的交给 iBATIS 来实现。这样是用户操作数据库简单、方便，这也是 iBATIS 的价值所在。下载资源本文示例代码 (code_example.rar | 7 KB)相关主题"扩展 iBatis 以透明支持多种数据库"（developerWorks，2007 年 12 月）：本文提供了一个简单有效的方法，通过扩展 iBatis 来透明地支持多数据库方言。"iBATIS 3 内的新特性"（developerWorks，2010 年 4 月）：本文将介绍 iBATIS 3 内的一些新特性。developerWorks Java 技术专区：数百篇关于 Java 编程各个方面的文章。说明：本文基于 iBATIS 框架编写。iBATIS 项目在 2010 年已更名为 MyBatis，后续版本特性与架构有所演进，但核心映射原理基本一致。文中部分链接可能因年代久远失效，请以官方最新文档为准。
深度剖析MyBatis：核心类的奥秘与强大功能
在 Java 持久层框架的世界里，MyBatis 以其灵活、高效的特性备受开发者青睐。今天，让我们一同深入探究 MyBatis 中几个至关重要的类，揭开它们的神秘面纱，领略 MyBatis 的强大魅力。一、MappedStatement：SQL 语句的映射使者（一）功能概述MappedStatement 在 MyBatis 框架中扮演着关键角色，它犹如一座桥梁，将 XML 文件中的 SQL 语句节点（如 、<update>、<insert> 标签）与 Java 代码紧密相连。在 MyBatis 框架初始化阶段，会对 XML 配置文件进行深度扫描和解析，将其中的 SQL 语句节点逐一转化为一个个 MappedStatement 对象，从而构建起 SQL 语句与代码逻辑之间的映射关系。（二）实例解析以一个简单的 XML Mapper 文件为例：<?xml version="1.0" encoding="UTF-8"?> <mapper namespace="mybatis.UserDao"> <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/> <resultMap id="userResultMap" type="UserBean"> <id property="userId" column="user_id"/> <result property="userName" column="user_name"/> <result property="userPassword" column="user_password"/> <result property="createDate" column="create_date"/> </resultMap> select * from user <where> <if test="userName!=null and userName!=''"> and user_name = #{userName} </if> <if test="userPassword!=null and userPassword!=''"> and user_password = #{userPassword} </if> <if test="createDate!=null"> and create_date = #{createDate} </if> </where> select * from user <where> <if test="userName!=null and userName!=''"> and user_name = #{userName} </if> <if test="userPassword!=null and userPassword!=''"> and user_password = #{userPassword} </if> <if test="createDate!=null"> and create_date = #{createDate} </if> </where> </mapper>MyBatis 解析该文件后，会注册两个 MappedStatement 对象，分别对应 id 为 find 和 find2 的 节点。在 MyBatis 框架中，为了确保每个 MappedStatement 对象的唯一性，其标识采用 Mapper 文件的 namespace 加上节点本身的 id 值。例如，上述两个 MappedStatement 对象在 MyBatis 框架中的唯一标识分别是 mybatis.UserDao.find 和 mybatis.UserDao.find2。（三）源码探秘查看 MappedStatement 对象的源码，我们可以看到其中包含了众多属性，这些属性与 XML 元素的属性存在着紧密的对应关系。其中，比较关键的属性包括：ParameterMap 对象：用于表示查询参数，它明确了输入参数的类型和映射关系，确保 SQL 语句在执行时能够正确获取参数值。ResultMap 列表（resultMaps）：负责定义 SQL 查询结果与 Java 对象之间的映射规则，使得 MyBatis 能够将从数据库中获取的数据准确无误地封装成 JavaBean 对象，方便在 Java 代码中进行处理。SqlSource 对象：这是最为重要的属性之一，它承担着执行动态 SQL 计算和获取的重任。通过这个对象，MappedStatement 能够根据用户提供的查询参数对象，动态生成要执行的 SQL 语句，充分展现了 MyBatis 的灵活性。（四）工作流程MappedStatement 对象的工作流程清晰而高效。当用户发起查询请求时，它首先接收用户传递的查询参数对象，然后借助 SqlSource 对象，根据参数对象的具体值动态计算出实际要执行的 SQL 语句。接着，将计算好的 SQL 语句发送到数据库执行，获取查询结果。最后，利用 ResultMap 列表将查询结果封装为 JavaBean 对象，并返回给用户。这个过程完美体现了 MyBatis 的核心价值：“根据用户提供的查询参数对象，动态执行 SQL 语句，并将结果封装为 Java 对象”。（五）类图展示class MappedStatement { - resource: String - configuration: Configuration - id: String - fetchSize: Integer - timeout: Integer - statementType: StatementType - resultSetType: ResultSetType - sqlSource: SqlSource - cache: Cache - parameterMap: ParameterMap - resultMaps: List<ResultMap> - flushCacheRequired: boolean - useCache: boolean - resultOrdered: boolean - sqlCommandType: SqlCommandType - keyGenerator: KeyGenerator - keyProperties: String[] - keyColumns: String[] - hasNestedResultMaps: boolean - databaseId: String - statementLog: Log - lang: LanguageDriver + MappedStatement() }二、SqlSource：动态 SQL 的幕后大师（一）接口定义SqlSource 是一个接口类，在 MappedStatement 对象中作为一个关键属性存在。其代码如下：package org.apache.ibatis.mapping; public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }这个接口仅有一个方法 getBoundSql(Object parameterObject)，该方法返回一个 BoundSql 对象。BoundSql 对象代表了一次 SQL 语句的实际执行内容，而 SqlSource 对象的核心职责就是根据传入的参数对象，动态计算并生成这个 BoundSql 对象。（二）常用实现类SqlSource 最常用的实现类是 DynamicSqlSource，其代码如下：package org.apache.ibatis.scripting.xmltags; import java.util.Map; import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.session.Configuration; public class DynamicSqlSource implements SqlSource { private Configuration configuration; private SqlNode rootSqlNode; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; } public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; } }在 getBoundSql 方法中，通过创建 DynamicContext 对象，并调用 rootSqlNode.apply(context) 启动了一个基于递归实现的动态计算 SQL 语句的过程。这个过程借助 OGNL 根据传入的参数对象计算表达式，从而生成该次调用过程中实际要执行的 SQL 语句。（三）动态 SQL 计算过程首先，创建 DynamicContext 对象，传入 Configuration 和参数对象。DynamicContext 会对参数对象进行"Map 化”处理，即将传入的 POJO 对象转换为一个类似 Map 的数据结构，以便后续统一使用 Map 接口方法来访问数据。接着，调用 rootSqlNode.apply(context)，这是动态计算 SQL 语句的核心步骤。rootSqlNode 会根据传入的参数对象，通过 OGNL 计算表达式，逐步构建出实际要执行的 SQL 语句，并将其添加到 DynamicContext 的 sqlBuilder 中。然后，创建 SqlSourceBuilder 对象，使用它来解析 DynamicContext 中的 SQL 语句和参数类型，生成一个新的 SqlSource 对象。最后，从新生成的 SqlSource 对象中获取 BoundSql 对象，并将 DynamicContext 中的绑定参数设置到 BoundSql 对象中，最终返回 BoundSql 对象，代表了这次动态计算得到的实际 SQL 语句和相关参数。（四）类图展示interface SqlSource { + getBoundSql(Object parameterObject): BoundSql } class DynamicSqlSource { - configuration: Configuration - rootSqlNode: SqlNode + DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) + getBoundSql(Object parameterObject): BoundSql }三、DynamicContext：参数处理的核心枢纽（一）功能介绍DynamicContext 类在 MyBatis 的参数处理和动态 SQL 计算过程中起着至关重要的作用。它主要负责对传入的参数对象进行处理，将其转换为适合动态 SQL 计算的格式，并提供了一系列方法用于操作和获取 SQL 相关的信息。（二）源码分析在 DynamicContext 的构造函数中，根据传入的参数对象是否为 Map 类型，有两种不同的处理方式来构造 ContextMap 对象。ContextMap 是一个继承自 HashMap 的内部类，其作用是统一参数的访问方式，使得无论是普通的 POJO 对象还是 Map 对象，都可以通过 Map 接口方法来访问数据。当传入的参数对象不是 Map 类型时，MyBatis 会使用 MetaObject 对象对其进行封装。在动态计算 SQL 过程中，当需要获取数据时，通过 Map 接口的 get 方法包装 MetaObject 对象的取值过程，从而实现对 POJO 对象属性的访问。DynamicContext 类中的静态初始块 static { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } 表明 MyBatis 使用 OGNL 来计算动态 SQL 语句。ContextAccessor 是 DynamicContext 的内部类，实现了 OGNL 中的 PropertyAccessor 接口，为 OGNL 提供了如何使用 ContextMap 参数对象的具体说明，从而完成了整个参数对象的"Map 化”处理。（三）参数传递和使用过程传入的参数对象首先被统一封装为 ContextMap 对象。OGNL 运行时环境在动态计算 SQL 语句时，按照 ContextAccessor 中描述的 Map 接口方式来访问和读取 ContextMap 对象，获取计算过程中所需的参数。ContextMap 对象内部可能封装了一个普通的 POJO 对象，也可能是直接传递的 Map 对象，但从外部来看，都是通过 Map 接口来读取数据，实现了对不同类型参数的无差别化处理。（四）类图展示class DynamicContext { - bindings: ContextMap - sqlBuilder: StringBuilder - uniqueNumber: int + PARAMETER_OBJECT_KEY: String + DATABASE_ID_KEY: String + DynamicContext(Configuration configuration, Object parameterObject) + getBindings(): Map<String, Object> + bind(String name, Object value) + appendSql(String sql) + getSql(): String + getUniqueNumber(): int static class ContextMap { - parameterMetaObject: MetaObject + ContextMap(MetaObject parameterMetaObject) + get(Object key): Object } static class ContextAccessor { + getProperty(Map context, Object target, Object name): Object + setProperty(Map context, Object target, Object name, Object value) } }（五）示例验证以下是一个 JUnit 测试方法，用于验证 MyBatis 参数获取过程中对 Map 对象和普通 POJO 对象的无差别化处理：@Test public void testSqlSource() throws Exception { String resource = "mybatis/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder() .build(inputStream); SqlSession session = sqlSessionFactory.openSession(); try { Configuration configuration = session.getConfiguration(); MappedStatement mappedStatement = configuration .getMappedStatement("mybatis.UserDao.find2"); assertNotNull(mappedStatement); UserBean param = new UserBean(); param.setUserName("admin"); param.setUserPassword("admin"); BoundSql boundSql = mappedStatement.getBoundSql(param); String sql = boundSql.getSql(); Map<String, Object> map = new HashMap<>(); map.put("userName", "admin"); map.put("userPassword", "admin"); BoundSql boundSql2 = mappedStatement.getBoundSql(map); String sql2 = boundSql2.getSql(); assertEquals(sql, sql2); UserBean bean = session.selectOne("mybatis.UserDao.find2", map); assertNotNull(bean); } finally { session.close(); } }在这个测试中，第一次使用 UserBean 对象获取和计算 SQL 语句，第二次使用 HashMap 对象进行同样的操作，甚至直接使用 HashMap 对象启动了一次 session 对象的查询。测试结果通过，充分说明了 MyBatis 在参数获取过程中，对 Map 对象和普通 POJO 对象的无差别化处理，因为在内部，两者都会被封装，然后通过 Map 接口来访问。总结通过对 MyBatis 中这几个重要类的深入剖析，我们清晰地了解了 MyBatis 的核心工作机制。MappedStatement 作为 SQL 语句的映射使者，协调了 SQL 与 Java 代码之间的关系；SqlSource 则是动态 SQL 的幕后大师，根据参数动态生成可执行的 SQL 语句；DynamicContext 作为参数处理的核心枢纽，确保了参数的统一访问和动态 SQL 计算的顺利进行。这些类相互协作，共同构建了 MyBatis 强大而灵活的持久层框架，为开发者提供了高效、便捷的数据库操作体验。说明：本文基于 MyBatis 3.x 版本进行剖析，核心类结构与机制在后续版本中保持相对稳定，但具体实现细节可能随版本迭代有所调整。
MyBatis缓存机制深度剖析：原理、源码与最佳实践
在当今的软件开发领域，数据访问效率一直是备受关注的焦点。随着应用程序规模的不断扩大，数据库操作的性能成为了影响系统整体性能的关键因素之一。MyBatis 作为一款流行的 Java 持久层框架，其缓存机制在提升数据访问效率方面发挥着重要作用。本文将深入剖析 MyBatis 的缓存机制，包括一级缓存和二级缓存的原理、源码实现以及最佳实践，旨在帮助读者全面理解并掌握这一特性，从而在实际应用中优化数据库操作性能。缓存机制概述MyBatis 提供了两级缓存机制来减少数据库交互次数，提升查询性能：一级缓存（Local Cache）：基于 SqlSession 级别，默认开启。二级缓存（Global Cache）：基于 Mapper（Namespace）级别，需手动配置开启。一级缓存MyBatis 的一级缓存基于 PerpetualCache 的 HashMap 实现，存储作用域为 SqlSession。当一个 SqlSession 被创建时，MyBatis 会为其创建一个对应的一级缓存对象。在该 SqlSession 进行数据库查询操作时，查询结果会被缓存在这个 HashMap 中。当再次执行相同的查询语句时，MyBatis 会先从一级缓存中查找，如果找到匹配的结果，则直接返回缓存中的数据，避免了再次与数据库交互，从而提高了查询效率。以下是一级缓存的简单示例代码：SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 第一次查询 User user1 = userMapper.getUserById(1); // 第二次查询，会从一级缓存中获取数据 User user2 = userMapper.getUserById(1); // user1 和 user2 是同一个对象 System.out.println(user1 == user2); } finally { sqlSession.close(); }二级缓存二级缓存与一级缓存机制相似，默认也使用 PerpetualCache 和 HashMap 存储。但其存储作用域为 Mapper（Namespace），这意味着不同的 Mapper 可以拥有独立的二级缓存。二级缓存可以通过配置文件进行自定义，支持多种存储源，如 Ehcache、Redis 等，以满足不同场景的需求。当在多个 SqlSession 之间共享数据时，二级缓存发挥着重要作用。在配置文件中开启二级缓存的示例如下：<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>缓存数据更新机制当在某一个作用域（一级缓存 SqlSession / 二级缓存 Namespace）内进行了插入（Insert）、更新（Update）或删除（Delete）操作后，默认情况下该作用域下所有的 SELECT 查询缓存将被清除。这确保了缓存数据与数据库数据的一致性，避免了因数据更新而导致的缓存数据过时问题。核心设计：Delegate 与装饰器模式MyBatis 的缓存采用了 Delegate（委托）机制及装饰器模式（Decorator Pattern）设计。当进行 put、get、remove 等操作时，会经过多层 Delegate Cache 处理。Cache 类别主要包括：BaseCache：基础缓存，是缓存数据最终存储的处理类，默认为 PerpetualCache，基于 Map 存储。EvictionCache：排除算法缓存，在缓存数量达到一定大小后，通过算法（如默认的 LRU 算法）对缓存数据进行清除。DecoratorCache：装饰器缓存，用于在缓存 put/get 处理前后进行装饰。例如：LoggingCache：输出缓存命中日志信息。SerializedCache：对 Cache 的数据 put 或 get 进行序列化及反序列化处理。源码深度剖析执行过程接口调用与方法拦截：在 Service 层调用 Mapper Interface 中的方法时，实际上调用的是 MapperProxy 中的方法，MapperProxy 会拦截所有方法调用。方法执行与数据库操作：MapperProxy 将方法调用委托给 MapperMethod 处理，MapperMethod 根据操作类型（如 INSERT、UPDATE、DELETE、SELECT）调用 DefaultSqlSession 中的相应执行方法。缓存执行器的选择：在 DefaultSqlSession 的 selectOne 或 selectList 方法中，会根据是否启用缓存来决定使用 CachingExecutor 还是 BaseExecutor 进行数据库查询。缓存查询与数据获取：如果启用了缓存，CachingExecutor 会先从二级缓存中查询数据；如果未找到，则委托给 BaseExecutor 从一级缓存或数据库中查询，并将查询结果存入缓存。执行器（Executor）结构及构建过程BaseExecutor：基础执行器抽象类，实现了一些通用方法（如 createCacheKey 等），并采用模板模式将具体的数据库操作逻辑（doUpdate、doQuery）交由子类实现。BaseExecutor 中维护了一个 localCache，用于实现一级缓存。BatchExecutor、ReuseExecutor、SimpleExecutor：这几个执行器继承了 BaseExecutor，分别实现了批量执行、重用 Statement 执行、普通方式执行数据库操作，它们在 doQuery、doUpdate 等方法中采用 JDBC 对数据库进行操作。CachingExecutor：二级缓存执行器，通过委托机制将查询操作委托给 BaseExecutor。当从二级缓存中获取数据失败时，会委托 BaseExecutor 从一级缓存或数据库中查询数据。Cache 委托链构建MyBatis 在解析 Mapper 配置文件时构建缓存实例。通过 XMLMapperBuilder 的 cacheElement 方法解析配置文件中的缓存配置信息，然后使用 CacheBuilder 构建缓存实例。CacheBuilder 采用 Builder 模式，根据配置信息创建基础缓存实例，并通过一系列装饰器（如 EvictionCache、DecoratorCache 等）对其进行装饰，最终生成一个责任链形式的缓存实例对象。Cache 实例解剖SynchronizedCache：用于控制 ReadWriteLock，避免并发时的线程安全问题。在 put 和 get 操作时，分别获取 Write 锁和 Read 锁，确保数据的一致性。LoggingCache：主要用于输出缓存命中率信息。在 get 操作时，统计命中次数和总请求次数，并在日志中输出命中率。SerializedCache：在向缓存中 put 或 get 数据时进行序列化及反序列化处理，确保缓存数据可以在不同的环境中正确存储和读取。LruCache：基于 LRU（最近最少使用）算法实现，通过覆盖 LinkedHashMap 的 removeEldestEntry 方法，移除最长时间不被使用的对象，以控制缓存的大小。PerpetualCache：直接使用 HashMap 来存储缓存数据，是最基础的缓存实现类。实战：自定义二级缓存（Redis）在实际生产环境中，常使用 Redis 作为分布式二级缓存。实现步骤如下：自定义 Redis 缓存实现类 RedisCache，实现 Cache 接口。在 RedisCache 的构造方法中，创建 JedisPool 连接池，用于与 Redis 服务器进行通信。实现 getObject、putObject、removeObject 等方法，通过 JedisPool 与 Redis 进行交互，实现数据的读取、写入和删除操作。在 Mapper 配置文件中指定使用自定义的 RedisCache 作为二级缓存。以下是自定义 RedisCache 的示例代码：import org.apache.ibatis.cache.Cache; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class RedisCache implements Cache { private final String id; private final JedisPool jedisPool; private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public RedisCache(String id) { this.id = id; // 配置 Redis 连接池 jedisPool = new JedisPool("localhost", 6379); } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { try (Jedis jedis = jedisPool.getResource()) { jedis.hset(id, key.toString(), value.toString()); } } @Override public Object getObject(Object key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hget(id, key.toString()); } } @Override public Object removeObject(Object key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hdel(id, key.toString()); } } @Override public void clear() { try (Jedis jedis = jedisPool.getResource()) { jedis.del(id); } } @Override public int getSize() { try (Jedis jedis = jedisPool.getResource()) { return jedis.hlen(id); } } @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } }在 Mapper 配置文件中使用自定义 RedisCache 的配置如下：<cache type="com.example.RedisCache"/>最佳实践与注意事项合理使用缓存场景选择：对于频繁查询且数据变化不频繁的场景，充分利用一级缓存和二级缓存可以显著提高查询性能。例如，在查询系统配置信息、字典表数据等场景中，缓存可以大大减少数据库查询次数。资源控制：避免在缓存中存储大量不常用的数据，以免占用过多内存资源。定期评估缓存数据的有效性，及时清理过期或不再使用的数据。注意缓存更新一致性维护：当执行插入、更新或删除操作时，要确保相关缓存数据被正确清除或更新，以维护数据的一致性。可以通过手动调用缓存清除方法或利用 MyBatis 的缓存更新机制来实现。分布式同步：在分布式环境中，要注意缓存的同步问题，避免不同节点之间的缓存数据不一致。可以使用分布式缓存解决方案（如 Redis 的分布式模式）来确保缓存数据的一致性。配置优化参数调整：根据实际应用场景，合理调整缓存的参数，如二级缓存的大小、刷新间隔等。例如，如果系统并发查询量较大，可以适当增大二级缓存的大小；如果数据更新较为频繁，可以缩短二级缓存的刷新间隔。淘汰算法：选择合适的缓存淘汰算法（如 LRU、FIFO 等），以适应不同的业务需求。LRU 算法适用于热点数据频繁访问的场景，而 FIFO 算法则适用于数据访问顺序较为固定的场景。监控与调试日志监控：启用 MyBatis 的日志功能，监控缓存的命中情况、数据加载时间等指标，以便及时发现缓存使用中的问题。可以通过分析日志信息来评估缓存的性能和有效性。调试跟踪：在开发和测试过程中，使用调试工具跟踪缓存的操作过程，检查缓存数据的正确性和完整性。避免滥用缓存适用性评估：并非所有查询都适合使用缓存，对于数据实时性要求较高或查询条件复杂多变的场景，缓存可能无法带来明显的性能提升，甚至可能导致数据不一致的问题。成本考量：在使用缓存时，要充分考虑缓存的维护成本和复杂性，确保缓存的使用能够带来实际的性能收益。说明：本文基于 MyBatis 3.x 版本机制进行剖析。在现代微服务架构中，鉴于二级缓存可能带来的分布式一致性问题，建议优先采用外部集中式缓存（如 Redis）替代 MyBatis 原生二级缓存，或将缓存逻辑上移至 Service 层统一管理。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。缓存概述正如大多数持久层框架一样，MyBatis 同样提供了一级缓存和二级缓存的支持：一级缓存：基于 PerpetualCache 的 HashMap 本地缓存。其存储作用域为 Session，当 Session flush 或 close 之后，该 Session 中的所有 Cache 将被清空。二级缓存：与一级缓存机制相同，默认也是采用 PerpetualCache（HashMap 存储）。不同在于其存储作用域为 Mapper (Namespace)，并且可自定义存储源，如 Ehcache、Hazelcast 等。缓存更新机制：当某一个作用域（一级缓存 Session / 二级缓存 Namespaces）进行了 C/U/D 操作后，默认该作用域下所有 select 中的缓存将被 clear。设计模式：MyBatis 的缓存采用了 Delegate 机制及装饰器模式设计。当 put、get、remove 时，其中会经过多层 Delegate Cache 处理。其 Cache 类别主要有：BaseCache（基础缓存）：为缓存数据最终存储的处理类，默认为 PerpetualCache，基于 Map 存储；可自定义存储处理，如基于 EhCache、Memcached 等。EvictionCache（排除算法缓存）：当缓存数量达到一定大小后，将通过算法对缓存数据进行清除。默认采用 LRU 算法 (LruCache)，提供有 FIFO 算法 (FifoCache) 等。DecoratorCache（装饰器缓存）：缓存 put/get 处理前后的装饰器。例如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache 的数据 put 或 get 进行序列化及反序列化处理、当设置 flushInterval (默认 1 小时) 后，则使用 ScheduledCache 对缓存数据进行定时刷新等。Key 生成规则：一般缓存框架的数据结构基本上都是 Key-Value 方式存储，MyBatis 对于其 Key 的生成采取规则为：[hashcode : checksum : mappedStatementId : offset : limit : executeSql : queryParams]。并发同步：对于并发 Read/Write 时缓存数据的同步问题，MyBatis 默认基于 JDK concurrent 中的 ReadWriteLock，使用 ReentrantReadWriteLock 的实现，从而通过 Lock 机制防止在并发 Write Cache 过程中出现线程安全问题。源码剖解接下来将结合 MyBatis 序列图进行源码分析。在分析其 Cache 前，先看看其整个处理过程。执行过程通常情况下，我们需要在 Service 层调用 Mapper Interface 中的方法实现对数据库的操作，上述根据产品 ID 获取 Product 对象。当调用 ProductMapper 中的方法时，其实这里所调用的是 MapperProxy 中的方法，并且 MapperProxy 已经将所有的方法拦截。其具体原理及分析，参考 MyBatis+Spring 基于接口编程的原理分析，其 invoke 方法代码为：// 当调用 Mapper 所有的方法时，将都交由 Proxy 中的 invoke 处理 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (!OBJECT_METHODS.contains(method.getName())) { final Class declaringInterface = findDeclaringInterface(proxy, method); // 最终交由 MapperMethod 类处理数据库操作，初始化 MapperMethod 对象 final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession); // 执行 mapper method，返回执行结果 final Object result = mapperMethod.execute(args); .... return result; } } catch (SQLException e) { e.printStackTrace(); } return null; }其中的 mapperMethod 中的 execute 方法代码如下：public Object execute(Object[] args) throws SQLException { Object result; // 根据不同的操作类别，调用 DefaultSqlSession 中的执行处理 if (SqlCommandType.INSERT == type) { Object param = getParam(args); result = sqlSession.insert(commandName, param); } else if (SqlCommandType.UPDATE == type) { Object param = getParam(args); result = sqlSession.update(commandName, param); } else if (SqlCommandType.DELETE == type) { Object param = getParam(args); result = sqlSession.delete(commandName, param); } else if (SqlCommandType.SELECT == type) { if (returnsList) { result = executeForList(args); } else { Object param = getParam(args); result = sqlSession.selectOne(commandName, param); } } else { throw new BindingException("Unknown execution method for: " + commandName); } return result; }由于这里是根据 ID 进行查询，所以最终调用为 sqlSession.selectOne 函数，也就是接下来的 DefaultSqlSession.selectOne 执行。⑤ 可以在 DefaultSqlSession 看到，其 selectOne 调用了 selectList 方法：public Object selectOne(String statement, Object parameter) { List list = selectList(statement, parameter); if (list.size() == 1) { return list.get(0); } ... } public List selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 如果启用了 Cache 才调用 CachingExecutor.query，反之则使用 BaseExecutor.query 进行数据库查询 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(); } }到这里，已经执行到具体数据查询的流程。在分析 CachingExecutor.query 前，先看看 MyBatis 中 Executor 的结构及构建过程。执行器（Executor）Executor：执行器接口，也是最终执行数据获取及更新的实例。其类结构如下：BaseExecutor：基础执行器抽象类。实现一些通用方法，如 createCacheKey 之类。并且采用模板模式将具体的数据库操作逻辑 (doUpdate、doQuery) 交由子类实现。另外，可以看到变量 localCache: PerpetualCache，在该类采用 PerpetualCache 实现基于 Map 存储的一级缓存，其 query 方法如下：public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); // 执行器已关闭 if (closed) throw new ExecutorException("Executor was closed."); List list; try { queryStack++; // 创建缓存 Key CacheKey key = createCacheKey(ms, parameter, rowBounds); // 从本地缓存在中获取该 key 所对应 的结果集 final List cachedList = (List) localCache.getObject(key); // 在缓存中找到数据 if (cachedList != null) { list = cachedList; } else { // 未从本地缓存中找到数据，开始调用数据库查询 // 为该 key 添加一个占位标记 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 执行子类所实现的数据库查询 操作 list = doQuery(ms, parameter, rowBounds, resultHandler); } finally { // 删除该 key 的占位标记 localCache.removeObject(key); } // 将 db 中的数据添加至本地缓存中 localCache.putObject(key, list); } } finally { queryStack--; } // 刷新当前队列中的所有 DeferredLoad 实例，更新 MateObject if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } } return list; }BatchExecutor、ReuseExecutor、SimpleExecutor：这几个继承了 BaseExecutor 的实现，实现其 doQuery、doUpdate 等方法，同样都是采用 JDBC 对数据库进行操作。三者区别在于：批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在 MyBatis 的文档上都有详细说明。CachingExecutor：二级缓存执行器。这里灵活地使用了 Delegate 机制，其委托执行的类是 BaseExecutor。当无法从二级缓存获取数据时，同样需要从 DB 中进行查询，于是在这里可以直接委托给 BaseExecutor 进行查询。其大概流程为：流程为：从二级缓存中进行查询 -> [如果缓存中没有，委托给 BaseExecutor] -> 进入一级缓存中查询 -> [如果也没有] -> 则执行 JDBC 查询。其 query 代码如下：public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { if (ms != null) { // 获取二级缓存实例 Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); // 获取 读锁 ( Read 锁可由多个 Read 线程同时保持) cache.getReadWriteLock().readLock().lock(); try { // 当前 Statement 是否启用了二级缓存 if (ms.isUseCache()) { // 将创建 cache key 委托给 BaseExecutor 创建 CacheKey key = createCacheKey(ms, parameterObject, rowBounds); final List cachedList = (List) cache.getObject(key); // 从二级缓存中找到缓存数据 if (cachedList != null) { return cachedList; } else { // 未找到缓存，委托给 BaseExecutor 执行查询 List list = delegate.query(ms, parameterObject, rowBounds, resultHandler); tcm.putObject(cache, key, list); return list; } } else { // 没有启用二级缓存，直接委托给 BaseExecutor 执行查询 return delegate.query(ms, parameterObject, rowBounds, resultHandler); } } finally { // 当前线程释放 Read 锁 cache.getReadWriteLock().readLock().unlock(); } } } return delegate.query(ms, parameterObject, rowBounds, resultHandler); }至此，已经完成了整个缓存执行器的流程分析。接下来是对缓存的数据管理实例进行分析，也就是其 Cache 接口，用于对缓存数据 put、get 及 remove 的实例对象。Cache 委托链构建正如最开始的缓存概述所描述，其缓存类的设计采用装饰模式，基于委托的调用机制。缓存实例构建：缓存实例的构建，MyBatis 在解析其 Mapper 配置文件时就已经将该实现初始化。在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中可以看到：private void cacheElement(XNode context) throws Exception { if (context != null) { // 基础缓存类型 String type = context.getStringAttribute("type", "PERPETUAL"); Class typeClass = typeAliasRegistry.resolveAlias(type); // 排除算法缓存类型 String eviction = context.getStringAttribute("eviction", "LRU"); Class evictionClass = typeAliasRegistry.resolveAlias(eviction); // 缓存自动刷新时间 Long flushInterval = context.getLongAttribute("flushInterval"); // 缓存存储实例引用的大小 Integer size = context.getIntAttribute("size"); // 是否是只读缓存 boolean readWrite = !context.getBooleanAttribute("readOnly", false); Properties props = context.getChildrenAsProperties(); // 初始化缓存实现 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); } }以下是 useNewCache 方法实现：public Cache useNewCache(Class typeClass, Class evictionClass, Long flushInterval, Integer size, boolean readWrite, Properties props) { typeClass = valueOrDefault(typeClass, PerpetualCache.class); evictionClass = valueOrDefault(evictionClass, LruCache.class); // 这里构建 Cache 实例采用 Builder 模式，每一个 Namespace 生成一个 Cache 实例 Cache cache = new CacheBuilder(currentNamespace) // Builder 前设置一些从 XML 中解析过来的参数 .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .properties(props) // 再看下面的 build 方法实现 .build(); configuration.addCache(cache); currentCache = cache; return cache; } public Cache build() { setDefaultImplementations(); // 创建基础缓存实例 Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); // 缓存排除算法初始化，并将其委托至基础缓存中 for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 标准装饰器缓存设置，如 LoggingCache 之类，同样将其委托至基础缓存中 cache = setStandardDecorators(cache); // 返回最终缓存的责任链对象 return cache; }最终生成后的缓存实例对象结构：可见，所有构建的缓存实例已经通过责任链方式将其串连在一起，各 Cache 各负其责、依次调用，直到缓存数据被 Put 至基础缓存实例中存储。Cache 实例解剖实例类：SynchronizedCache说明：用于控制 ReadWriteLock，避免并发时所产生的线程安全问题。解剖：对于 Lock 机制来说，其分为 Read 和 Write 锁。其 Read 锁允许多个线程同时持有，而 Write 锁一次能被一个线程持有。如果当 Write 锁没有释放，其它需要 Write 的线程只能等待其释放才能去持有。其代码实现：public void putObject(Object key, Object object) { acquireWriteLock(); // 获取 Write 锁 try { delegate.putObject(key, object); // 委托给下一个 Cache 执行 put 操作 } finally { releaseWriteLock(); // 释放 Write 锁 } }对于 Read 数据来说，也是如此，不同的是 Read 锁允许多线程同时持有：public Object getObject(Object key) { acquireReadLock(); try { return delegate.getObject(key); } finally { releaseReadLock(); } }其具体原理可以看看 JDK concurrent 中的 ReadWriteLock 实现。实例类：LoggingCache说明：用于日志记录处理，主要输出缓存命中率信息。解剖：说到缓存命中信息的统计，只有在 get 的时候才需要统计命中率：public Object getObject(Object key) { requests++; // 每调用一次该方法，则获取次数 +1 final Object value = delegate.getObject(key); if (value != null) { // 命中！命中 +1 hits++; } if (log.isDebugEnabled()) { // 输出命中率。计算方法为：hits / requests 则为命中率 log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); } return value; }实例类：SerializedCache说明：向缓存中 put 或 get 数据时的序列化及反序列化处理。解剖：序列化在 Java 里面已经是最基础的东西了，这里也没有什么特殊之处：public void putObject(Object key, Object object) { // PO 类需要实现 Serializable 接口 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); } } public Object getObject(Object key) { Object object = delegate.getObject(key); // 获取数据时对 byte 数据进行反序列化 return object == null ? null : deserialize((byte[]) object); }其 serialize 及 deserialize 代码就不必要贴了。实例类：LruCache说明：最近最少使用的：移除最长时间不被使用的对象，基于 LRU 算法。解剖：这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好像之前看过 XMemcached 的 LRU 算法也是这样实现的。初始化 LinkedHashMap，默认为大小为 1024 个元素：public LruCache(Cache delegate) { this.delegate = delegate; setSize(1024); // 设置 map 默认大小 } public void setSize(final int size) { // 设置其 capacity 为 size, 其 factor 为.75F keyMap = new LinkedHashMap(size, .75F, true) { // 覆盖该方法，当每次往该 map 中 put 时数据时，如该方法返回 True，便移除该 map 中使用最少的 Entry // 其参数 eldest 为当前最老的 Entry protected boolean removeEldestEntry(Map.Entry eldest) { boolean tooBig = size() > size; if (tooBig) { eldestKey = eldest.getKey(); // 记录当前最老的缓存数据的 Key 值，因为要委托给下一个 Cache 实现删除 } return tooBig; } }; } public void putObject(Object key, Object value) { delegate.putObject(key, value); cycleKeyList(key); // 每次 put 后，调用移除最老的 key } // 看看当前实现是否有 eldestKey, 有的话就调用 removeObject，将该 key 从 cache 中移除 private void cycleKeyList(Object key) { keyMap.put(key, key); // 存储当前 put 到 cache 中的 key 值 if (eldestKey != null) { delegate.removeObject(eldestKey); eldestKey = null; } } public Object getObject(Object key) { keyMap.get(key); // 便于 该 Map 统计 get 该 key 的次数 return delegate.getObject(key); }实例类：PerpetualCache说明：这个比较简单，直接通过一个 HashMap 来存储缓存数据。所以没什么说的，直接看下面的 MemcachedCache 吧。自定义二级缓存：Memcached 实现其自定义二级缓存也较为简单，它本身默认提供了对 Ehcache 及 Hazelcast 的缓存支持：Mybatis-Cache。我这里参考它们的实现，自定义了针对 Memcached 的缓存支持，其代码如下：package com.xx.core.plugin.mybatis; import java.util.LinkedList; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.ibatis.cache.Cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.xx.core.memcached.JMemcachedClientAdapter; import com.xx.core.memcached.service.CacheService; import com.xx.core.memcached.service.MemcachedService; /** * Cache adapter for Memcached. * * @author denger */ public class MemcachedCache implements Cache { // Sf4j logger reference private static Logger logger = LoggerFactory.getLogger(MemcachedCache.class); /** The cache service reference. */ protected static final CacheService CACHE_SERVICE = createMemcachedService(); /** The ReadWriteLock. */ private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; private LinkedList<String> cacheKeys = new LinkedList<String>(); public MemcachedCache(String id) { this.id = id; } // 创建缓存服务类，基于 java-memcached-client protected static CacheService createMemcachedService() { JMemcachedClientAdapter memcachedAdapter; try { memcachedAdapter = new JMemcachedClientAdapter(); } catch (Exception e) { String msg = "Initial the JMmemcachedClientAdapter Error."; logger.error(msg, e); throw new RuntimeException(msg); } return new MemcachedService(memcachedAdapter); } @Override public String getId() { return this.id; } // 根据 key 从缓存中获取数据 @Override public Object getObject(Object key) { String cacheKey = String.valueOf(key.hashCode()); Object value = CACHE_SERVICE.get(cacheKey); if (!cacheKeys.contains(cacheKey)){ cacheKeys.add(cacheKey); } return value; } @Override public ReadWriteLock getReadWriteLock() { return this.readWriteLock; } // 设置数据至缓存中 @Override public void putObject(Object key, Object value) { String cacheKey = String.valueOf(key.hashCode()); if (!cacheKeys.contains(cacheKey)){ cacheKeys.add(cacheKey); } CACHE_SERVICE.put(cacheKey, value); } // 从缓存中删除指定 key 数据 @Override public Object removeObject(Object key) { String cacheKey = String.valueOf(key.hashCode()); cacheKeys.remove(cacheKey); return CACHE_SERVICE.delete(cacheKey); } // 清空当前 Cache 实例中的所有缓存数据 @Override public void clear() { for (int i = 0; i < cacheKeys.size(); i++){ String cacheKey = cacheKeys.get(i); CACHE_SERVICE.delete(cacheKey); } cacheKeys.clear(); } @Override public int getSize() { return cacheKeys.size(); } }在 ProductMapper 中增加配置：<cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />启动 Memcached：memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v执行 Mapper 中的查询、修改等操作，Test：@Test public void testSelectById() { Long pid = 100L; Product dbProduct = productMapper.selectByID(pid); Assert.assertNotNull(dbProduct); Product cacheProduct = productMapper.selectByID(pid); Assert.assertNotNull(cacheProduct); productMapper.updateName("IPad", pid); Product product = productMapper.selectByID(pid); Assert.assertEquals(product.getName(), "IPad"); }Memcached Logging：看上去没什么问题~ OK 了。说明：本文基于 MyBatis 3.0.x 版本源码进行分析，部分类名（如 BaseExcutor 应为 BaseExecutor）及外部链接（如 grepcode、iteye）可能因版本迭代或服务关闭而失效，实际开发请以官方最新文档为准。