MyBatis-Plus BaseMapper 实现原理

原文:MyBatis-Plus 的 BaseMapper 实现原理

MyBatis-Plus 自定义通用 Mapper 方法

MyBatis-Plus 提供了一些通用的 Mapper 方法,例如insertupdateselectById等。通过让自定义的 Mapper 继承BaseMapper类,我们可以直接调用这些基础的 SQL 方法,而无需自己编写 SQL。

public interface BaseMapper<T> extends Mapper<T> {}

然而,在使用过程中,我们发现提供的方法数量有限。当我们想添加自定义的通用 SQL 方法时,可以参考官方文档中描述的 SQL 注入器。例如,我们可以自定义一个saveBatch方法,用于批量插入数据。

BaseMapper 自定义扩展

MyBatis-Plus 提供了ISqlInjector接口和AbstractSqlInjector抽象类。我们可以通过实现该接口或继承抽象类的方式,注入自定义的 SQL 逻辑。

除了这两个接口外,MyBatis-Plus 还提供了一个默认实现:DefaultSqlInjector。该类中已经包含了一些 MyBatis-Plus 封装的BaseMapper方法。如果我们想进行扩展,可以直接继承这个类并添加自定义的方法。

下面我们在BaseMapper外添加的saveBatch方法,用于批量插入数据:

  1. 继承DefaultSqlInjector类,覆盖getMethodList方法。该方法的参数是 Mapper 接口的 Class 类,返回值是List<AbstractMethod>。我们自定义的方法需要实现AbstractMethod。可以参考 MyBatis-Plus 中已实现的一些AbstractMethod方法,仿照编写一个SaveBatch类。

    public class CustomSqlInjector extends DefaultSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass) {// 父类的 list 已经包含了 BaseMapper 的基础方法。List<AbstractMethod> methodList = super.getMethodList(mapperClass);// 添加我们需要增加的自定义方法。methodList.add(new SaveBatch());return methodList;}
    }
    
  2. 实现SaveBatch类的逻辑(以下为官方示例)。该逻辑主要用于生成MappedStatement对象

    public class SaveBatch extends AbstractMethod {@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {final String sql = "<script>insert into %s %s values %s</script>";final String fieldSql = prepareFieldSql(tableInfo);final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo);final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);return this.addInsertMappedStatement(mapperClass, modelClass, "saveBatch", sqlSource, new NoKeyGenerator(), null, null);}private String prepareFieldSql(TableInfo tableInfo) {StringBuilder fieldSql = new StringBuilder();fieldSql.append(tableInfo.getKeyColumn()).append(",");tableInfo.getFieldList().forEach(x -> {fieldSql.append(x.getColumn()).append(",");});fieldSql.delete(fieldSql.length() - 1, fieldSql.length());fieldSql.insert(0, "(");fieldSql.append(")");return fieldSql.toString();}private String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) {final StringBuilder valueSql = new StringBuilder();valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));valueSql.delete(valueSql.length() - 1, valueSql.length());valueSql.append("</foreach>");return valueSql.toString();}
    }
    

    注意其中的injectMappedStatement方法返回MappedStatement对象,且方法内部通过调用了父类的addInsertMappedStatement方法构建MappedStatement实例。

  3. 最后,我们需要将自定义的 Injector 注入 Spring 容器中,以替换默认的 Injector

    @Bean
    public CustomSqlInjector customSqlInjector() {return new CustomSqlInjector();
    }
    
  4. 验证

    public interface MyBaseMapper<T> extends BaseMapper<T> {int saveBatch(List<T> entityList);
    }@Mapper
    public interface TB3Mapper extends MyBaseMapper<Tb3> {
    }@Test
    public void test() {List<Tb3> tb3s = Arrays.asList(Tb3.getInstance(), Tb3.getInstance());tb3Mapper.saveBatch(tb3s);
    }
    
    // 输出日志
    ==>  Preparing: insert into tb3 (id,f1,f2,f3) values ( ?,?,?,? ),( ?,?,?,? )
    ==> Parameters: 38(Integer), 62(Integer), -1546785812(Integer), -16950756(Integer), 24(Integer), 17(Integer), -1871764773(Integer), 169785869(Integer)
    <==    Updates: 2
    

原理解析

MyBatis-Plus 的工作原理是全面代理了 MyBatis 的一些功能。例如,自动配置转用了MyBatisPlusAutoConfigurationSqlSessionFactoryBean转用了MyBatisSqlSessionFactoryBean等等。这些 MyBatis 的核心部件都被 MyBatis-Plus 替换,并在其内部定制了逻辑。

