揭秘 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 方法。

相关推荐
Cikiss8 分钟前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
wxin_VXbishe8 分钟前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
Cikiss9 分钟前
微服务实战——平台属性
java·数据库·后端·微服务
无敌の星仔18 分钟前
一个月学会Java 第2天 认识类与对象
java·开发语言
OEC小胖胖23 分钟前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web
2401_857617621 小时前
SpringBoot校园资料平台:开发与部署指南
java·spring boot·后端
quokka561 小时前
Springboot 整合 logback 日志框架
java·spring boot·logback
计算机学姐1 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
救救孩子把1 小时前
深入理解 Java 对象的内存布局
java
落落落sss1 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis