MyBatis 源码手记
一、SqlSession 与执行器 Executor
SqlSession 是 MyBatis 的门面接口,我们在业务代码里最常打交道的家伙。它不直接执行 SQL,而是委托给内部的 Executor 来干活。Executor 负责 SQL 的实际执行、事务控制和缓存管理。
看看 DefaultSqlSession 的实现:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
// ... 其他字段
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.autoCommit = autoCommit;
// ... 初始化
}
@Override
public <T> T selectOne(String statement, Object parameter) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
List<T> list = executor.query(ms, wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
}
}
// ... 其他方法如 update、insert 等类似
}
这里的关键是 executor.query()
,它会根据 MappedStatement(封装了 SQL 配置)来执行查询。SqlSession 还管理事务:commit()
会调用 executor.commit()
,rollback()
同理。如果 autoCommit
为 true,就不会自动提交事务——这在批量操作时特别有用,避免了频繁的 JDBC commit 开销。
Executor 类型详解
MyBatis 提供了几种 Executor 实现,每种针对不同场景优化:
SimpleExecutor
最基础的,每次执行 SQL 都会新建 Statement 对象。简单粗暴,适合单次查询,不用担心资源复用。但在循环执行时性能差,因为反复创建 Statement 会增加 JDBC 开销。源码里 doQuery()
方法每次都调用 statementHandler.prepare()
来创建新 Statement。
ReuseExecutor
聪明点,它会复用 PreparedStatement。对于相同的 SQL,它用一个 Map 来缓存 Statement 对象,避免重复 prepare。看代码:
private final Map<String, Statement> statementMap = new HashMap<>();
@Override
protected int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
// ... 获取 BoundSql
String sql = boundSql.getSql();
Statement stmt = statementMap.get(sql);
if (stmt == null) {
stmt = handler.prepare(connection, transaction.getTimeout());
statementMap.put(sql, stmt);
}
// ... 设置参数并执行
}
这在高频相同 SQL 的场景下(如循环插入相同结构的数据)能省不少时间。我在项目里用过,确实能感觉到性能提升,但要注意事务结束时要清空 statementMap
,否则内存泄漏。
BatchExecutor
专为批量操作设计。它会缓冲多个 SQL 操作,直到 flushStatements()
或 commit()
时一次性发送到数据库。内部用一个 List 来收集结果。适合大批量 insert/update,比如导入 Excel 数据时用这个能把性能从 O(n) 的 JDBC 调用降到接近 O(1)。
CachingExecutor
这是一个装饰器(Decorator),包裹其他 Executor 来添加二级缓存逻辑。如果配置了二级缓存,它会在 query 前先查缓存,miss 后再执行底层 Executor。
Executor 基类 BaseExecutor 还内置了一级缓存(PerpetualCache,默认 HashMap 实现),查询时用 queryStack 来防止循环查询(比如嵌套查询)。事务管理也在 BaseExecutor 里:localCache 在 commit/rollback 时清空。
感悟:MyBatis 的 Executor 体系体现了策略模式,不同执行策略无缝切换,业务代码零感知。
二、SqlSource 与动态 SQL
MyBatis 的 SQL 构建是其亮点之一,分静态和动态两种。静态 SQL 简单高效,动态 SQL 则通过树状结构处理复杂逻辑。
静态 SQL
直接的 SQL 字符串,比如:
String sql = "SELECT * FROM user WHERE id = ?";
StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, parameterMappings);
BoundSql boundSql = sqlSource.getBoundSql(parameterObject); // 直接返回固定 SQL 和参数
参数映射在解析 XML 时就固定了,运行时零开销。适合不变的查询。
动态 SQL
动态 SQL 用 SqlNode 树表示,DynamicSqlSource 在 getBoundSql()
时遍历树生成最终 SQL。SqlNode 是接口,有各种实现:
- TextSqlNode:纯文本 SQL 片段
- IfSqlNode:条件判断,内部持有一个 SqlNode 和表达式(如 “id != null”)
- ChooseSqlNode:类似 switch,包含 When 和 Otherwise
- ForEachSqlNode:处理集合迭代,生成 IN 子句或批量插入
- TrimSqlNode:去除多余的 AND/OR 前缀
- MixedSqlNode:容器,持有子节点列表
示例构建树:
List<SqlNode> contents = new ArrayList<>();
contents.add(new StaticTextSqlNode("SELECT * FROM user WHERE 1=1 "));
if (condition) {
contents.add(new IfSqlNode(new StaticTextSqlNode("AND id = #{id}"), "id != null"));
}
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(configuration, mixedSqlNode);
getBoundSql(parameterObject)
时,会用 DynamicContext 上下文递归 apply()
每个节点,收集 SQL 和 ParameterMapping。BoundSql 最终封装 SQL、参数列表和映射。
动态 SQL 的灵活性来自于 OGNL 的表达式求值(详见下节),但性能稍差,因为每次执行都要解析树。优化点:如果参数固定,可以预编译,但 MyBatis 默认运行时解析。
个人吐槽:第一次看动态 SQL 源码时,被 SqlNode 树的递归搞晕了,但用习惯后觉得这设计太优雅了——XML 配置直接转成树,运行时动态组装,避免了字符串拼接的 SQL 注入风险。
三、ParameterMapping 与 OGNL
参数处理是 MyBatis 安全的核心。SQL 中的 #{}
被解析成 ParameterMapping 对象,包含属性名、Java 类型、JDBC 类型等。
ParameterMapping mapping = ParameterMapping.builder(configuration, "id", Integer.class)
.javaType(Integer.class)
.jdbcType(JdbcType.INTEGER)
.build();
在 StatementHandler.setParameters()
时,用 TypeHandler 设置参数:
TypeHandler<Object> typeHandler = parameterMapping.getTypeHandler();
typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
值从 parameterObject 取,用 OGNL:
OGNL (Object Graph Navigation Language)
OGNL 是 MyBatis 的表达式引擎,OgnlCache.getValue(expression, parameterObject)
可以解析 “user.address.street” 这样的嵌套属性。内部用 OgnlRuntime 缓存解析结果,避免重复解析。OGNL 支持方法调用、集合访问等,比反射灵活。
TypeHandler 负责类型转换,TypeHandlerRegistry 注册了常见类型(如 StringTypeHandler、IntegerTypeHandler),自定义类型可扩展。参数映射确保了 preparedStatement 的安全,防 SQL 注入。
感悟:OGNL 的强大让我在业务中也用类似表达式引擎来处理配置,超级方便。
四、ResultMap 与结果映射
ResultMap 是结果集到对象的桥梁。XML 转成 ResultMap 对象,包含 ResultMapping 列表(列名到属性名的映射)。
在 ResultSetHandler.handleResultSets()
时,用 DefaultResultSetHandler 处理:
while (rs.next()) {
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
// ... 通过 MetaObject 设置属性
metaObject.setValue(resultMapping.getProperty(), value);
}
支持:
- 自动映射:列名匹配属性名(下划线转驼峰)
- 嵌套结果:association/collection 处理一对一/一对多
- 构造函数注入
- 鉴别器(discriminator):基于列值选择映射
- 自定义 TypeHandler
ResultMap 解耦了 SQL 和 POJO,改表结构只需调映射,不动代码。懒加载(lazyLoadingEnabled)在访问时再查嵌套对象。
吐槽:复杂嵌套映射时,调试 ResultSetWrapper 的列类型匹配真是个坑,但一旦搞懂,处理复杂查询如鱼得水。
五、缓存机制
MyBatis 缓存分两级,设计精妙。
一级缓存
SqlSession 级,默认开启。在 BaseExecutor.query()
中,先查 localCache(PerpetualCache,HashMap)。key 是 [MappedStatement id + offset + limit + SQL + params]。命中直接返回,miss 执行 SQL 后 put。update/commit 清空。防止脏读,在事务内有效。
二级缓存
Namespace 级(Mapper.xml),需配置 <cache/>
或 cacheEnabled=true
。CachingExecutor 装饰底层 Executor,先查二级缓存(可配置如 FifoCache、LruCache),miss 后执行并 put。支持序列化(Serializable),集群时可集成 Redis 等。flushCache/selective 配置控制刷新。
缓存 key 生成用 CacheKey,包含 hashCode、multiplier 等,确保唯一。事务提交时才同步到二级缓存。
优化:读写锁(SynchronizedCache)防止并发问题。
个人经验:在高并发读场景,用二级缓存能把数据库压力降 80%,但要注意失效策略。
缓存装饰器模式实现
public class Cache {
// 基础缓存实现
PerpetualCache delegate;
// 可选的装饰器链
LruCache lru;
FifoCache fifo;
SynchronizedCache sync;
// 装饰器包装
cache = new SynchronizedCache(new LruCache(new PerpetualCache(id)));
}
六、插件与拦截器
插件用责任链模式扩展核心组件。Interceptor 接口:
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置逻辑
Object result = invocation.proceed(); // 调用下一个
// 后置逻辑
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this); // 动态代理
}
}
Plugin.wrap()
用 JDK 动态代理包装目标(如 Executor)。多个插件链式执行。
常见用例
- 分页插件:拦截 query,修改 SQL 添加 LIMIT
- 分库分表插件:修改 SQL,路由到不同库表
- 性能监控插件:记录执行时间,慢查询报警
- 数据脱敏插件:结果集处理,敏感数据打码
启发:这让我在设计业务框架时也用拦截器来加日志、权限检查,核心代码干净。
七、事务管理
事务在 Transaction 接口,JdbcTransaction 实现 JDBC commit/rollback。Executor 持 Transaction:
public void commit(boolean required) throws SQLException {
if (transaction != null) {
transaction.commit();
}
}
SqlSessionFactory.openSession(autoCommit)
决定是否自动提交。BatchExecutor 在 flush 时批量 commit。支持 Spring 集成,通过 TransactionManager。
底层:getConnection()
从 DataSource 取连接,setAutoCommit(false)
开启手动事务。回滚时 close 连接释放资源。
事务隔离级别
public enum TransactionIsolationLevel {
NONE(Connection.TRANSACTION_NONE),
READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);
}
坑点:嵌套事务不支持,需小心多 SqlSession 场景。
八、反射与工具类
MyBatis 重度依赖反射简化对象操作。
核心反射组件
- Reflector/MetaClass:Reflector 缓存类元数据(getter/setter),MetaClass 找属性路径
- MetaObject:统一读写对象,支持嵌套:“user.address.street”。内部用 ObjectWrapper(BeanWrapper for POJO, MapWrapper for Map, CollectionWrapper for List)
- Invoker:抽象 get/set,如 MethodInvoker、GetFieldInvoker
// 使用示例
MetaObject metaObject = SystemMetaObject.forObject(user);
metaObject.setValue("name", "grok");
String name = (String) metaObject.getValue("address.street");
PropertyTokenizer
解析属性表达式 “user.address[0].street”:
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name; // user
private String indexedName; // address[0]
private String index; // 0
private String children; // street
}
这些工具让参数/结果映射通用化,避免硬编码反射调用。
启发:业务中用类似 MetaObject 处理动态表单,省时省力。
九、类型处理
TypeHandlerRegistry
管理类型转换:
registry.register(Integer.class, new IntegerTypeHandler());
registry.register(String.class, new StringTypeHandler());
// 自定义枚举处理
registry.register(MyEnum.class, new EnumTypeHandler<>(MyEnum.class));
getTypeHandler()
根据 JavaType/JdbcType 找。内置 40+ handler,支持自定义如 EnumTypeHandler。JDBC null 处理用 NullTypeHandler。
常见 TypeHandler
// 基础类型
IntegerTypeHandler, LongTypeHandler, StringTypeHandler
// 日期时间
DateTypeHandler, LocalDateTimeTypeHandler, InstantTypeHandler
// 大对象
BlobTypeHandler, ClobTypeHandler
// 数组
ArrayTypeHandler
自定义 TypeHandler 示例
public class JsonTypeHandler implements TypeHandler<Object> {
@Override
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) {
ps.setString(i, JSON.toJSONString(parameter));
}
@Override
public Object getResult(ResultSet rs, String columnName) {
return JSON.parseObject(rs.getString(columnName));
}
}
类型别名 TypeAliasRegistry 简化配置,如 “int” alias Integer。
十、日志与调试
日志抽象
日志用 Log 接口,适配多种框架(SLF4J、Log4J 等)。配置 logImpl=SLF4J
。
public interface Log {
boolean isDebugEnabled();
void debug(String s);
void debug(String s, Throwable e);
// ... 其他级别
}
具体实现
- Slf4jImpl
- Log4jImpl
- Log4j2Impl
- JdkLoggingImpl
- CommonsLoggingImpl
- StdOutImpl(标准输出)
- NoLoggingImpl(无日志)
SQL 日志
StatementLog 在 prepare/execute 前后 log SQL 和 params。调试时,开启 trace 级别看绑定参数:
2023-01-01 10:00:00.001 [main] DEBUG c.example.mapper.UserMapper.selectById - ==> Preparing: SELECT * FROM user WHERE id = ?
2023-01-01 10:00:00.002 [main] DEBUG c.example.mapper.UserMapper.selectById - ==> Parameters: 1(Integer)
2023-01-01 10:00:00.005 [main] DEBUG c.example.mapper.UserMapper.selectById - <== Total: 1
感悟:日志设计早,帮我排查过无数 SQL 问题。框架日志抽象让我学到:日志别硬编码,适配器模式永不过时。
十一、SQL 重用与优化
性能优化策略
- ReuseExecutor 缓存 Statement,减少 prepare 调用
- BatchExecutor 批量
addBatch()
,executeBatch()
一次性发 - 预编译 SQL 用
#{}
,参数复用安全 - 一级缓存 避重复查询,二级缓存跨 Session
- RowBounds 内存分页(不推荐大结果集,用物理分页)
- 懒加载 延迟嵌套查询
SQL 执行流程优化
// 1. SQL 解析缓存
MappedStatement ms = configuration.getMappedStatement(statement);
// 2. 参数处理优化
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey cacheKey = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 缓存命中检查
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(cacheKey) : null;
if (list != null) {
// 缓存命中,直接返回
return list;
}
// 4. 数据库查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
localCache.putObject(cacheKey, list);
优化建议
- 监控 slow SQL:用插件加执行时间 log
- 高负载调优:调 cache size,避免 OOM
- 连接池配置:合理设置 maxActive、maxIdle
- 批量操作:使用 BatchExecutor,避免 N+1 查询
- 结果集优化:只查需要的字段,避免 SELECT *
十二、配置与初始化
Configuration 核心配置
Configuration 是 MyBatis 的配置中心,包含所有配置信息:
public class Configuration {
// 核心组件
protected Environment environment;
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 final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
// 映射语句
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>();
protected final Map<String, Cache> caches = new StrictMap<>();
protected final Map<String, ResultMap> resultMaps = new StrictMap<>();
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>();
}
XML 解析流程
// 1. 解析 mybatis-config.xml
XMLConfigBuilder configBuilder = new XMLConfigBuilder(inputStream);
Configuration config = configBuilder.parse();
// 2. 解析 Mapper XML
XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(inputStream, config, resource);
mapperBuilder.parse();
// 3. 构建 SqlSessionFactory
SqlSessionFactory factory = new DefaultSqlSessionFactoryBuilder().build(config);
注解解析
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "user_name")
})
User selectById(@Param("id") Integer id);
MapperAnnotationBuilder 解析注解,转换成 MappedStatement。
十三、对日常开发的启发
设计模式应用
- 接口优先,解耦模块:SqlSession 接口隐藏实现,换 Executor 零成本。业务中多用接口,易测试/扩展
- 关注点分离:SQL 构建、参数、结果、事务、缓存各模块独立。避免 monolithic 代码
- 可插拔设计:插件让扩展 non-invasive。业务用 AOP 类似
- 装饰器模式:CachingExecutor、各种 Cache 装饰器
- 策略模式:多种 Executor 实现,运行时选择
- 模板方法:BaseExecutor 定义执行模板,子类实现具体逻辑
架构思想
- 动态静态平衡:静态高性能,动态灵活。项目中混用
- 抽象复杂性:反射/OGNL/TypeHandler 藏细节。业务抽象工具类
- 透明执行:业务无感知 JDBC,专注逻辑
- 配置驱动:XML/注解配置,代码零侵入
- 缓存分层:一级/二级缓存,不同粒度不同策略
实战经验总结
// 1. 善用批量操作
SqlSession batchSession = factory.openSession(ExecutorType.BATCH);
for (User user : users) {
batchSession.insert("insertUser", user);
}
batchSession.commit();
// 2. 合理使用缓存
@CacheNamespace(
size = 512,
flushInterval = 60000,
eviction = LRU.class,
readWrite = false
)
// 3. 自定义类型处理
@MappedTypes({MyEnum.class})
@MappedJdbcTypes({JdbcType.VARCHAR})
public class MyEnumTypeHandler implements TypeHandler<MyEnum> {
// 实现类型转换逻辑
}
// 4. 插件扩展
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare"))
public class SqlPrintPlugin implements Interceptor {
// 打印执行的 SQL
}
十四、进阶主题
MyBatis-Spring 集成
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory factory) {
return new SqlSessionTemplate(factory);
}
}
分页插件实现原理
@Intercepts(@Signature(type = Executor.class, method = "query"))
public class PageInterceptor implements Interceptor {
@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];
if (rowBounds != RowBounds.DEFAULT) {
// 构造 COUNT SQL
String countSql = "SELECT COUNT(*) FROM (" + originalSql + ") tmp_count";
// 构造分页 SQL
String pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + "," + rowBounds.getLimit();
// 先查总数,再查分页数据
// ...
}
return invocation.proceed();
}
}