目录
[🎯 先说说我遇到过的真实问题](#🎯 先说说我遇到过的真实问题)
[✨ 摘要](#✨ 摘要)
[1. MyBatis架构总览:不只是"写SQL的工具"](#1. MyBatis架构总览:不只是"写SQL的工具")
[1.1 别把MyBatis看简单了](#1.1 别把MyBatis看简单了)
[1.2 核心组件职责解析](#1.2 核心组件职责解析)
[2. SqlSession:MyBatis的"大门"](#2. SqlSession:MyBatis的"大门")
[2.1 SqlSession的创建过程](#2.1 SqlSession的创建过程)
[2.2 执行器类型:选对模式很重要](#2.2 执行器类型:选对模式很重要)
[3. Mapper接口绑定:动态代理的魔法](#3. Mapper接口绑定:动态代理的魔法)
[3.1 从接口到SQL:到底发生了什么?](#3.1 从接口到SQL:到底发生了什么?)
[3.2 MapperMethod:方法执行的指挥官](#3.2 MapperMethod:方法执行的指挥官)
[4. 配置文件加载:MyBatis的"启动过程"](#4. 配置文件加载:MyBatis的"启动过程")
[4.1 配置文件的完整加载流程](#4.1 配置文件的完整加载流程)
[4.2 Configuration:MyBatis的"大脑"](#4.2 Configuration:MyBatis的"大脑")
[4.3 Mapper注册过程](#4.3 Mapper注册过程)
[5. SQL执行过程:从Java方法到数据库](#5. SQL执行过程:从Java方法到数据库)
[5.1 完整的SQL执行链路](#5.1 完整的SQL执行链路)
[5.2 Executor:SQL执行的"指挥官"](#5.2 Executor:SQL执行的"指挥官")
[5.3 StatementHandler:真正的SQL执行者](#5.3 StatementHandler:真正的SQL执行者)
[6. 缓存机制:性能提升的关键](#6. 缓存机制:性能提升的关键)
[6.1 一级缓存 vs 二级缓存](#6.1 一级缓存 vs 二级缓存)
[6.2 一级缓存的坑](#6.2 一级缓存的坑)
[6.3 二级缓存的正确使用](#6.3 二级缓存的正确使用)
[7. 插件机制:MyBatis的"钩子"](#7. 插件机制:MyBatis的"钩子")
[7.1 插件原理:责任链模式](#7.1 插件原理:责任链模式)
[7.2 插件的执行顺序](#7.2 插件的执行顺序)
[8. 类型处理器:Java与数据库的桥梁](#8. 类型处理器:Java与数据库的桥梁)
[8.1 内置类型处理器](#8.1 内置类型处理器)
[8.2 自定义类型处理器](#8.2 自定义类型处理器)
[9. 实战:性能优化与问题排查](#9. 实战:性能优化与问题排查)
[9.1 性能优化技巧](#9.1 性能优化技巧)
[9.2 常见问题排查](#9.2 常见问题排查)
[10. 企业级最佳实践](#10. 企业级最佳实践)
[10.1 我的"MyBatis军规"](#10.1 我的"MyBatis军规")
[📜 第一条:明确SQL写在XML还是注解](#📜 第一条:明确SQL写在XML还是注解)
[📜 第二条:合理使用缓存](#📜 第二条:合理使用缓存)
[📜 第三条:批量操作要用批量模式](#📜 第三条:批量操作要用批量模式)
[📜 第四条:监控SQL性能](#📜 第四条:监控SQL性能)
[📜 第五条:代码可读性](#📜 第五条:代码可读性)
[10.2 生产环境配置模板](#10.2 生产环境配置模板)
[11. 最后的话](#11. 最后的话)
[📚 推荐阅读](#📚 推荐阅读)

🎯 先说说我遇到过的真实问题
去年我们团队接手一个老系统,用的是MyBatis,但没一个人懂原理。有天线上突然报错:"Too many connections",数据库连接池爆了。我们查了三天,最后发现是某个Mapper方法里写了复杂的动态SQL,每次执行都创建新SqlSession,但没人close。
更绝的是,有次性能优化,我们发现某个查询方法被调用了1000次,但实际只需要查一次数据库。后来一查,是MyBatis一级缓存在作怪,但团队里没人知道MyBatis还有缓存。
这些事让我明白:不懂MyBatis源码的程序员,就像开自动挡车不知道变速箱原理,早晚要出事。
✨ 摘要
MyBatis作为Java生态中最流行的ORM框架,其核心在于SqlSession的动态管理和Mapper接口的绑定机制。本文深度解析MyBatis从配置文件加载、SqlSession创建、Mapper代理生成到SQL执行的完整链路。通过源码级分析,揭示动态代理、反射、JDBC封装等底层技术如何协同工作。结合性能测试数据和实战案例,提供MyBatis优化策略和故障排查指南。
1. MyBatis架构总览:不只是"写SQL的工具"
1.1 别把MyBatis看简单了
很多人觉得MyBatis就是"写SQL的框架",比Hibernate简单多了。这话对了一半,MyBatis确实让SQL更直观,但它的内部设计绝不简单。
看看这个调用链路:

图1:MyBatis完整执行链路
看到没?从你的Mapper方法调用到真正执行SQL,中间隔了至少7层!每一层都有它的职责。
1.2 核心组件职责解析
| 组件 | 英文 | 职责 | 类比 |
|---|---|---|---|
| SqlSession | SQL会话 | 对外API入口 | 餐厅服务员 |
| Executor | 执行器 | SQL执行调度 | 厨师长 |
| StatementHandler | 语句处理器 | 操作Statement | 厨师 |
| ParameterHandler | 参数处理器 | 设置参数 | 配菜员 |
| ResultSetHandler | 结果集处理器 | 处理结果 | 摆盘员 |
| MapperProxy | Mapper代理 | 接口方法代理 | 点餐员 |
2. SqlSession:MyBatis的"大门"
2.1 SqlSession的创建过程
很多人以为SqlSession就是个简单的数据库连接包装,大错特错!看看它的创建过程:
java
// 这是SqlSessionFactory创建SqlSession的核心方法
public class DefaultSqlSessionFactory implements SqlSessionFactory {
@Override
public SqlSession openSession() {
return openSessionFromDataSource(
configuration.getDefaultExecutorType(), // 执行器类型
null, // 事务隔离级别
false // 是否自动提交
);
}
private SqlSession openSessionFromDataSource(ExecutorType execType,
TransactionIsolationLevel level,
boolean autoCommit) {
Transaction tx = null;
try {
// 1. 获取环境配置
final Environment environment = configuration.getEnvironment();
// 2. 创建事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 3. 创建事务
tx = transactionFactory.newTransaction(
environment.getDataSource(), level, autoCommit
);
// 4. 创建执行器(关键!)
final Executor executor = configuration.newExecutor(tx, execType);
// 5. 创建SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 关闭事务
closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. ", e);
}
}
}
代码清单1:SqlSession创建源码(MyBatis 3.5+)
关键点:SqlSession不是简单的Connection包装,它包含:
-
配置信息(Configuration)
-
执行器(Executor)
-
事务控制
2.2 执行器类型:选对模式很重要
MyBatis有三种执行器,性能差异很大:
java
public enum ExecutorType {
SIMPLE, // 简单执行器
REUSE, // 重用执行器
BATCH // 批量执行器
}
性能测试数据(插入1000条记录):
| 执行器类型 | 耗时(ms) | 内存占用 | 适用场景 |
|---|---|---|---|
| SIMPLE | 1250 | 45MB | 通用查询 |
| REUSE | 980 | 42MB | 频繁相同SQL |
| BATCH | 350 | 50MB | 批量插入 |
测试环境:MySQL 8.0,1000条记录插入
代码示例:
java
// 使用批量执行器
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < 1000; i++) {
User user = new User("user" + i, "email" + i + "@test.com");
mapper.insert(user);
}
session.commit(); // 一次性提交
}
3. Mapper接口绑定:动态代理的魔法
3.1 从接口到SQL:到底发生了什么?
这是MyBatis最神奇的地方。你写个接口,MyBatis就能执行SQL:
java
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
}
// 使用
User user = userMapper.findById(1L); // 这里发生了什么?
答案:动态代理 + 方法签名解析。看源码:
java
// MapperProxy是核心
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 如果是Object的方法,直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 默认方法(Java 8+)
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 3. 缓存中获取MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 4. 执行SQL
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k ->
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
);
}
}
代码清单2:MapperProxy核心源码
3.2 MapperMethod:方法执行的指挥官
MapperMethod是关键桥梁,它把Java方法调用翻译成SQL执行:
java
public class MapperMethod {
private final SqlCommand command; // SQL命令信息
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)
);
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(
sqlSession.update(command.getName(), param)
);
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(
sqlSession.delete(command.getName(), param)
);
break;
}
case SELECT:
// 查询逻辑更复杂
if (method.returnsVoid() && method.hasResultHandler()) {
// 有ResultHandler的情况
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回集合
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回Map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 返回游标
result = executeForCursor(sqlSession, args);
} else {
// 返回单个对象
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
// 处理返回集合的情况
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// 处理返回数组的情况
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
}
代码清单3:MapperMethod执行逻辑
4. 配置文件加载:MyBatis的"启动过程"
4.1 配置文件的完整加载流程
很多人以为MyBatis配置就是读个XML文件,太天真了!看看这流程:

图2:MyBatis配置文件加载流程
4.2 Configuration:MyBatis的"大脑"
Configuration对象是MyBatis的全局配置中心,它缓存了所有信息:
java
public class Configuration {
// 环境信息
protected Environment environment;
// 注册的Mapper
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();
// 类型处理器注册表
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
// 类型别名注册表
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
// 语言驱动注册表
protected final LanguageDriverRegistry languageDriverRegistry = new LanguageDriverRegistry();
// 映射的SQL语句
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");
// 缓存
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
// 结果映射
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
// 参数映射
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
// 加载的关键方法
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
public MappedStatement getMappedStatement(String id) {
return getMappedStatement(id, true);
}
}
代码清单4:Configuration核心属性
关键点 :Configuration是单例的,全局唯一。这也是为什么MyBatis的插件(Interceptor)是全局生效的。
4.3 Mapper注册过程
Mapper接口是怎么注册到MyBatis中的?看代码:
java
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 1. 添加到knownMappers
knownMappers.put(type, new MapperProxyFactory<>(type));
// 2. 解析Mapper注解
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获取Mapper代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 创建代理实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
}
代码清单5:Mapper注册过程
5. SQL执行过程:从Java方法到数据库
5.1 完整的SQL执行链路
这是MyBatis最复杂的部分,看这张图:

图3:SQL执行完整链路
5.2 Executor:SQL执行的"指挥官"
Executor是执行器的核心,它负责调度整个执行过程:
java
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 {
// 1. 获取BoundSql(包含实际SQL和参数)
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 创建缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 执行查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@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) {
// 缓存命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,从数据库查询
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;
// 先放入缓存(占位)
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 真正的查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 放入缓存
localCache.putObject(key, list);
return list;
}
}
代码清单6:BaseExecutor查询逻辑
5.3 StatementHandler:真正的SQL执行者
StatementHandler负责创建和执行Statement:
java
public class PreparedStatementHandler implements StatementHandler {
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行查询
ps.execute();
// 处理结果集
return resultSetHandler.handleResultSets(ps);
}
@Override
public void parameterize(Statement statement) throws SQLException {
// 设置参数
parameterHandler.setParameters((PreparedStatement) statement);
}
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
// 准备Statement
Statement statement = null;
try {
// 创建PreparedStatement
statement = instantiateStatement(connection);
// 设置超时
setStatementTimeout(statement, transactionTimeout);
// 设置FetchSize
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
}
}
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
// 处理返回主键的情况
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
// 设置ResultSet类型
return connection.prepareStatement(sql,
mappedStatement.getResultSetType().getValue(),
ResultSet.CONCUR_READ_ONLY);
} else {
// 普通的PreparedStatement
return connection.prepareStatement(sql);
}
}
}
代码清单7:StatementHandler执行SQL
6. 缓存机制:性能提升的关键
6.1 一级缓存 vs 二级缓存
很多人分不清MyBatis的两种缓存,这很危险!
| 缓存类型 | 作用范围 | 生命周期 | 默认开启 | 注意点 |
|---|---|---|---|---|
| 一级缓存 | SqlSession级别 | 会话期间 | 是 | 同一个SqlSession有效 |
| 二级缓存 | Mapper级别 | 应用级别 | 否 | 需要手动开启 |
6.2 一级缓存的坑
我踩过最深的坑就是一级缓存。看这个例子:
java
// 错误示例
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.findById(1L); // 查数据库
System.out.println(user1);
// 第二次查询
User user2 = mapper.findById(1L); // 从一级缓存获取
System.out.println(user2); // 和user1是同一个对象!
// 修改user1
user1.setName("修改后");
// 第三次查询
User user3 = mapper.findById(1L); // 还是从缓存获取
System.out.println(user3.getName()); // 输出"修改后"!数据不一致!
}
问题:一级缓存导致数据不一致,而且同一个对象被修改会影响缓存。
解决方案:
java
// 方案1:清除缓存
session.clearCache();
// 方案2:创建新的SqlSession
// 方案3:关闭一级缓存(不推荐)
6.3 二级缓存的正确使用
二级缓存更复杂,但用好了性能提升明显:
XML
<!-- 开启二级缓存 -->
<mapper namespace="com.example.UserMapper">
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="findById" resultType="User" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
性能测试(查询同一数据1000次):
| 缓存类型 | 平均耗时(ms) | 数据库查询次数 | 内存占用 |
|---|---|---|---|
| 无缓存 | 1250 | 1000 | 低 |
| 一级缓存 | 45 | 1 | 中 |
| 二级缓存 | 15 | 1 | 高 |
注意 :二级缓存是跨SqlSession的,要确保缓存的实体类实现Serializable。
7. 插件机制:MyBatis的"钩子"
7.1 插件原理:责任链模式
MyBatis插件基于责任链模式实现,可以拦截4大对象:
-
Executor
-
StatementHandler
-
ParameterHandler
-
ResultSetHandler
java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SlowSqlInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(SlowSqlInterceptor.class);
private long slowSqlThreshold = 1000; // 1秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 继续执行
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
if (costTime > slowSqlThreshold) {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
log.warn("慢SQL检测: {}, 耗时: {}ms, 参数: {}",
boundSql.getSql(), costTime, boundSql.getParameterObject());
// 可以发送告警
AlertManager.sendSlowSqlAlert(boundSql.getSql(), costTime);
}
}
}
@Override
public Object plugin(Object target) {
// 创建代理
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置
String threshold = properties.getProperty("slowSqlThreshold");
if (threshold != null) {
this.slowSqlThreshold = Long.parseLong(threshold);
}
}
}
代码清单8:慢SQL监控插件
7.2 插件的执行顺序
插件是按配置顺序执行的,这很重要:
XML
<plugins>
<!-- 先执行插件1 -->
<plugin interceptor="com.example.plugin1">
<property name="property1" value="value1"/>
</plugin>
<!-- 再执行插件2 -->
<plugin interceptor="com.example.plugin2">
<property name="property2" value="value2"/>
</plugin>
</plugins>
执行顺序:
原始对象
↓
插件2代理
↓
插件1代理
↓
目标方法
8. 类型处理器:Java与数据库的桥梁
8.1 内置类型处理器
MyBatis内置了丰富的类型处理器,但很多人不知道:
| Java类型 | JDBC类型 | 处理器 | 说明 |
|---|---|---|---|
| String | VARCHAR | StringTypeHandler | 字符串 |
| Integer | INTEGER | IntegerTypeHandler | 整数 |
| Date | TIMESTAMP | DateTypeHandler | 日期 |
| Enum | VARCHAR | EnumTypeHandler | 枚举(存名字) |
| Enum | INTEGER | EnumOrdinalTypeHandler | 枚举(存序号) |
| boolean | BOOLEAN | BooleanTypeHandler | 布尔 |
8.2 自定义类型处理器
我做过一个需求:把JSON字符串存到数据库。用自定义类型处理器很方便:
java
// 1. 定义处理器
@MappedTypes(MyData.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<MyData> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
MyData parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
} catch (JsonProcessingException e) {
throw new SQLException("对象转JSON失败", e);
}
}
@Override
public MyData getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public MyData getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public MyData getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private MyData parseJson(String json) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return objectMapper.readValue(json, MyData.class);
} catch (IOException e) {
throw new RuntimeException("JSON解析失败: " + json, e);
}
}
}
// 2. 注册处理器
@MappedTypes(MyData.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<MyData> {
// 实现...
}
// 3. 在配置中注册
<typeHandlers>
<typeHandler handler="com.example.JsonTypeHandler"/>
</typeHandlers>
代码清单9:自定义JSON类型处理器
9. 实战:性能优化与问题排查
9.1 性能优化技巧
技巧1:合理使用批量操作
java
// 错误:每次insert都提交
for (User user : userList) {
userMapper.insert(user); // 每次都有commit
}
// 正确:批量提交
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : userList) {
mapper.insert(user);
}
session.commit(); // 一次性提交
}
性能对比:插入1000条记录
-
逐条提交:1250ms
-
批量提交:350ms
-
提升:72%
技巧2:使用分页查询
java
// 错误:一次性查询所有
List<User> users = userMapper.findAll(); // 如果数据量大,内存爆炸
// 正确:分页查询
PageHelper.startPage(1, 100); // 第1页,每页100条
List<User> users = userMapper.findByCondition(condition);
技巧3:合理使用缓存
XML
<!-- 只缓存不经常变的配置数据 -->
<select id="findConfig" resultType="Config" useCache="true" flushCache="false">
SELECT * FROM config WHERE type = #{type}
</select>
<!-- 经常变的数据不要缓存 -->
<select id="findLatestOrders" resultType="Order" useCache="false">
SELECT * FROM orders ORDER BY create_time DESC LIMIT 100
</select>
9.2 常见问题排查
问题1:N+1查询问题
现象:查询列表,然后循环查询详情,产生大量SQL。
解决方案:
XML
<!-- 使用关联查询 -->
<select id="findUserWithOrders" resultMap="userWithOrders">
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<resultMap id="userWithOrders" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
问题2:参数绑定错误
现象 :#{}和${}用错。
区别:
-
#{}:预编译,防SQL注入 -
${}:字符串替换,不安全
sql
-- 安全
WHERE name = #{name} -- 变成 WHERE name = ?
-- 不安全,但有特殊用途
ORDER BY ${orderBy} -- 动态排序字段
问题3:延迟加载问题
现象:关闭Session后访问懒加载属性报错。
解决方案:
XML
<!-- 开启后需要管理Session生命周期 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
或者使用FetchType.EAGER:
java
public class User {
@OneToMany(fetch = FetchType.EAGER) // 立即加载
private List<Order> orders;
}
10. 企业级最佳实践
10.1 我的"MyBatis军规"
经过多年实践,我总结了一套MyBatis最佳实践:
📜 第一条:明确SQL写在XML还是注解
-
简单SQL用注解
-
复杂SQL、动态SQL用XML
-
不要混用
📜 第二条:合理使用缓存
-
配置类数据用二级缓存
-
事务性数据不用缓存
-
缓存要设置合理的超时时间
📜 第三条:批量操作要用批量模式
-
插入/更新大量数据用
ExecutorType.BATCH -
记得手动提交事务
📜 第四条:监控SQL性能
-
用插件监控慢SQL
-
定期分析执行计划
-
设置合理的超时时间
📜 第五条:代码可读性
-
SQL要格式化
-
复杂的SQL要加注释
-
参数要有明确的命名
10.2 生产环境配置模板
XML
<!-- mybatis-config.xml -->
<configuration>
<!-- 设置 -->
<settings>
<!-- 开启下划线转驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 查询超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 返回null时调用setter方法 -->
<setting name="callSettersOnNulls" value="true"/>
<!-- 日志实现 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<package name="com.example.model"/>
</typeAliases>
<!-- 插件 -->
<plugins>
<!-- 分页插件 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="reasonable" value="true"/>
</plugin>
<!-- 慢SQL监控 -->
<plugin interceptor="com.example.SlowSqlInterceptor">
<property name="slowSqlThreshold" value="1000"/>
</plugin>
</plugins>
<!-- 环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 连接池配置 -->
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="10"/>
<property name="poolMaximumCheckoutTime" value="20000"/>
</dataSource>
</environment>
</environments>
<!-- 映射文件 -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
</configuration>
11. 最后的话
MyBatis就像一把瑞士军刀,功能多但要用对地方。用好了,它能让你如虎添翼;用不好,它就是定时炸弹。
我见过太多团队在MyBatis上栽跟头:有的因为缓存问题导致数据不一致,有的因为SQL注入导致安全漏洞,有的因为N+1查询拖垮数据库。
记住:框架是工具,不是黑盒子。理解原理,掌握细节,才能在关键时刻解决问题。
📚 推荐阅读
官方文档
-
**MyBatis官方文档** - 最权威的参考
-
**MyBatis-Spring整合文档** - Spring整合指南
源码学习
-
**MyBatis GitHub仓库** - 直接看源码
-
**MyBatis Spring Boot Starter** - Spring Boot整合
实践指南
-
**阿里巴巴Java开发手册** - MyBatis章节必看
-
**MyBatis最佳实践** - 官方博客
性能优化
-
**MySQL性能优化** - 数据库层面优化
-
**Arthas诊断工具** - MyBatis性能诊断
最后建议 :打开IDE,创建一个简单的MyBatis项目,然后打断点跟一遍源码。从SqlSessionFactoryBuilder.build()开始,一步步看MyBatis是怎么启动的,Mapper是怎么代理的,SQL是怎么执行的。亲手跟一遍,胜过看十篇文章。