链路追踪视角:MyBatis-Plus 如何基于 MyBatis 封装 BaseMapper
MyBatis-Plus(简称 MP)作为 MyBatis 的增强框架,通过 BaseMapper
提供了通用的 CRUD 操作,极大地提升了开发效率。为了更透彻地理解其封装机制,本文将采用链路追踪的思维,从开发者调用接口开始,逐步深入到 MyBatis-Plus 的核心实现,分析其如何基于 MyBatis 完成对 BaseMapper 的封装。
一、从调用开始:BaseMapper 的使用场景
假设我们有一个简单的实体类 User
和对应的 Mapper 接口:
java
@TableName("t_user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
// getter 和 setter 省略
}
public interface UserMapper extends BaseMapper<User> {
}
开发者只需这样调用:
java
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);
表面上看,selectById
是 BaseMapper
提供的方法,但其背后是如何实现的呢?让我们沿着调用链路逐步追踪。
二、链路追踪:从接口调用到代理执行
1. 获取 Mapper 代理对象
当调用 sqlSession.getMapper(UserMapper.class)
时,MyBatis 的 SqlSession
会委托给 Configuration
的 getMapper
方法:
java
// MyBatis: org.apache.ibatis.session.Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
这里的 mapperRegistry
是 MyBatis 的 MapperRegistry
类,但在 MyBatis-Plus 中被替换为 MybatisMapperRegistry
。链路进入 MyBatis-Plus 的自定义实现:
java
// MyBatis-Plus: com.baomidou.mybatisplus.core.MybatisMapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
return mapperProxyFactory.newInstance(sqlSession);
}
- 关键点 :MyBatis-Plus 重写了
MapperRegistry
,并在启动时通过扫描将所有继承BaseMapper
的接口注册到knownMappers
中。 - 代理生成 :
newInstance
方法创建了一个MapperProxy
对象,这是 JDK 动态代理的实现。
2. 方法调用拦截
当调用 userMapper.selectById(1L)
时,代理对象 MapperProxy
的 invoke
方法被触发:
java
// MyBatis-Plus: com.baomidou.mybatisplus.core.override.MybatisMapperProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
- 链路分叉 :
cachedMapperMethod
会根据方法签名缓存并返回一个MybatisMapperMethod
对象。 - 执行逻辑 :
execute
方法根据方法名和参数决定具体的执行路径。
对于 selectById
,链路进入 MybatisMapperMethod.execute
:
java
// MyBatis-Plus: com.baomidou.mybatisplus.core.override.MybatisMapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
if (SqlCommandType.SELECT == command.getType()) {
if (method.getReturnType().isAssignableFrom(List.class)) {
return sqlSession.selectList(command.getName(), args);
}
return sqlSession.selectOne(command.getName(), args[0]);
}
// 其他类型如 INSERT、UPDATE 等略
}
- 关键点 :
command.getName()
返回的是一个全局唯一的 SQL ID,例如com.baomidou.mybatisplus.core.mapper.BaseMapper.selectById
。 - 执行 SQL :最终调用 MyBatis 的
selectOne
方法执行查询。
三、SQL 注入:BaseMapper 方法的实现来源
问题来了:selectById
的 SQL 是从哪里来的?答案在于 MyBatis-Plus 的 SQL 注入机制。
1. 启动时的 SQL 注入
MyBatis-Plus 在 Spring 容器初始化时,通过 MapperScannerConfigurer
扫描 Mapper 接口,并调用 ISqlInjector
注入通用 SQL:
java
// MyBatis-Plus: com.baomidou.mybatisplus.core.injector.DefaultSqlInjector
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
List<AbstractMethod> methodList = getMethodList(mapperClass);
for (AbstractMethod method : methodList) {
method.inject(builderAssistant, mapperClass);
}
}
- 方法列表 :
getMethodList
返回BaseMapper
中定义的所有方法(如selectById
、insert
等)的实现类,例如SelectById
。 - 注入过程 :以
SelectById
为例:
java
// MyBatis-Plus: com.baomidou.mybatisplus.core.injector.methods.SelectById
public void injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = String.format("<script>SELECT %s FROM %s WHERE %s = #{id}</script>",
sqlSelectColumns(), tableInfo.getTableName(), tableInfo.getKeyColumn());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
addSelectMappedStatement(mapperClass, "selectById", sqlSource, modelClass, tableInfo);
}
- SQL 生成 :根据
TableInfo
(通过反射解析实体类)动态生成SELECT * FROM t_user WHERE id = #{id}
。 - 注册 :将生成的
MappedStatement
注册到 MyBatis 的Configuration
中。
2. TableInfo 的作用
TableInfo
是 MyBatis-Plus 的核心元数据类,通过 TableInfoHelper
在启动时解析实体类:
@TableName("t_user")
→ 表名t_user
。@TableId
→ 主键字段id
。- 字段映射 → 自动推断
name
、age
等列名。
这些信息为 SQL 注入提供了基础数据。
四、链路总结:从调用到执行的全流程
- 开发者调用 :
userMapper.selectById(1L)
。 - 代理拦截 :
MybatisMapperProxy.invoke
→MybatisMapperMethod.execute
。 - SQL 执行 :
sqlSession.selectOne
→ MyBatis 执行预注入的MappedStatement
。 - 结果映射 :MyBatis 将查询结果映射为
User
对象返回。
背后支持:
- 启动时 :扫描 Mapper → 注入通用 SQL → 注册
MappedStatement
。 - 运行时:动态代理 → 方法路由 → SQL 执行。
五、与 MyBatis 的协作与增强
- 复用 MyBatis :动态代理、
SqlSession
、结果映射等核心机制完全继承自 MyBatis。 - 增强点 :
- SQL 自动化 :通过
ISqlInjector
注入通用 SQL。 - 元数据管理 :
TableInfo
实现实体与表的自动映射。 - 条件构造 :
Wrapper
扩展了动态查询能力。
- SQL 自动化 :通过
六、结论
MyBatis-Plus 对 BaseMapper 的封装,是在 MyBatis 动态代理和 SQL 执行框架上的巧妙扩展。通过启动时的 SQL 注入和运行时的代理拦截,它实现了通用 CRUD 的零配置使用。链路追踪显示,这种设计既保留了 MyBatis 的灵活性,又通过自动化大幅提升了开发效率,堪称对 MyBatis 的"完美补完"。