Java · #orm#source-code#java#mybatis

MyBatis核心源码分析与插件开发

2025.03.05 Java 13 min 5.0k
// 目录 · contents

引言

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
// SqlSessionFactory的创建(通常全局单例)
public class MyBatisConfig {

public SqlSessionFactory createSqlSessionFactory() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

// SqlSessionFactoryBuilder解析配置并构建SqlSessionFactory
// 内部流程:XML解析 -> Configuration对象 -> DefaultSqlSessionFactory
return new SqlSessionFactoryBuilder().build(inputStream);
}
}

// SqlSession的正确使用方式
public class SqlSessionLifecycle {

private final SqlSessionFactory sqlSessionFactory;

public SqlSessionLifecycle(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}

public User getUserById(Long id) {
// 每次操作创建新的SqlSession(非线程安全)
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectById(id);
}
// try-with-resources确保SqlSession被正确关闭
}

public void batchInsert(List<User> users) {
// 批量操作使用BATCH类型的Executor
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
/**
* MapperProxy核心逻辑简化版
* 将Mapper接口的方法调用转换为SqlSession的操作
*/
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 {
// Object类的方法直接调用(如toString、hashCode等)
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}

// 从缓存获取或创建MapperMethodInvoker
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}

private MapperMethodInvoker cachedInvoker(Method method) {
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
// Java 8+ 接口默认方法,直接调用
return new DefaultMethodInvoker(m);
} else {
// 普通接口方法,创建MapperMethod
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, m, sqlSession.getConfiguration()));
}
});
}
}

/**
* MapperMethod: 封装了SQL命令类型和方法签名
* 根据SQL类型(SELECT/INSERT/UPDATE/DELETE)调用SqlSession对应方法
*/
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
/**
* SimpleExecutor的doQuery方法简化版
* 每次查询都创建新的Statement
*/
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(会经过插件拦截器链)
StatementHandler handler = configuration.newStatementHandler(
wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 预编译SQL并设置参数
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);
// handler.prepare() 创建PreparedStatement
Statement stmt = handler.prepare(connection, transaction.getTimeout());
// handler.parameterize() 设置SQL参数
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
/**
* 一级缓存(SqlSession级别,默认开启)
* 存储在BaseExecutor的localCache中
*/
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);
// 缓存Key由 statementId + offset + limit + SQL + 参数值 + 环境id 组成
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
/**
* 自定义TypeHandler: 将JSON字符串与Java对象互相转换
* 适用于数据库中存储JSON格式的字段
*/
@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
<!-- 动态SQL示例 -->
<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 &gt;= #{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
/**
* Configuration中创建四大对象时会通过InterceptorChain包装
* 每个插件都会用JDK动态代理包一层
*/
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;
}
}

/**
* InterceptorChain: 按注册顺序依次包装
* 注意:后注册的插件先执行(洋葱模型)
*/
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
/**
* 自定义分页插件
* 拦截Executor的query方法,自动添加分页SQL
*/
@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();
}

// 1. 执行COUNT查询获取总记录数
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)) {
// 设置参数(复用原SQL的参数)
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<>();
}

// 2. 改写SQL添加LIMIT子句
String pageSql = originalSql + " LIMIT " + pageParam.getOffset()
+ ", " + pageParam.getPageSize();

// 通过反射修改BoundSql中的SQL
Field sqlField = BoundSql.class.getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, pageSql);

log.debug("分页SQL: {}", pageSql);

// 3. 执行分页查询
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) {
// Plugin.wrap会判断当前拦截器是否需要代理target
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 可以从mybatis-config.xml中读取插件配置
}
}

实战: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
/**
* SQL执行耗时监控插件
* 拦截StatementHandler的query和update方法,记录执行时间
*/
@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; // 慢SQL阈值,默认1秒

@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
<!-- mybatis-config.xml -->
<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
/**
* 批量插入对比:逐条插入 vs BATCH模式 vs foreach拼接SQL
*/
public class BatchInsertComparison {

// 方式1: 逐条插入(最慢,每次都创建Statement)
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();
}
}

// 方式2: BATCH模式(推荐,攒SQL批量发送)
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));
// 每500条刷新一次,防止内存溢出
if ((i + 1) % batchSize == 0) {
session.flushStatements();
}
}
session.commit();
}
}

// 方式3: foreach拼接多值INSERT(最快,但SQL长度有限制)
// INSERT INTO user (name, age) VALUES ('a', 1), ('b', 2), ('c', 3)...
public void insertForeach(List<User> users) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 分批处理,避免SQL过长
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
<!-- 问题场景:查询用户列表时,每个用户的订单通过嵌套查询获取 -->
<!-- 这会产生 1(查用户列表) + N(每个用户查一次订单) 次SQL -->
<resultMap id="userWithOrders" type="User">
<id column="id" property="id"/>
<collection property="orders" column="id"
select="com.example.mapper.OrderMapper.selectByUserId"/>
</resultMap>

<!-- 解决方案1: 使用JOIN一次查询(推荐) -->
<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>

<!-- 解决方案2: 使用fetchType="lazy"延迟加载 -->
<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核心源码的分析,我们可以更清晰地理解其工作原理:

  1. MapperProxy通过JDK动态代理将Mapper接口方法调用转换为SqlSession操作,实现了面向接口编程的优雅体验。

  2. Executor是SQL执行的核心引擎,SimpleExecutor每次新建Statement,ReuseExecutor复用Statement,BatchExecutor攒SQL批量发送。CachingExecutor作为装饰器提供二级缓存能力。

  3. 四大处理器(StatementHandler、ParameterHandler、ResultSetHandler、TypeHandler)各司其职,分别负责SQL预编译、参数设置、结果映射和类型转换。

  4. 插件机制基于JDK动态代理和责任链模式,可以拦截四大核心对象的方法调用。在开发自定义插件时,需注意拦截器链的执行顺序(后注册先执行)和性能影响。

  5. 性能优化方面,批量操作优先使用BATCH模式,关注N+1查询问题,合理使用缓存,避免在动态SQL中拼接过长的SQL语句。

理解这些源码级的实现细节,不仅有助于日常开发中的问题排查,更能帮助我们在需要时开发高质量的MyBatis插件来满足特定的业务需求。

作者 · authorzt
发布 · date2025-03-05
篇幅 · length5.0k 字 · 13 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论