MyBatis Plus (MP) 是一个深受欢迎的框架,其核心宗旨是在 MyBatis 的基础上进行增强而非改变。在使用 MP 时,开发者常有两个主要的疑问 🤔️ :
- MP 如何实现对 MyBatis 的无痛扩展?
- MP 是如何将默认的 CRUD 方法自动注入到 Mapper 中的?
MP 如何实现 MyBatis 的无侵入式扩展
让我们通过一个简单的启动方法来进行源码调试,以揭示 MP 的工作原理:
java
public class MySimpleTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeAll
public static void init() throws IOException, SQLException {
InputStream reader = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(reader);
/**
* 运行初始化脚本
*/
Configuration configuration = sqlSessionFactory.getConfiguration();
DataSource dataSource = configuration.getEnvironment().getDataSource();
Connection connection = dataSource.getConnection();
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.runScript(Resources.getResourceAsReader("h2/user.ddl.sql"));
}
@Test
public void selectList(){
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
H2UserMapper mapper = sqlSession.getMapper(H2UserMapper.class);
List<H2User> user = mapper.selectList(null);
System.out.println(user);
}
}
}
从上面的启动方法中可以看出,MP 的启动流程和 mybatis 是一样的。但其中的奥妙就是 MP 的启动用的是 MybatisSqlSessionFactoryBuilder
,而不是 mybatis 中的 SqlSessionFactoryBuilder
,这一改变使得 MP 能够在整个初始化流程中使用自己重写或继承自 mybatis 的各种类。
首先是使用 MybatisXMLConfigBuilder(copy 的 XMLConfigBuilder)
进行配置文件的解析。
MybatisConfiguration
MybatisMapperRegistry
*(通过 MybatisMapperRegistry 完成 CRUD 方法注入 )MybatisMapperProxyFactory
MybatisMapperProxy
MybatisMapperMethod
MybatisMapperAnnotationBuilder
这里的重点是在两个位置:
- 入口文件:
MybatisSqlSessionFactoryBuilder
通过重写入口文件进而可以控制整个初始化流程。- 在 spring 环境下 会使用
MybatisSqlSessionFactoryBean
但流程是不变的
- 在 spring 环境下 会使用
- MapperRegistry:
MybatisMapperRegistry
通过重写了使用自己的 MapperRegistry 达到了封装 Mapper 的作用,则可以注入默认的 CRUD 方法。
这里的所有的 Mybaits 开头的类,都在 mybatis 源码中有对应的一样的类,而MP 则是 copy 或者 基础了 mybaits 的类,然后在自己需要的地方做了修改。
MP 如何注入默认的 CRUD 方法
仅由上面分析,MybatisMapperRegistry
中会初始化 MybatisMapperAnnotationBuilder
类,谜底就在这个类中。 而这个类 只重写了 {@link MapperAnnotationBuilder#parse} 和 #getReturnType
方法,我们来看看 parse 方法里关键的一段:
java
@Override
public void parse() {
//...
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
// 处理默认CRUD注入
parserInjector();
}
//...
}
这里判断 这个Mapper 是否继承自某个特定的Mapper 如果是的话,则处理默认CRUD注入。
先看看 GlobalConfigUtils.isSupperMapperChildren(configuration, type)
这里的SupperMapper 到底是什么?
这里看到 只要 Mapper 继承了 Mapper
这个类,则会进行默认的CRUD注入。
🤔️ 默认我们都会继承
BaseMapper
,而BaseMapper
是Mapper
的子类 。 继承BaseMapper
后,则默认可以调用CRUD功能,而继承了Mapper
却什么都没有。 那只继承了Mapper
就可以注入默认的CRUD 是为什么呢?
我们来看看 parserInjector();
处理注入这个方法,有什么奥妙吧! parserInjector()
方法最后调用了 AbstractSqlInjector.inspectInject(...)
java
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
if (CollectionUtils.isNotEmpty(methodList)) {
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
这里获取了modelClass
也就是实体 class,然后判断是否已经注入。
- 若没有注入 则进行
initTableInfo
- 根据实体和配置信息初始化表信息对象 - 关键方法:
this.getMethodList(mapperClass, tableInfo);
获取需要注入的方法列表
这里看看 getMethodList(...)
方法
java
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
.add(new Insert())
.add(new Delete())
.add(new Update())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectObjs())
.add(new SelectList());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}```
这里可以看到就是默认的 CRUD 在这个方法里添加进来了。
我们以 `SelectList` 为例,看看这里面做了什么
```java
public class SelectList extends AbstractMethod {
public SelectList() {
this(SqlMethod.SELECT_LIST.getMethod());
}
/**
* @param name 方法名
* @since 3.5.0
*/
public SelectList(String name) {
super(name);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
}
}
- SqlMethod:这个类是 MybatisPlus 的一个 Sql 定义方法 枚举,里面的存有 方法名、注释、方法SQL 三个参数。 这里的
injectMappedStatement
方法组装SQL后调用了addSelectMappedStatementForTable
方法,最终调用MapperBuilderAssistant.addMappedStatement
也就是通过 Mybaits 官方的构建助手 添加了 MappedStatement
这也就解释了为什么我们继承了 BaseMapper 则默认可以调用 CRUD 方法。