引言
MyBatis是Java生态中使用最广泛的持久层框架之一。与Hibernate等全自动ORM不同,MyBatis采用半自动映射策略,开发者通过XML或注解编写SQL,框架负责参数映射和结果集转换。这种设计在保持SQL可控性的同时,显著降低了数据访问层的开发复杂度。
理解MyBatis的源码不仅有助于排查日常开发中的疑难问题,更能帮助我们掌握其强大的插件扩展机制。本文将从整体架构出发,逐层深入分析核心组件的实现原理,并通过实战演示如何开发自定义插件。
MyBatis整体架构
graph TB
subgraph 接口层
SqlSession[SqlSession 接口]
end
subgraph 核心处理层
Configuration[Configuration 全局配置]
Executor[Executor 执行器]
StatementHandler[StatementHandler 语句处理器]
ParameterHandler[ParameterHandler 参数处理器]
ResultSetHandler[ResultSetHandler 结果集处理器]
TypeHandler[TypeHandler 类型处理器]
end
subgraph 基础支撑层
DataSource[DataSource 数据源]
Transaction[Transaction 事务管理]
Cache[Cache 缓存]
Logging[Logging 日志]
Reflection[Reflection 反射工具]
end
SqlSession --> Executor
Executor --> StatementHandler
StatementHandler --> ParameterHandler
StatementHandler --> ResultSetHandler
ParameterHandler --> TypeHandler
ResultSetHandler --> TypeHandler
Executor --> Cache
Executor --> Transaction
Transaction --> DataSource
style SqlSession fill:#e3f2fd
style Executor fill:#fff3e0
style Configuration fill:#e8f5e9
SQL执行全流程
以一个简单的查询操作为例,追踪从mapper.selectById(1L)到返回结果的完整执行链路:
sequenceDiagram
participant Client as 调用方
participant Proxy as MapperProxy
participant Session as SqlSession
participant Exec as Executor
participant SH as StatementHandler
participant PH as ParameterHandler
participant RSH as ResultSetHandler
participant DB as 数据库
Client->>Proxy: mapper.selectById(1L)
Proxy->>Session: sqlSession.selectOne(statementId, param)
Session->>Exec: executor.query(ms, param, rowBounds, resultHandler)
Exec->>Exec: 查询一级缓存
alt 缓存命中
Exec-->>Session: 返回缓存结果
else 缓存未命中
Exec->>SH: handler.query(statement, resultHandler)
SH->>PH: parameterHandler.setParameters(ps)
PH->>DB: PreparedStatement.execute()
DB-->>RSH: ResultSet
RSH->>RSH: 结果集映射为Java对象
RSH-->>Exec: List<Object>
Exec->>Exec: 结果存入一级缓存
end
Exec-->>Session: 返回结果
Session-->>Proxy: 返回结果
Proxy-->>Client: 返回User对象
SqlSession生命周期
SqlSession是MyBatis工作的核心接口,每个数据库操作都始于SqlSession的创建,终于其关闭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class MyBatisConfig { public SqlSessionFactory createSqlSessionFactory () throws IOException { String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); return new SqlSessionFactoryBuilder ().build(inputStream); } }public class SqlSessionLifecycle { private final SqlSessionFactory sqlSessionFactory; public SqlSessionLifecycle (SqlSessionFactory sqlSessionFactory) { this .sqlSessionFactory = sqlSessionFactory; } public User getUserById (Long id) { try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); return mapper.selectById(id); } } public void batchInsert (List<User> users) { try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper = session.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); } session.commit(); } } }
MapperProxy源码解析
当我们调用session.getMapper(UserMapper.class)时,MyBatis返回的是一个基于JDK动态代理生成的代理对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 public class MapperProxy <T> implements InvocationHandler { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache; @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , args); } return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } private MapperMethodInvoker cachedInvoker (Method method) { return methodCache.computeIfAbsent(method, m -> { if (m.isDefault()) { return new DefaultMethodInvoker (m); } else { return new PlainMethodInvoker ( new MapperMethod (mapperInterface, m, sqlSession.getConfiguration())); } }); } }public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public Object execute (SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT -> { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } case UPDATE -> { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } case DELETE -> { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } case SELECT -> { if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null ; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } default -> throw new BindingException ("Unknown execution method for: " + command.getName()); } return result; } }
Executor执行器类型
graph TD
Executor[Executor 接口] --> BaseExecutor[BaseExecutor 抽象基类]
BaseExecutor --> SimpleExecutor[SimpleExecutor<br/>每次创建新的Statement]
BaseExecutor --> ReuseExecutor[ReuseExecutor<br/>复用Statement对象]
BaseExecutor --> BatchExecutor[BatchExecutor<br/>批量执行,攒SQL一次发送]
Executor --> CachingExecutor[CachingExecutor<br/>二级缓存装饰器]
CachingExecutor --> BaseExecutor
style Executor fill:#e3f2fd
style CachingExecutor fill:#fff9c4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class SimpleExecutor extends BaseExecutor { @Override public <E> List<E> doQuery (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null ; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler( wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } private Statement prepareStatement (StatementHandler handler, Log statementLog) throws SQLException { Connection connection = getConnection(statementLog); Statement stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; } }
一级缓存与二级缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public abstract class BaseExecutor implements Executor { protected PerpetualCache localCache; @Override public <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); } @Override public <E> List<E> query (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list = (List<E>) localCache.getObject(key); if (list != null ) { return list; } list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); return list; } private <E> List<E> queryFromDatabase (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); localCache.putObject(key, list); return list; } }
TypeHandler类型处理器
TypeHandler负责Java类型与JDBC类型之间的转换。MyBatis内置了大量TypeHandler,也支持自定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @MappedTypes({Map.class, List.class}) @MappedJdbcTypes(JdbcType.VARCHAR) public class JsonTypeHandler <T> extends BaseTypeHandler <T> { private static final ObjectMapper MAPPER = new ObjectMapper (); private final Class<T> type; public JsonTypeHandler (Class<T> type) { this .type = type; } @Override public void setNonNullParameter (PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { try { ps.setString(i, MAPPER.writeValueAsString(parameter)); } catch (JsonProcessingException e) { throw new SQLException ("JSON序列化失败" , e); } } @Override public T getNullableResult (ResultSet rs, String columnName) throws SQLException { return parseJson(rs.getString(columnName)); } @Override public T getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return parseJson(rs.getString(columnIndex)); } @Override public T getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return parseJson(cs.getString(columnIndex)); } private T parseJson (String json) throws SQLException { if (json == null || json.isEmpty()) { return null ; } try { return MAPPER.readValue(json, type); } catch (JsonProcessingException e) { throw new SQLException ("JSON反序列化失败: " + json, e); } } }
在Mapper XML中使用:
1 2 3 4 5 6 <resultMap id ="userResultMap" type ="com.example.entity.User" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> <result column ="extra_info" property ="extraInfo" typeHandler ="com.example.handler.JsonTypeHandler" /> </resultMap >
动态SQL实现原理
MyBatis的动态SQL是通过SqlNode树来实现的。每个动态标签(<if>、<choose>、<foreach>等)对应一个SqlNode实现:
graph TD
MixedSqlNode[MixedSqlNode<br/>根节点, 包含子节点列表] --> StaticText[StaticTextSqlNode<br/>静态SQL文本]
MixedSqlNode --> IfNode[IfSqlNode<br/>条件判断]
MixedSqlNode --> ForEach[ForEachSqlNode<br/>循环遍历]
MixedSqlNode --> Choose[ChooseSqlNode<br/>多条件分支]
MixedSqlNode --> Where[WhereSqlNode<br/>自动处理WHERE前缀]
MixedSqlNode --> Set[SetSqlNode<br/>自动处理SET前缀]
MixedSqlNode --> Trim[TrimSqlNode<br/>自定义前后缀处理]
IfNode --> OGNL[OGNL表达式求值]
ForEach --> Collection[遍历集合参数]
style MixedSqlNode fill:#e3f2fd
style OGNL fill:#fff9c4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <select id ="searchUsers" resultMap ="userResultMap" > SELECT * FROM user <where > <if test ="name != null and name != ''" > AND name LIKE CONCAT('%', #{name}, '%') </if > <if test ="minAge != null" > AND age > = #{minAge} </if > <if test ="departmentIds != null and departmentIds.size() > 0" > AND department_id IN <foreach collection ="departmentIds" item ="deptId" open ="(" separator ="," close =")" > #{deptId} </foreach > </if > <choose > <when test ="status == 'ACTIVE'" > AND status = 1 </when > <when test ="status == 'INACTIVE'" > AND status = 0 </when > <otherwise > AND status >= 0 </otherwise > </choose > </where > ORDER BY <choose > <when test ="orderBy == 'name'" > name</when > <when test ="orderBy == 'age'" > age</when > <otherwise > id</otherwise > </choose > <if test ="desc" > DESC</if > </select >
插件(拦截器)机制
MyBatis的插件机制基于JDK动态代理,可以拦截四大核心对象的方法调用:
Executor :拦截执行器方法(update、query、commit、rollback等)
StatementHandler :拦截SQL语句处理(prepare、parameterize、query、update等)
ParameterHandler :拦截参数设置(setParameters)
ResultSetHandler :拦截结果集处理(handleResultSets)
graph LR
subgraph 插件拦截器链
P1[插件1 代理] --> P2[插件2 代理]
P2 --> P3[插件3 代理]
P3 --> Target[目标对象]
end
Call[方法调用] --> P1
Target --> Result[执行结果]
style P1 fill:#e3f2fd
style P2 fill:#e3f2fd
style P3 fill:#e3f2fd
style Target fill:#e8f5e9
插件的创建过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Configuration { protected final InterceptorChain interceptorChain = new InterceptorChain (); public StatementHandler newStatementHandler (Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler handler = new RoutingStatementHandler ( executor, ms, parameter, rowBounds, resultHandler, boundSql); handler = (StatementHandler) interceptorChain.pluginAll(handler); return handler; } }public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList <>(); public Object pluginAll (Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } }
实战:分页插件开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 @Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class PaginationPlugin implements Interceptor { private static final Logger log = LoggerFactory.getLogger(PaginationPlugin.class); @Override public Object intercept (Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0 ]; Object parameter = args[1 ]; RowBounds rowBounds = (RowBounds) args[2 ]; PageParam pageParam = extractPageParam(parameter); if (pageParam == null ) { return invocation.proceed(); } BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); String countSql = "SELECT COUNT(*) FROM (" + originalSql + ") _count_table" ; Connection connection = ((Executor) invocation.getTarget()) .getTransaction().getConnection(); long totalCount; try (PreparedStatement countStmt = connection.prepareStatement(countSql)) { DefaultParameterHandler paramHandler = new DefaultParameterHandler ( ms, parameter, boundSql); paramHandler.setParameters(countStmt); try (ResultSet rs = countStmt.executeQuery()) { totalCount = rs.next() ? rs.getLong(1 ) : 0 ; } } pageParam.setTotal(totalCount); if (totalCount == 0 ) { return new ArrayList <>(); } String pageSql = originalSql + " LIMIT " + pageParam.getOffset() + ", " + pageParam.getPageSize(); Field sqlField = BoundSql.class.getDeclaredField("sql" ); sqlField.setAccessible(true ); sqlField.set(boundSql, pageSql); log.debug("分页SQL: {}" , pageSql); return invocation.proceed(); } private PageParam extractPageParam (Object parameter) { if (parameter instanceof PageParam) { return (PageParam) parameter; } if (parameter instanceof Map<?, ?> map) { for (Object value : map.values()) { if (value instanceof PageParam) { return (PageParam) value; } } } return null ; } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { } }
实战:SQL执行耗时监控插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}) }) public class SqlTimingPlugin implements Interceptor { private static final Logger log = LoggerFactory.getLogger(SqlTimingPlugin.class); private long slowSqlThresholdMs = 1000 ; @Override public Object intercept (Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql().replaceAll("\\s+" , " " ).trim(); long start = System.currentTimeMillis(); try { Object result = invocation.proceed(); long elapsed = System.currentTimeMillis() - start; if (elapsed >= slowSqlThresholdMs) { log.warn("[慢SQL] 耗时 {}ms | SQL: {} | 参数: {}" , elapsed, sql, getParameterValues(boundSql)); } else { log.debug("[SQL] 耗时 {}ms | SQL: {}" , elapsed, sql); } return result; } catch (Exception e) { long elapsed = System.currentTimeMillis() - start; log.error("[SQL异常] 耗时 {}ms | SQL: {} | 异常: {}" , elapsed, sql, e.getMessage()); throw e; } } private String getParameterValues (BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); if (parameterObject == null ) { return "null" ; } List<ParameterMapping> mappings = boundSql.getParameterMappings(); if (mappings == null || mappings.isEmpty()) { return parameterObject.toString(); } StringBuilder sb = new StringBuilder ("[" ); MetaObject metaObject = SystemMetaObject.forObject(parameterObject); for (int i = 0 ; i < mappings.size(); i++) { String property = mappings.get(i).getProperty(); if (metaObject.hasGetter(property)) { sb.append(property).append("=" ).append(metaObject.getValue(property)); } else if (boundSql.hasAdditionalParameter(property)) { sb.append(property).append("=" ) .append(boundSql.getAdditionalParameter(property)); } if (i < mappings.size() - 1 ) sb.append(", " ); } sb.append("]" ); return sb.toString(); } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { String threshold = properties.getProperty("slowSqlThresholdMs" ); if (threshold != null ) { this .slowSqlThresholdMs = Long.parseLong(threshold); } } }
插件注册
1 2 3 4 5 6 7 8 <plugins > <plugin interceptor ="com.example.plugin.SqlTimingPlugin" > <property name ="slowSqlThresholdMs" value ="500" /> </plugin > <plugin interceptor ="com.example.plugin.PaginationPlugin" /> </plugins >
Spring Boot中的注册方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class MyBatisPluginConfig { @Bean public SqlTimingPlugin sqlTimingPlugin () { SqlTimingPlugin plugin = new SqlTimingPlugin (); Properties props = new Properties (); props.setProperty("slowSqlThresholdMs" , "500" ); plugin.setProperties(props); return plugin; } @Bean public PaginationPlugin paginationPlugin () { return new PaginationPlugin (); } }
性能考量与最佳实践
缓存使用建议
一级缓存
SqlSession
开启
注意多次查询间的数据变更,必要时手动clearCache
二级缓存
Mapper namespace
关闭
仅适用于读多写少的场景,分布式环境慎用
批量操作优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class BatchInsertComparison { public void insertOneByOne (List<User> users) { try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); } session.commit(); } } public void insertBatch (List<User> users) { try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) { UserMapper mapper = session.getMapper(UserMapper.class); int batchSize = 500 ; for (int i = 0 ; i < users.size(); i++) { mapper.insert(users.get(i)); if ((i + 1 ) % batchSize == 0 ) { session.flushStatements(); } } session.commit(); } } public void insertForeach (List<User> users) { try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); List<List<User>> partitions = Lists.partition(users, 500 ); for (List<User> batch : partitions) { mapper.batchInsert(batch); } session.commit(); } } }
N+1查询问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <resultMap id ="userWithOrders" type ="User" > <id column ="id" property ="id" /> <collection property ="orders" column ="id" select ="com.example.mapper.OrderMapper.selectByUserId" /> </resultMap > <resultMap id ="userWithOrdersJoin" type ="User" > <id column ="user_id" property ="id" /> <result column ="user_name" property ="name" /> <collection property ="orders" ofType ="Order" > <id column ="order_id" property ="id" /> <result column ="order_no" property ="orderNo" /> </collection > </resultMap > <select id ="selectUsersWithOrders" resultMap ="userWithOrdersJoin" > SELECT u.id AS user_id, u.name AS user_name, o.id AS order_id, o.order_no FROM user u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = 1</select > <resultMap id ="userWithOrdersLazy" type ="User" > <id column ="id" property ="id" /> <collection property ="orders" column ="id" fetchType ="lazy" select ="com.example.mapper.OrderMapper.selectByUserId" /> </resultMap >
总结
通过对MyBatis核心源码的分析,我们可以更清晰地理解其工作原理:
MapperProxy 通过JDK动态代理将Mapper接口方法调用转换为SqlSession操作,实现了面向接口编程的优雅体验。
Executor 是SQL执行的核心引擎,SimpleExecutor每次新建Statement,ReuseExecutor复用Statement,BatchExecutor攒SQL批量发送。CachingExecutor作为装饰器提供二级缓存能力。
四大处理器 (StatementHandler、ParameterHandler、ResultSetHandler、TypeHandler)各司其职,分别负责SQL预编译、参数设置、结果映射和类型转换。
插件机制 基于JDK动态代理和责任链模式,可以拦截四大核心对象的方法调用。在开发自定义插件时,需注意拦截器链的执行顺序(后注册先执行)和性能影响。
性能优化 方面,批量操作优先使用BATCH模式,关注N+1查询问题,合理使用缓存,避免在动态SQL中拼接过长的SQL语句。
理解这些源码级的实现细节,不仅有助于日常开发中的问题排查,更能帮助我们在需要时开发高质量的MyBatis插件来满足特定的业务需求。