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

相关推荐
极客先躯29 分钟前
高级java每日一道面试题-2025年01月23日-数据库篇-主键与索引有什么区别 ?
java·数据库·java高级·高级面试题·选择合适的主键·谨慎创建索引·定期评估索引的有效性
码至终章32 分钟前
kafka常用目录文件解析
java·分布式·后端·kafka·mq
Mr.Demo.36 分钟前
[Spring] Nacos详解
java·后端·spring·微服务·springcloud
luoganttcc1 小时前
华为升腾算子开发(一) helloword
java·前端·华为
Dlwyz1 小时前
Maven私服-Nexus3安装与使用
java·maven
智_永无止境1 小时前
Springboot使用war启动的配置
java·spring boot·后端·war
九月十九2 小时前
AviatorScript用法
java·服务器·前端
翻晒时光2 小时前
深入解析Java集合框架:春招面试要点
java·开发语言·面试
sin22012 小时前
MyBatis-Plus的插件
java·mybatis
小丁爱养花2 小时前
Spring MVC:综合练习 - 深刻理解前后端交互过程
java·spring·mvc