要了解 MyBatis-Plus 的工作原理,需要了解 MyBatis 的工作原理。MyBatis 的整体逻辑可以分为两部分:

  1. 配置文件解析:这个过程包括解析 MyBatis 配置,以及mapper.xml文件。最终配置都会被解析到一个Configuration对象里面,后面的每个SqlSession也都会包含一个该Configuration对象实例的引用。这个Configuration里面有两个最重要的部分:

    img

    • mappedStatements:存放 mapper 对应的 SQL 信息
    • mybatisMapperRegistry.knownMappers:存放 mapper 接口对应的代理类
  2. 接口的调用:我们接口调用的其实是代理的包装类MybatisMapperProxy,这个类由上图mybatisMapperRegistry.knownMappers里面展示的MybatisMapperProxyFactory(MyBatis 是MapperProxyFactory)的getObject方法返回。这个代理类里面的主要逻辑就是去ConfigurationmappedStatements里面找到对应的 SQL 然后执行。

可以猜到:在Configuration加载的时候,一定有地方将BaseMapper的默认方法对应的 SQL 信息给装载到mappedStatements这个 map 里面去。下面分析这些默认的MappedStatement对象是在哪里进行构建并加入到Configuration中的。

第一步,肯定是自动配置要注入SqlSessionFactory到容器,其通过MybatisSqlSessionFactoryBean对象的getObject方法返回,我们跟进MybatisSqlSessionFactoryBean.getObject()

@Override
public SqlSessionFactory getObject() throws Exception {if (this.sqlSessionFactory == null) {afterPropertiesSet();}return this.sqlSessionFactory;
}@Override
public void afterPropertiesSet() throws Exception {notNull(dataSource, "Property 'dataSource' is required");state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");// 这里才是开始构建 SqlSessionFactory 的this.sqlSessionFactory = buildSqlSessionFactory();
}

可以看到,最终会执行到buildSqlSessionFactory()。这块方法的主要逻辑就是解析 XML 配置来创建Configuration对象。我们可以在最下面发现解析我们mapper.xml文件的逻辑:

if (this.mapperLocations != null) {if (this.mapperLocations.length == 0) {LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");} else {for (Resource mapperLocation : this.mapperLocations) {if (mapperLocation == null) {continue;}try {// 对每一个 mapper.xml 文件进行解析XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());xmlMapperBuilder.parse();} catch (Exception e) {throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);} finally {ErrorContext.instance().reset();}LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");}}
} else {LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}

重点看看xmlMapperBuilder.parse();

public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode("/mapper"));configuration.addLoadedResource(resource);// debug 发现,Configuration 中 mappedStatements 在执行该方法之后,mapper 方法数量就变多了。bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();
}

bindMapperForNamespace里面,是在执行configuration.addMapper(boundType); 之后方法变多的。这个方法最终调用的是MybatisMapperRegistry.addMapper(),这个方法里面最终会转去调用MybatisMapperAnnotationBuilder.parse()方法,将 mapper 的方法加入到mappedStatements中。

@Override
public void parse() {// ...try {if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {// 执行该步骤之后,新增了 mappestatmentparserInjector();}} catch (IncompleteElementException e) {configuration.addIncompleteMethod(new InjectorResolver(this));}parsePendingMethods();
}

parserInjector方法如下:

void parserInjector() {GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}// GlobalConfigUtils.getSqlInjector
public static ISqlInjector getSqlInjector(Configuration configuration) {return getGlobalConfig(configuration).getSqlInjector();
}// getSqlInjector()
private ISqlInjector sqlInjector = new DefaultSqlInjector();
// MybatisPlusAutoConfiguration.sqlSessionFactory#sqlInjector
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);

可以看到,通过一连串的方法拿到ISqlInjector实现类。默认是DefaultSqlInjector,但是如果 Spring 中被自行注入了该实现类的话,就会在自动配置的时候,修改为我们自定义的SqlInjector(比如前面的CustomSqlInjector)。

获取到SqlInjector之后,调用其inspectInject方法,CustomSqlInjector继承自DefaultSqlInjectorDefaultSqlInjector继承自AbstractSqlInjector,其中有inspectInject方法。

// DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {return Stream.of(new Insert(),new Delete(),// ....).collect(toList());
}// AbstractSqlInjector
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {Class<?> modelClass = extractModelClass(mapperClass);if (modelClass != null) {String className = mapperClass.toString();Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());if (!mapperRegistryCache.contains(className)) {// 可以看到这里拿取我们 CustomSqlInjector 返回的 AbstractMethod list,然后循环调用 injectList<AbstractMethod> methodList = this.getMethodList(mapperClass);if (CollectionUtils.isNotEmpty(methodList)) {TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);// 循环注入自定义方法methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));} else {logger.debug(mapperClass.toString() + ", No effective injection method was found.");}mapperRegistryCache.add(className);}}
}// AbstractMethod
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {this.configuration = builderAssistant.getConfiguration();this.builderAssistant = builderAssistant;this.languageDriver = configuration.getDefaultScriptingLanguageInstance();/* 注入自定义方法 */injectMappedStatement(mapperClass, modelClass, tableInfo);
}

可以看到inspectInject方法调用了getMethodList方法,然后循环getMethodList返回的AbstractMethod集合,调用各项的inject方法。

AbstractMethodinject最终会调用injectMappedStatement方法,该方法是抽象方法,由子类实现。

比如SaveBatchinjectMappedStatement方法在构建好一个MappedStatement对象需要的元素后,调用AbstractMethod中的addInsertMappedStatement将其加入到ConfigurationmappedStatements中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862128.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024 年终总结

2024年终总结昨天英语听力考试时又享受了它的轻松明快,于是年终总结每个部分之间都会有它五线谱的一小节?猜出来这是什么的可以分享在评论区! 遵循一个“写不出来可以不写”的原则,咱少写一些。 这一年几乎所有的学术精力都投入给了大模型。组里做的是 LLM agent,在 Tence…

Everything(文件快速搜索工具)v1.4.1.1026

Everything是速度最快的文件搜索软件,可以瞬间搜索到你需要的文件。如果你用过Windows自Everything是速度最快的文件搜索软件,可以瞬间搜索到你需要的文件。如果你用过Windows自带的搜索工具、Total Commander的搜索、Google 桌面搜索或百度硬盘搜索,都因为速度或其他原因而…

Anyviewer(傲梅免费远程桌面软件) v4.6.0

AnyViewer是一款免费、安全、快速的远程桌面控制软件,可以通过AnyViewer访问你家里、办公室电脑,实现其远程操作;可以帮您远程办公、玩游戏、登录云桌面,以及远程技术支持等。 支持的操作系统 Microsoft Windows 7(所有版本,32 位和 64 位) Microsoft Windows 8.1(所有…

Escrcpy(手机投屏) v1.28.3 便携版

Escrcpy 是一款强大的工具,它允许用户通过图形化的 Scrcpy 界面来显示和控制他们的 Android 设备。这款应用程序由 Electron 作为其底层框架驱动。Escrcpy 无需任何账户就可以使用,无需担心隐私或安全问题。Escrcpy没有广告,完全免费开源。 软件特色 同步:得益于 Web 技术,…

【THM】Tor(Tor网络使用简介)-学习

面向初学者的 Tor 网络使用指南本文相关的TryHackMe实验房间链接:https://tryhackme.com/r/room/torforbeginners 本文相关内容:面向初学者的 Tor 网络使用指南。Tor介绍 Tor是一款免费的开源软件,可用于实现匿名通信。Tor通过一个免费的全球志愿者覆盖网络引导互联网流量,…

6.定位

6.1相对定位position:relative; top.left.right.bottom四个方向均可以设置偏移值 相对定位的话,它仍然在标准文档流中,原来的位置会被保留。 6.2绝对定位 定位:基于xxx定位,上下左右 1.没有父级元素定位的前提下,相对于浏览器定位。 2.假设父级元素存在定位,我们通常会相…

JVM实战—6.频繁YGC和频繁FGC的后果

大纲 1.JVM GC导致系统突然卡死无法访问 2.什么是Young GC什么是Full GC 3.Young GC、Old GC和Full GC的发生情况 4.频繁YGC的案例(G1解决大内存YGC过慢) 5.频繁FGC的案例(YGC存活对象S区放不下) 6.问题汇总1.JVM GC导致系统突然卡死无法访问 (1)基于JVM运行的系统最怕什么 (2)…

面向过程 面向对象 回顾方法 20241231

面向过程 & 面向对象 20241231 面向过程思想 步骤清晰简单,第一步做什么,第二步做什么.….面对过程适合处理一些较为简单的问题 (线性思维) 面向对象思想 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分…

低空经济新动力:无人机航测技术的普及与应用

在低空经济的快速发展背景下,航空测绘(简称航测)技术的应用日益广泛,它为城市规划、农业监测、环境评估等领域提供了重要支撑。随着技术的进步和成本的降低,航测技术正逐渐从专业领域走向平民化,这不仅为低空经济的发展带来了新的机遇,也提出了新的挑战。 航测技术的发展…

Javaer开发环境的搭建(个人喜好向)

前言 我们进入到新的工作环境,或者电脑重新安装系统后,总要重新配置开发环境,这时候需要四处搜来搜去的就很麻烦了,所以在此做一个笔记 文件编辑器 个人习惯使用 Visual Studio Code 大文件查看(比如看1GB以上的日志文件)可以使用 EmEditor Java JDK下载 个人使用更习惯O…

什么是聚合根和聚合

实体和值对象组成聚合,再根据业务,将多个聚合划定到同一限界上下文,并在限界上下文内完成领域建模。 聚合只是单纯将一些共享父类、密切关联的对象聚集成一个对象树吗?如果是这样,对于存在于这个树中的对象,有没有一个实用的数目限制? 既然一个聚合可以引用另一个聚合,…

【Java 温故而知新系列】基础知识-03 基本类型对应之包装类

1、包装类都有哪些? 基本类型都有对应的包装类型,这些包装类提供了一种面向对象的方式来处理基本数据类型,允许它们被用于需要对象的场景,如集合框架、泛型等。 对应关系:基本类型包装类型boolean Booleanbyte Bytechar Characterfloat Floatint Integerlong Longshort Sh…