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

相关推荐
儿时可乖了30 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol32 分钟前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-13141 小时前
jdk各个版本介绍
java
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
陈王卜2 小时前
django+boostrap实现发布博客权限控制
java·前端·django