揭秘 MyBatis Plus:如何无痛扩展 MyBatis 和实现自动化 CRUD

MyBatis Plus (MP) 是一个深受欢迎的框架,其核心宗旨是在 MyBatis 的基础上进行增强而非改变。在使用 MP 时,开发者常有两个主要的疑问 🤔️ :

  1. MP 如何实现对 MyBatis 的无痛扩展?
  2. 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

这里的重点是在两个位置:

  1. 入口文件:MybatisSqlSessionFactoryBuilder 通过重写入口文件进而可以控制整个初始化流程。
    • 在 spring 环境下 会使用 MybatisSqlSessionFactoryBean 但流程是不变的
  2. 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 ,而 BaseMapperMapper 的子类 。 继承 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 方法。

相关推荐
七星静香19 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员20 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU20 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie624 分钟前
在IDEA中使用Git
java·git
Elaine20239139 分钟前
06 网络编程基础
java·网络
G丶AEOM40 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201341 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀42 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel