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

相关推荐
w_zero_one5 分钟前
链表(2)
java·数据结构·链表
白杨尚青13 分钟前
C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)
java·开发语言·c++
SimonKing23 分钟前
SSE、WebSocket 连接丢 Redis 里?那可踩大坑了!
java·后端·程序员
倔强的石头10628 分钟前
【Linux指南】动静态库系列(九):动态库如何进入进程地址空间:从磁盘 .so 到共享内存映射
java·linux·服务器
TeamDev34 分钟前
JxBrowser 9.5.2 版本发布啦!
java·前端·windows·chromium·混合应用·jxbrowser·浏览器控件
鬼手点金36 分钟前
Claude Code示范案例-进阶学习路径
java·服务器·前端·学习·计算机视觉·前向传播
浪子明X38 分钟前
用 CMake Presets 固定跨平台构建:从本地开发到 CI 的依赖一致性实践
java·spring·ci/cd
苏supper1 小时前
记一次Nacos鉴权报错unknown user,403排查|SecurityProxy源码,fastjson2扩展包缺失
java·后端
\光辉岁月/1 小时前
6.mybatisplus学习-BaseMapper、Service、常用注解
java·mybatis·mybatisplus
微信开发api1 小时前
基于WTAPI构建社群运营平台:自动拉群与会话承接链路设计
java·大数据·网络·数据库·微信·自动化