MyBatis代理机制深度解析:从配置到执行的完整流程
ztj100 2025-07-21 19:09 4 浏览 0 评论
前言
你是否也有过这样的疑惑:为什么只需要配置一个数据源,写一个XML文件定义SQL,再写一个Mapper接口,MyBatis就能神奇地执行数据库操作?这背后到底发生了什么?
本文将深入分析MyBatis的核心工作原理,揭开Mapper代理机制的神秘面纱。
1. MyBatis核心组件概述
在深入原理之前,我们先了解MyBatis的核心组件:
1.1 核心组件关系图
1.2 以如下项目为例
在你的项目中,我们可以看到这样的结构:
// Mapper接口
public interface UserMapper {
int insert(UserQueryPO reportQueryPO);
// 其他方法...
}
// Repository实现类
@Service
@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final UserMapper userMapper; // 这里就是MyBatis的代理对象
@Override
public boolean insertOrUpdate(User userPO) {
// 调用Mapper方法,实际上调用的是代理对象
int result = userMapper.insert(userPO);
return result > 0;
}
}
2. MyBatis启动流程:从配置到代理
2.1 配置解析阶段
当Spring Boot应用启动时,MyBatis会经历以下步骤:
步骤1:解析配置文件
// MyBatis会解析application.yml中的配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/cbi_report
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
步骤2:构建Configuration对象
public class Configuration {
// 存储所有的配置信息
protected Environment environment; // 数据源和事务管理器
protected MapperRegistry mapperRegistry; // Mapper注册表
protected Map<String, MappedStatement> mappedStatements; // SQL映射语句
// ... 其他配置
}
步骤3:扫描并注册Mapper接口
// MyBatis会扫描@Mapper注解或配置的包路径
@MapperScan("com.cbi.report.engine.infrastructure.persistence.mapper")
public class MyBatisConfig {
// 扫描指定包下的所有Mapper接口
}
2.2 Mapper注册过程
当MyBatis发现`UserMapper`接口时,会执行以下操作:
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 为每个Mapper接口创建一个代理工厂
MapperProxyFactory<T> mapperProxyFactory = new MapperProxyFactory<>(type);
knownMappers.put(type, mapperProxyFactory);
}
}
}
3. 代理对象创建:MapperProxy的诞生
3.1 MapperProxyFactory工厂类
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 创建代理对象的核心方法
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy // InvocationHandler
);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
3.2 MapperProxy代理处理器
这是MyBatis的核心所在:
public class MapperProxy<T> implements InvocationHandler, Serializable {
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 {
try {
// 如果是Object类的方法(如toString、equals),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 这里是关键:为每个Mapper方法创建MapperMethodInvoker
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return methodCache.computeIfAbsent(method, m -> {
// 为每个方法创建调用器
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
}
4. 方法调用执行:从接口到SQL
4.1 当你调用Mapper方法时发生了什么
以如下代码为例:
int result = userMapper.insert(userPO);
实际的执行流程是:
Step 1: 代理拦截
// MapperProxy.invoke()被调用
public Object invoke(Object proxy, Method method, Object[] args) {
// method = UserMapper.insert(UserPO)
// args = [userPO]
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
Step 2: MapperMethod执行
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:
// ...
case DELETE:
// ...
case SELECT:
// ...
}
return result;
}
}
Step 3: SqlSession执行
// 最终调用SqlSession的insert方法
public int insert(String statement, Object parameter) {
return update(statement, parameter); // INSERT实际上也是update操作
}
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
4.2 Executor执行SQL
public class SimpleExecutor extends BaseExecutor {
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 准备Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行SQL
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 创建PreparedStatement
stmt = handler.prepare(connection, transaction.getTimeout());
// 设置参数
handler.parameterize(stmt);
return stmt;
}
}
5. 完整的调用时序图
5.1 MyBatis执行流程时序图:
5.2 MyBatis核心组件架构图:
5.3 Mapper方法调用生命周期的流程图:
5.4 图表说明
1.时序图 - MyBatis执行流程
展示了从你的代码 userMapper.insert(userPO) 开始,到数据库执行完成的完整时序流程,包含16个关键步骤:
代理拦截 - JDK动态代理拦截方法调用
方法解析 - 确定SQL类型和参数映射
SQL执行 - 通过多层处理器最终执行数据库操作
2.架构图- MyBatis核心组件
展示了MyBatis的整体架构,包括:
应用层 - 你的业务代码和Spring容器
MyBatis核心层 - 配置中心、代理工厂、会话管理 执行层 - SQL执行器和各种处理器
数据层 - 数据源和数据库
3.流程图- Mapper方法调用生命周期
详细展示了一个Mapper方法的完整调用过程:
代理检查 - 验证是否为代理对象 缓存机制 - 方法缓存和性能优化
SQL类型判断 -
INSERT/UPDATE/DELETE/SELECT分支处理
参数处理 - Java对象到SQL参数的转换
实际应用场景
以如下代码为例:
// 这行代码触发的完整流程
int result = userMapper.insert(userPO);
执行路径:
Client → Spring注入代理 → MapperProxy拦截 → 解析INSERT类型 → SqlSession管理 → Executor执行 → StatementHandler处理 → 数据库执行 → 返回影响行数
这些图表帮助你可视化理解:
为什么不需要实现类 - 代理对象替代了实现
如何找到对应的SQL - 通过方法名和配置映射
性能如何优化 - 通过缓存和连接池
事务如何管理 - 通过Executor层统一处理
现在MyBatis的"魔法"应该不再神秘了!每个组件都有明确的职责,通过层层调用最终完成数据库操作。
6. 关键问题解答
6.1 为什么Mapper接口不需要实现类?
因为MyBatis使用JDK动态代理为每个接口创建了代理对象。当你调用接口方法时,实际上调用的是`MapperProxy.invoke()`方法。
6.2 MyBatis如何知道执行哪个SQL?
通过`MappedStatement`对象,它包含了:
- SQL语句ID(通常是接口全名+方法名)
- SQL语句内容
- 参数映射
- 结果映射
6.3 参数是如何传递的?
// 在你的例子中
userMapper.insert(userPO);
// MyBatis会:
// 1. 解析方法参数
// 2. 根据@Param注解或参数位置创建参数映射
// 3. 将Java对象转换为SQL参数
7. MyBatis Plus的增强
在你的项目中使用的是MyBatis Plus,它在MyBatis基础上增加了:
7.1 BaseMapper接口
public interface BaseMapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int updateById(T entity);
T selectById(Serializable id);
// ... 更多通用方法
}
7.2 自动SQL生成
MyBatis Plus可以根据实体类自动生成SQL,无需编写XML:
// 你的Mapper只需要继承BaseMapper
public interface UserMapper extends BaseMapper<UserPO> {
// 自动拥有CRUD方法,无需写SQL
}
8. 实战调试:查看代理对象
你可以通过以下方式验证代理机制:
@Service
@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final UserMapper userMapper;
@Override
public boolean insertOrUpdate(User user) {
// 打印代理对象信息
System.out.println("Mapper类型: " + userMapper.getClass());
System.out.println("是否为代理: " + Proxy.isProxyClass(userMapper.getClass()));
// 执行数据库操作
UserPO userPO = convertToPO(user);
int result = userMapper.insert(userPO);
return result > 0;
}
}
输出结果类似:
Mapper类型: class com.sun.proxy.$Proxy123
是否为代理: true
9. 总结
MyBatis的"魔法"实际上是通过以下技术实现的:
1. **JDK动态代理**:为Mapper接口创建代理对象
2. **反射机制**:解析接口方法和注解
3. **XML解析**:解析SQL映射文件
4. **JDBC封装**:处理数据库连接和SQL执行
整个流程可以概括为:
配置解析 → Mapper注册 → 代理创建 → 方法拦截 → SQL执行 → 结果返回
当你理解了这个原理后,MyBatis就不再神秘了。它只是将复杂的JDBC操作进行了优雅的封装,让开发者可以专注于业务逻辑而不是技术细节。
---
**扩展思考**:
- 为什么MyBatis选择JDK动态代理而不是CGLIB?
- MyBatis的一级缓存和二级缓存是如何实现的?
- 在分布式环境下,MyBatis的事务是如何管理的?
这些问题留给读者进一步探索和学习。
相关推荐
- 新手学Docker:挂载卷Volumes(docker挂载nfs卷)
-
默认情况下,在容器内创建的文件,都会保存在容器基于镜像之上的一个可读写层。但是,这一层是依赖于容器而存在的,当容器不存在时,这些数据也就不存在了。而且这些数据紧紧的耦合在容器内部,很难将这些数据导出。...
- Docker命令最全详解(39个最常用命令)
-
Docker是云原生的核心,也是大厂的必备技能,下面我就全面来详解Docker核心命令@mikechen本文作者:陈睿|mikechen文章来源:mikechen.cc一、Docker基本命令...
- 松勤软件测试:详解Docker,如何用portainer管理Docker容器
-
镜像管理搜索镜像dockersearch镜像名称拉取镜像dockerpullname[:tag]列出镜像dockerimages删除镜像dockerrmiimage名称或id删除...
- 【Docker 新手入门指南】第十四章:Docker常用命令
-
以下Docker常用命令大全,涵盖容器、镜像、网络、数据卷及系统管理等核心操作,结合分类与示例助你快速掌握:一、容器生命周期管理...
- Docker 一键清盘术,释放磁盘空间不求人
-
在日常开发和运维中,Docker容器化让我们的工作更高效,但不知不觉中,磁盘空间却被镜像、容器和卷悄悄吃掉。今天教你一套干净利落的Docker磁盘清理攻略,让你轻松腾出宝贵空间!一、快速查看磁盘...
- 第六节 Docker 容器核心操作与实践指南
-
一、镜像与容器的本质关系(一)镜像(Image)的核心特性静态模板:包含应用运行所需的所有依赖(系统库、运行时、配置文件等)不可变性:镜像构建完成后内容不可修改,确保环境一致性...
- 别再去找Docker命令了,你要的常用的全都在这
-
Docker常用命令集合给大家介绍一些常用的Docker命令,对你有帮助的同学建议收藏作为一个查询手册哦。Docker容器的一些命令按功能分类大致如下:Docker环境信息info、versio...
- Docker 数据持久化最佳实践:Volume 使用全指南
-
你是不是也经常在使用Docker的时候,遇到“数据持久化”相关的困惑?容器删了,数据也跟着没了?今天就带你深入了解Docker的Volume机制,搞懂什么是Volume、怎么使用、存储在...
- 【Docker 新手入门指南】第十五章:常见故障排除
-
一、前期准备:收集关键信息在排查问题前,建议先获取以下系统数据,便于精准定位故障:...
- Docker网络与iptables实战指南:从原理到安全加固
-
一、Docker网络核心原理:容器与iptables的“共生关系”Docker容器的网络能力依赖于Linux内核的两大核心技术:虚拟网络设备(vethpair、网桥)和iptables规则链。以默认...
- 别再docker exec了!盘点进入容器排错的5种“更优雅”姿势
-
一、dockerattach:直接“附身”容器的原始方法当容器日志疯狂刷屏时,dockerattach就像直接跳进正在行驶的汽车——能看到仪表盘数据,但方向盘不在你手上。这种直接附加到容器主进程的...
- 云计算核心技术Docker教程:Docker数据卷的使用
-
在生产环境中使用Docker,要想实现数据的持久化(所谓Docker的数据持久化即数据不随着Container的结束而结束)或者需要在多个容器之间进行数据共享,需要将数据从宿主机挂载到容器中,这就会...
- 【开发技术】Mybatis中进行多表关联查询?性能是不是会变好呢?
-
Mybatis是一种基于Java的持久层框架,能够帮助我们操作数据库。在Mybatis中,进行多表关联的整合查询,需要使用嵌套查询或者使用ResultMap进行映射。下面,我们将从这两个方面来介绍多表...
- Mybatis 批量更新数据 Mysql批量更新数据
-
通常如果需要一次更新多条数据有两个方式,(1)在业务代码中循环遍历逐条更新。(2)一次性更新所有数据1批量更新相同的值不同的条件...
- Mybatis 如何批量删除数据(mybatis批量merge)
-
Mybatis如何批量删除数据本期以最常用的根据id批量删除数据为例:接口设计1:List类型单参数IntegerdeleteByIds(List<Integer>ids);...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)