mybatis-plus 是完全基于 mybatis 开发的一个增强工具,它的设计理念是在 mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生,它在 mybatis 的基础上增加了很多实用性的功能,比如增加了乐观锁插件、字段自动填充功能、分页插件、条件构造器、sql 注入器等等,这些在开发过程中都是非常实用的功能,mybatis-plus 可谓是站在巨人的肩膀上进行了一系列的创新,我个人极力推荐。下面我会详细地从源码的角度分析 mybatis-plus(下文简写成 mp)是如何实现 sql 自动注入的原理。
温故知新
我们回顾一下 mybatis 的 Mapper 的注册与绑定过程,我之前也写过一篇「Mybatis 源码分析之 Mapper 注册与绑定」,在这篇文章中,我详细地讲解了 Mapper 绑定的最终目的是将 xml 或者注解上的 sql 信息与其对应 Mapper 类注册到 MappedStatement 中,既然 mybatis-plus 的设计理念是在 mybatis 的基础上只做增强不做改变,那么 sql 注入器必然也是在将我们预先定义好的 sql 和预先定义好的 Mapper 注册到 MappedStatement 中。
现在我将 Mapper 的注册与绑定过程用时序图再梳理一遍:
解析一下这几个类的作用:
SqlSessionFactoryBean:继承了 FactoryBean 和 InitializingBean,符合 spring loc 容器 bean 的基本规范,可在获取该 bean 时调用 getObject() 方法到 SqlSessionFactory。
XMLMapperBuilder:xml 文件解析器,解析 Mapper 对应的 xml 文件信息,并将 xml 文件信息注册到 Configuration 中。
XMLStatementBuilder:xml 节点解析器,用于构建 select/insert/update/delete 节点信息。
MapperBuilderAssistant:Mapper 构建助手,将 Mapper 节点信息封装成 statement 添加到 MappedStatement 中。
MapperRegistry:Mapper 注册与绑定类,将 Mapper 的类信息与 MapperProxyFactory 绑定。
MapperAnnotationBuilder:Mapper 注解解析构建器,这也是为什么 mybatis 可以直接在 Mapper 方法添加注解信息就可以不用在 xml 写 sql 信息的原因,这个构建器专门用于解析 Mapper 方法注解信息,并将这些信息封装成 statement 添加到 MappedStatement 中。
从时序图可知,Configuration 配置类存储了所有 Mapper 注册与绑定的信息,然后创建 SqlSessionFactory 时再将 Configuration 注入进去,最后经过 SqlSessionFactory 创建出来的 SqlSession 会话,就可以根据 Configuration 信息进行数据库交互,而 MapperProxyFactory 会为每个 Mapper 创建一个 MapperProxy 代理类,MapperProxy 包含了 Mapper 操作 SqlSession 所有的细节,因此我们就可以直接使用 Mapper 的方法就可以跟 SqlSession 进行交互。
饶了一圈,发现我现在还没讲 sql 注入器的源码分析,你不用慌,你得体现出老司机的成熟稳定,之前我也跟你说了 sql 注入器的原理了,只剩下源码分析,这时候我们应该在源码分析之前做足前戏,前戏做足就剩下撕、拉、扯、剥开源码的外衣了,来不及解释了快上车!
源码分析
从 Mapper 的注册与绑定过程的时序图看,要想将 sql 注入器无缝链接地添加到 mybatis 里面,那就得从 Mapper 注册步骤添加,果然,mp 很鸡贼地继承了 MapperRegistry 这个类然后重写了 addMapper 方法:
com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// throw new BindingException(“Type ” + type +
// ” is already known to the MybatisPlusMapperRegistry.”);
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It’s important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won’t try.
// TODO 自定义无 XML 注入
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
方法中将 MapperAnnotationBuilder 替换成了自家的 MybatisMapperAnnotationBuilder,在这里特别说明一下,mp 为了不更改 mybatis 原有的逻辑,会用继承或者直接粗暴地将其复制过来,然后在原有的类名上加上前缀“Mybatis”。
com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
// TODO 注入 CURD 动态 SQL (应该在注解之前注入)
if (BaseMapper.class.isAssignableFrom(type)) {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
sql 注入器就是从这个方法里面添加上去的,首先判断 Mapper 是否是 BaseMapper 的超类或者超接口,BaseMapper 是 mp 的基础 Mapper,里面定义了很多默认的基础方法,意味着我们一旦使用上 mp,通过 sql 注入器,很多基础的数据库操作都可以直接继承 BaseMapper 实现了,开发效率爆棚有木有!
com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:
public static ISqlInjector getSqlInjector(Configuration configuration) {
// fix #140
GlobalConfiguration globalConfiguration = getGlobalConfig(configuration);
ISqlInjector sqlInjector = globalConfiguration.getSqlInjector();
if (sqlInjector == null) {
sqlInjector = new AutoSqlInjector();
globalConfiguration.setSqlInjector(sqlInjector);
}
return sqlInjector;
}
GlobalConfiguration 是 mp 的全局缓存类,用于存放 mp 自带的一些功能,很明显,sql 注入器就存放在 GlobalConfiguration 中。
这个方法是先从全局缓存类中获取自定义的 sql 注入器,如果在 GlobalConfiguration 中没有找到自定义 sql 注入器,就会设置一个 mp 默认的 sql 注入器 AutoSqlInjector。
sql 注入器接口:
// SQL 自动注入器接口
public interface ISqlInjector {
// 根据 mapperClass 注入 SQL
void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
// 检查 SQL 是否注入 (已经注入过不再注入)
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
// 注入 SqlRunner 相关
void injectSqlRunner(Configuration configuration);
}
所有自定义的 sql 注入器都需要实现 ISqlInjector 接口,mp 已经为我们默认实现了一些基础的注入器:
com.baomidou.mybatisplus.mapper.AutoSqlInjector
com.baomidou.mybatisplus.mapper.LogicSqlInjector
其中 AutoSqlInjector 提供了最基本的 sql 注入,以及一些通用的 sql 注入与拼装的逻辑,LogicSqlInjector 在 AutoSqlInjector 的基础上复写了删除逻辑,因为我们的数据库的数据删除实质上是软删除,并不是真正的删除。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
inject(builderAssistant, mapperClass);
mapperRegistryCache.add(className);
}
}
该方法是 sql 注入器的入口,在入口处添加了注入过后不再注入的判断功能。
// 注入单点 crudSql
@Override
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
// 驼峰设置 PLUS 配置 > 原始配置
GlobalConfiguration globalCache = this.getGlobalConfig();
if (!globalCache.isDbColumnUnderline()) {
globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase());
}
Class<?> modelClass = extractModelClass(mapperClass);
if (null != modelClass) {
// 初始化 SQL 解析
if (globalCache.isSqlParserCache()) {
PluginUtils.initSqlParserInfoCache(mapperClass);
}
TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
injectSql(builderAssistant, mapperClass, modelClass, table);
}
}
注入之前先将 Mapper 类提取泛型模型,因为继承 BaseMapper 需要将 Mapper 对应的 model 添加到泛型里面,这时候我们需要将其提取出来,提取出来后还需要将其初始化成一个 TableInfo 对象,TableInfo 存储了数据库对应的 model 所有的信息,包括表主键 ID 类型、表名称、表字段信息列表等等信息,这些信息通过反射获取。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:
protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
if (StringUtils.isNotEmpty(table.getKeyProperty())) {
/** 删除 */
this.injectDeleteByIdSql(false, mapperClass, modelClass, table);
/** 修改 */
this.injectUpdateByIdSql(true, mapperClass, modelClass, table);
/** 查询 */
this.injectSelectByIdSql(false, mapperClass, modelClass, table);
}
/** 自定义方法 */
this.inject(configuration, builderAssistant, mapperClass, modelClass, table);
}
所有需要注入的 sql 都是通过该方法进行调用,AutoSqlInjector 还提供了一个 inject 方法,自定义 sql 注入器时,继承 AutoSqlInjector,实现该方法就行了。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:
protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
SqlSource sqlSource;
if (batch) {
sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS;
StringBuilder ids = new StringBuilder();
ids.append(“\n<foreach item=\”item\” index=\”index\” collection=\”coll\” separator=\”,\”>”);
ids.append(“#{item}”);
ids.append(“\n</foreach>”);
sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(),
sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass);
} else {
sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false),
table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class);
}
this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table);
}
我随机选择一个删除 sql 的注入,其它 sql 注入都是类似这么写,SqlMethod 是一个枚举类,里面存储了所有自动注入的 sql 与方法名,如果是批量操作,SqlMethod 的定义的 sql 语句在添加批量操作的语句。再根据 table 和 sql 信息创建一个 SqlSource 对象。
com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:
public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType,
KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
// MappedStatement 是否存在
String statementName = mapperClass.getName() + “.” + id;
if (hasMappedStatement(statementName)) {
System.err.println(“{” + statementName
+ “} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.”);
return null;
}
/** 缓存逻辑处理 */
boolean isSelect = false;
if (sqlCommandType == SqlCommandType.SELECT) {
isSelect = true;
}
return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null,
parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
configuration.getDatabaseId(), languageDriver, null);
}
sql 注入器的最终操作,这里会判断 MappedStatement 是否存在,这个判断是有原因的,它会防止重复注入,如果你的 Mapper 方法已经在 Mybatis 的逻辑里面注册了,mp 不会再次注入。最后调用 MapperBuilderAssistant 助手类的 addMappedStatement 方法执行注册操作。
到这里,一个 sql 自动注入器的源码就分析完了,其实实现起来很简单,因为它利用了 Mybatis 的机制,站在巨人的肩膀上进行创新。
我希望在你们今后的职业生涯里,不要只做一个只会调用 API 的 crud 程序员,我们要有一种刨根问底的精神。阅读源码很枯燥,但阅读源码不仅会让你知道 API 底层的实现原理,让你知其然也知其所以然,还可以开阔你的思维,提升你的架构设计能力,通过阅读源码,可以看到大佬们是如何设计一个框架的,为什么会这么设计。