背景
在项目中需要同时操作Sql Server 以及 MySQL 数据库,可能平时直接使用 BaseMapper
中提供的方法习惯 了,不用的话总感觉影响开发效率,但是两个数据库的SQL语法稍微有点差别,有些暴露的方法并不能直接使用,所以便想提前注入相关SQL。(果然偷懒是第一生产力!!!)
方案
那怎么解决这种问题呢?理论上这种成熟的框架应该会提供相关的解决方案,所以我当场去翻Mybatis
的官方文档,果然找到一个SQL注入器功能。
SQL注入器从文档中来看好像确实看不出来是什么…其实也不难理解,就是将自定义的方法注入到MP中,这样便可以直接调用。不要问我为什么不直接写到XML文件里,都说了我想偷懒,每个XML文件里面都写一遍,不累的吗?
在这里还是要吐槽下Mybatis
的官方文档是真的简洁 ~~~ 还好提供了一个完整案例。
SQL注入器案例
SQL注入器简单使用
看完官方案例后,我们可以将步骤分为以下几步:
- 自定义方法类:自定义SQL模板
- 编写注册类:注册自定义方法类
- 定义继承类:该类需要继承
BaseMapper
,同时写入自定义方法 - 将注册类交给Spring容器管理(或者在全局配置中处理)
- Mapper层继承自定义继承类即可使用新方法
自定义方法类
该类的主要作用就是自定义SQL模板,我们可以创建多个注册到MP中。
首先需要继承AbstractMethod
,然后重写injectMappedStatement
方法,这里的SQL模板我是直接使用的SELECT_BY_ID
方法的SQL模板,只不过在后面加了一个limit 1
,虽然实际上并没有啥效果,但是就是一个简单测试,只要控制台能将其输出就说明我成功了(手动狗头)。
public class FindOneById extends AbstractMethod {
public FindOneById() {
this("findOneById");
}
public FindOneById(String methodName) {
super(methodName);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlSource sqlSource = new RawSqlSource(configuration, String.format("SELECT %s FROM %s WHERE %s=#{%s} limit 1",
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty()), Object.class);
return this.addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, tableInfo);
}
}
注册类
接下来我们编写一个注入类,将自定义方法注入到MP中。这里我们直接继承DefaultSqlInjector
类,当然你也可以继承AbstractSqlInjector
类,两者的区别在于AbstractSqlInjector
更加灵活,能直接将SQL注入到MP中;DefaultSqlInjector
中则已经实现了一些常用的SQL操作。
以下代码我们可以看到获取DefaultSqlInjector
封装好的方法列表,然后将我们自定义的方法添加进去即可,同时我这里已经通过Component
注解将该注册类交给容器管理了。
@Component // 交给容器管理
public class Inject extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
// 自定义
methodList.add(new FindOneById());
return methodList;
}
}
Mapper
想要使用自定义方法,我们还需要自定义一个Mapper类供外部继承,在其中写入我们的自定义方法。继承BaseMapper
的原因在于能继续使用原本提供的方法。
public interface MyBaseMapper<T> extends BaseMapper<T> {
T findOneById(Serializable id);
}
测试
上述步骤都完成后就可以进行测试了。首先编写我们的业务Mapper层,继承我们自定义的MyBaseMapper
;
@Repository //持久层注解,表示该类交给Springboot管理
public interface UserMapper extends MyBaseMapper<User> {
}
然后编写测试类查看效果:
@SpringBootTest
class SpringbootMybatisInjectorApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
User user = userMapper.findOneById(1);
System.out.println(user);
}
}
由此可见我们的自定义SQL注入成功,至此大功告成。
深度剖析
你以为就这么结束了?都看了这篇文章了,还不直接把SQL注入器彻底搞懂?
SQL是如何注入的?
那么SQL到底是如何注入的呢?我们先来看SQL注入器的顶级接口ISqlInjector
,怎么知道它是顶级接口的?在万能的IDEA里面直接通过我们自定义的注入器查看其UML图就知道了;
或者我们可以直接查看MP的源码,在injector
目录下:
如果这个时候你查看了methods
目录下的内容,就会发现其实这些内容就等同于我们自定义的SQL方法,这些都是MP帮我们实现好的内置SQL方法。
回归正题,那么ISqlInjector
的作用是什么呢?它只做一件事,那就是检查SQL是否注入,已经注入过则不再注入。
public interface ISqlInjector {
// 检查SQL是否注入,已经注入过则不再注入。
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
按照UML图,我们接下来应该看其下一层抽象类AbstractSqlInjector
。该类主要为两个方法:
-
inspectInject
:实现顶层接口方法,分为以下几个步骤:- 通过反射获取实体类对象
- 获取
mapperRegistry
缓存用于对比,防止覆盖 - 封装
TableInfo
存储表信息对象 - 循环注入自定义方法
-
getMethodList
:获取注入的方法,为相关实现类提供的模板方法;
public abstract class AbstractSqlInjector implements ISqlInjector {
protected final Log logger = LogFactory.getLog(this.getClass());
public AbstractSqlInjector() {
}
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
// 通过反射获取实体类对象
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
if (modelClass != null) {
String className = mapperClass.toString();
//获取mapperRegistry缓存用于对比,防止覆盖
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
// 封装TableInfo 存储表信息对象
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 {
this.logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
// 获取 注入的方法
public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo);
}
最后我们来看DefaultSqlInjector
,作为MP的默认SQL注入器,它又做了些什么呢?
public class DefaultSqlInjector extends AbstractSqlInjector {
public DefaultSqlInjector() {
}
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Builder<AbstractMethod> builder = Stream.builder().add(new Insert()).add(new Delete()).add(new DeleteByMap()).add(new Update()).add(new SelectByMap()).add(new SelectCount()).add(new SelectMaps()).add(new SelectMapsPage()).add(new SelectObjs()).add(new SelectList()).add(new SelectPage());
if (tableInfo.havePK()) {
builder.add(new DeleteById()).add(new DeleteBatchByIds()).add(new UpdateById()).add(new SelectById()).add(new SelectBatchByIds());
} else {
this.logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType()));
}
return (List)builder.build().collect(Collectors.toList());
}
}
从代码可以很清晰的看出,DefaultSqlInjector
实现了AbstractSqlInjector
抽象类的
getMethodList
方法,在这里将MP默认实现的CRUD方法进行注入。
虽然上面说了一堆,但是我相信你还是没懂,不如一起debug来看看:这里主要看下数据库信息的获取以及方法的注入;
获取数据库信息
实体类如下:
首先我们可以看到TableInfoHelper.initTableInfo
方法获取到了数据库表的相关信息,那么它是如何做到的呢?我们进入刚方法内查看:
通过查看代码,我们可以看到初始化数据库信息的方法主要为initTableName
以及initTableFields
;
initTableName
的作用是获取表名信息,主要通过实体上的@TableName
注解拿到表名,所以我们这里拿到了User
类名。
initTableFields
的作用主要为获取主键及其他字段信息:
至此,数据库信息则获取成功,可供注入SQL方法时使用。
方法注入
获取完表信息后,可以发现通过getMethodList
方法获取了所有自定义方法,这些方法都是哪里来的呢?都是从AbstractSqlInjector
抽象类的子类中获取的,比如默认的SQL注入器DefaultSqlInjector
以及我们自定的Inject
。
获取所有自定义方法后,可以发现通过调用AbstractMethod
的inject
方法实现了SQL的自动注入,这里也把上文获取到的数据库表对象传入用来进行SQL的封装。
injectMappedStatement
方法则需要每个SQL类根据各自需求重写,最后将生成好的MappedStatement
对象加入到全局配置类对象中。
SQL 语句是怎么生成的?
AbstractMethod
上面了解了SQL是如何注入后,我们再来看下SQL是怎么生成的。我们直接看DefaultSqlInjector
提供的默认方法,可以发现所有的方法都继承了AbstractMethod
。该类主要用于封装Mapper
接口中定义的方法信息,并提供了一些默认实现。通过继承 AbstractMethod
类并重写其中的方法,我们可以自定义生成 SQL 语句的方式,从而实现更加灵活的 SQL 操作。
该类我们目前只需要关注inject
方法,它主要通过injectMappedStatement
方法实现了自动注入SQL的动作。injectMappedStatement
是一个模板方法,每个自定义SQL类都可以对其进行重写,然后将封装好的sql存放到全局配置文件类中。
/**
* 抽象的注入方法类
*/
public abstract class AbstractMethod implements Constants {
protected static final Log logger = LogFactory.getLog(AbstractMethod.class);
protected Configuration configuration;
protected LanguageDriver languageDriver;
protected MapperBuilderAssistant builderAssistant;
/**
* 方法名称
*/
protected final String methodName;
/**
* @param methodName 方法名
*/
protected AbstractMethod(String methodName) {
Assert.notNull(methodName, "方法名不能为空");
this.methodName = methodName;
}
/**
* 注入自定义方法
*/
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);
}
/**
* 是否已经存在MappedStatement
*
* @param mappedStatement MappedStatement
* @return true or false
*/
private boolean hasMappedStatement(String mappedStatement) {
return configuration.hasStatement(mappedStatement, false);
}
/**
* SQL 更新 set 语句
*
* @param table 表信息
* @return sql set 片段
*/
protected String sqlLogicSet(TableInfo table) {
return "SET " + table.getLogicDeleteSql(false, false);
}
/**
* SQL 更新 set 语句
*
* @param logic 是否逻辑删除注入器
* @param ew 是否存在 UpdateWrapper 条件
* @param table 表信息
* @param alias 别名
* @param prefix 前缀
* @return sql
*/
protected String sqlSet(boolean logic, boolean ew, TableInfo table, boolean judgeAliasNull, final String alias,
final String prefix) {
String sqlScript = table.getAllSqlSet(logic, prefix);
if (judgeAliasNull) {
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", alias), true);
}
if (ew) {
sqlScript += NEWLINE;
sqlScript += convertIfEwParam(U_WRAPPER_SQL_SET, false);
}
sqlScript = SqlScriptUtils.convertSet(sqlScript);
return sqlScript;
}
/**
* SQL 注释
*
* @return sql
*/
protected String sqlComment() {
return convertIfEwParam(Q_WRAPPER_SQL_COMMENT, true);
}
protected String sqlFirst() {
return convertIfEwParam(Q_WRAPPER_SQL_FIRST, true);
}
protected String convertIfEwParam(final String param, final boolean newLine) {
return SqlScriptUtils.convertIf(SqlScriptUtils.unSafeParam(param),
String.format("%s != null and %s != null", WRAPPER, param), newLine);
}
/**
* SQL 查询所有表字段
*
* @param table 表信息
* @param queryWrapper 是否为使用 queryWrapper 查询
* @return sql 脚本
*/
protected String sqlSelectColumns(TableInfo table, boolean queryWrapper) {
/* 假设存在用户自定义的 resultMap 映射返回 */
String selectColumns = ASTERISK;
if (table.getResultMap() == null || table.isAutoInitResultMap()) {
/* 未设置 resultMap 或者 resultMap 是自动构建的,视为属于mp的规则范围内 */
selectColumns = table.getAllSqlSelect();
}
if (!queryWrapper) {
return selectColumns;
}
return convertChooseEwSelect(selectColumns);
}
/**
* SQL 查询记录行数
*
* @return count sql 脚本
*/
protected String sqlCount() {
return convertChooseEwSelect(ASTERISK);
}
/**
* SQL 设置selectObj sql select
*
* @param table 表信息
*/
protected String sqlSelectObjsColumns(TableInfo table) {
return convertChooseEwSelect(table.getAllSqlSelect());
}
protected String convertChooseEwSelect(final String otherwise) {
return SqlScriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_SELECT),
SqlScriptUtils.unSafeParam(Q_WRAPPER_SQL_SELECT), otherwise);
}
/**
* SQL map 查询条件
*/
protected String sqlWhereByMap(TableInfo table) {
if (table.isWithLogicDelete()) {
// 逻辑删除
String sqlScript = SqlScriptUtils.convertChoose("v == null", " ${k} IS NULL ",
" ${k} = #{v} ");
sqlScript = SqlScriptUtils.convertForeach(sqlScript, COLUMN_MAP, "k", "v", "AND");
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and !%s.isEmpty", COLUMN_MAP, COLUMN_MAP), true);
sqlScript += (NEWLINE + table.getLogicDeleteSql(true, true));
sqlScript = SqlScriptUtils.convertWhere(sqlScript);
return sqlScript;
} else {
String sqlScript = SqlScriptUtils.convertChoose("v == null", " ${k} IS NULL ",
" ${k} = #{v} ");
sqlScript = SqlScriptUtils.convertForeach(sqlScript, COLUMN_MAP, "k", "v", "AND");
sqlScript = SqlScriptUtils.convertWhere(sqlScript);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and !%s", COLUMN_MAP,
COLUMN_MAP_IS_EMPTY), true);
return sqlScript;
}
}
/**
* EntityWrapper方式获取select where
*
* @param newLine 是否提到下一行
* @param table 表信息
* @return String
*/
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
if (table.isWithLogicDelete()) {
String sqlScript = table.getAllSqlWhere(true, true, WRAPPER_ENTITY_DOT);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY),
true);
sqlScript += (NEWLINE + table.getLogicDeleteSql(true, true) + NEWLINE);
String normalSqlScript = SqlScriptUtils.convertIf(String.format("AND ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_NONEMPTYOFNORMAL), true);
normalSqlScript += NEWLINE;
normalSqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_EMPTYOFNORMAL), true);
sqlScript += normalSqlScript;
sqlScript = SqlScriptUtils.convertChoose(String.format("%s != null", WRAPPER), sqlScript,
table.getLogicDeleteSql(false, true));
sqlScript = SqlScriptUtils.convertWhere(sqlScript);
return newLine ? NEWLINE + sqlScript : sqlScript;
} else {
String sqlScript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
sqlScript += NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_NONEMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
WRAPPER_EMPTYOFWHERE), true);
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
return newLine ? NEWLINE + sqlScript : sqlScript;
}
}
protected String sqlOrderBy(TableInfo tableInfo) {
/* 不存在排序字段,直接返回空 */
List<TableFieldInfo> orderByFields = tableInfo.getOrderByFields();
if (CollectionUtils.isEmpty(orderByFields)) {
return StringPool.EMPTY;
}
orderByFields.sort(Comparator.comparingInt(TableFieldInfo::getOrderBySort));
StringBuilder sql = new StringBuilder();
sql.append(NEWLINE).append(" ORDER BY ");
sql.append(orderByFields.stream().map(tfi -> String.format("%s %s", tfi.getColumn(),
tfi.getOrderByType())).collect(joining(",")));
/* 当wrapper中传递了orderBy属性,@orderBy注解失效 */
return SqlScriptUtils.convertIf(sql.toString(), String.format("%s == null or %s", WRAPPER,
WRAPPER_EXPRESSION_ORDER), true);
}
/**
* 过滤 TableFieldInfo 集合, join 成字符串
*/
protected String filterTableFieldInfo(List<TableFieldInfo> fieldList, Predicate<TableFieldInfo> predicate,
Function<TableFieldInfo, String> function, String joiningVal) {
Stream<TableFieldInfo> infoStream = fieldList.stream();
if (predicate != null) {
return infoStream.filter(predicate).map(function).collect(joining(joiningVal));
}
return infoStream.map(function).collect(joining(joiningVal));
}
/**
* 获取乐观锁相关
*
* @param tableInfo 表信息
* @return String
*/
protected String optlockVersion(TableInfo tableInfo) {
if (tableInfo.isWithVersion()) {
return tableInfo.getVersionFieldInfo().getVersionOli(ENTITY, ENTITY_DOT);
}
return EMPTY;
}
/**
* 查询
*/
protected MappedStatement addSelectMappedStatementForTable(Class<?> mapperClass, String id, SqlSource sqlSource,
TableInfo table) {
String resultMap = table.getResultMap();
if (null != resultMap) {
/* 返回 resultMap 映射结果集 */
return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null,
resultMap, null, NoKeyGenerator.INSTANCE, null, null);
} else {
/* 普通查询 */
return addSelectMappedStatementForOther(mapperClass, id, sqlSource, table.getEntityType());
}
}
/**
* 查询
*/
protected MappedStatement addSelectMappedStatementForTable(Class<?> mapperClass, SqlSource sqlSource, TableInfo table) {
return addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, table);
}
/**
* 查询
*/
protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, String id, SqlSource sqlSource,
Class<?> resultType) {
return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null,
null, resultType, NoKeyGenerator.INSTANCE, null, null);
}
/**
* 查询
*/
protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, SqlSource sqlSource, Class<?> resultType) {
return addSelectMappedStatementForOther(mapperClass, this.methodName, sqlSource, resultType);
}
/**
* 插入
*/
protected MappedStatement addInsertMappedStatement(Class<?> mapperClass, Class<?> parameterType, String id,
SqlSource sqlSource, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.INSERT, parameterType, null,
Integer.class, keyGenerator, keyProperty, keyColumn);
}
/**
* 插入
*/
protected MappedStatement addInsertMappedStatement(Class<?> mapperClass, Class<?> parameterType,
SqlSource sqlSource, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
return addInsertMappedStatement(mapperClass, parameterType, this.methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
}
/**
* 删除
*/
protected MappedStatement addDeleteMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource) {
return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.DELETE, null,
null, Integer.class, NoKeyGenerator.INSTANCE, null, null);
}
protected MappedStatement addDeleteMappedStatement(Class<?> mapperClass, SqlSource sqlSource) {
return addDeleteMappedStatement(mapperClass, this.methodName, sqlSource);
}
/**
* 更新
*/
protected MappedStatement addUpdateMappedStatement(Class<?> mapperClass, Class<?> parameterType, String id,
SqlSource sqlSource) {
return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.UPDATE, parameterType, null,
Integer.class, NoKeyGenerator.INSTANCE, null, null);
}
protected MappedStatement addUpdateMappedStatement(Class<?> mapperClass, Class<?> parameterType,
SqlSource sqlSource) {
return addUpdateMappedStatement(mapperClass, parameterType, this.methodName, sqlSource);
}
/**
* 添加 MappedStatement 到 Mybatis 容器
*/
protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class<?> parameterType,
String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
String statementName = mapperClass.getName() + DOT + id;
if (hasMappedStatement(statementName)) {
logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
return null;
}
/* 缓存逻辑处理 */
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType,
null, null, null, parameterType, resultMap, resultType,
null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
configuration.getDatabaseId(), languageDriver, null);
}
protected MappedStatement addMappedStatement(Class<?> mapperClass, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class<?> parameterType,
String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
return addMappedStatement(mapperClass, this.methodName, sqlSource, sqlCommandType, parameterType, resultMap, resultType, keyGenerator, keyProperty, keyColumn);
}
/**
* 注入自定义 MappedStatement
*
* @param mapperClass mapper 接口
* @param modelClass mapper 泛型
* @param tableInfo 数据库表反射信息
* @return MappedStatement
*/
public abstract MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo);
}
这里我们以SelectById
类作为案例来对其分析。
首先可以看到在构造方法中将SqlMethod
枚举类中定义好的方法名传入到父类中,方便后续使用;同时重写injectMappedStatement
方法,通过SQL模板构建出SQL语句并存入到全局配置类中。
public class SelectById extends AbstractMethod {
public SelectById() {
//给methodName属性赋值
this(SqlMethod.SELECT_BY_ID.getMethod());
}
public SelectById(String name) {
//给methodName属性赋值
super(name);
}
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
//获取SqlMethod类
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
//生成SqlSource对象
SqlSource sqlSource = new RawSqlSource(this.configuration, String.format(sqlMethod.getSql(), this.sqlSelectColumns(tableInfo, false), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true)), Object.class);
//将MappedStatement对象加入配置类对象中
return this.addSelectMappedStatementForTable(mapperClass, this.methodName, sqlSource, tableInfo);
}
}
SqlMethod
SqlMethod
就是一个枚举类,存储了两个关键的元素:
BaseMapper
中的方法名- 方法名对应的sql语句模板
看到这两个元素,相信大家应该已经知道SQL自动生成的本质了:根据不同的方法来提供一些通用的模板,项目启动后再将其加载进mappedStatement
。
SqlSource
SqlSource
对象里面则是通过解析SQL模板、以及传入的表信息和主键信息构建出了一条SQL语句:
可能会有人疑惑这里的表信息是从何而来,其实这些表信息就是在SQL注入的时候获取的表信息,然后传到AbstractMethod
中的,所以在重写injectMappedStatement
方法的时候就可以使用到了。
Mapper文件被添加的过程
看完上述内容相信大家应该都知道了SQL注入器的基本原理了,那么SQL注入器是在哪里添加到Mybatis
中的呢?如果不太清楚的话,我们带着问题往下看。
首先我们回顾下Mybatis
的执行流程,一般可以分为以下几个步骤:
- 加载配置文件:在应用启动时,
Mybatis
会读取配置文件(mybatis-config.xml
)并解析其中的配置信息,例如数据库连接信息、映射器信息等。 - 创建
SqlSessionFactory
:通过SqlSessionFactoryBuilder
类加载配置文件中的信息,并创建SqlSessionFactory
对象。SqlSessionFactory
是一个重量级的对象,它的作用是创建SqlSession
对象,SqlSession
是用于执行 SQL 语句的核心对象。 - 创建
SqlSession
:通过SqlSessionFactory
的openSession
方法创建SqlSession
对象。在执行 SQL 操作时,我们需要通过SqlSession
对象获取到对应的 Mapper 接口,然后调用该接口中定义的方法来执行 SQL 语句。 - 获取
Mapper
接口:在Mybatis
中,我们通常通过Mapper
接口的方式执行 SQL 操作。因此,在获取 Mapper 接口之前,我们需要先配置映射关系,即在配置文件中指定 Mapper 接口所对应的 XML 文件或注解类。在创建SqlSession
对象后,我们可以通过SqlSession
的getMapper
方法获取到对应的 Mapper 接口。 - 执行 SQL 语句:当我们获取到
Mapper
接口后,就可以通过调用其方法执行 SQL 语句了。在执行 SQL 语句前,Mybatis
会将Mapper
接口中定义的 SQL 语句转换成MappedStatement
对象,并将其中的参数信息传递给Executor
对象执行 SQL 语句。 - 处理 SQL 语句的执行结果:在执行 SQL 语句后,
Mybatis
会将查询结果封装成对应的 Java 对象并返回。
看完上述流程后,你觉得会在那个步骤进行添加SQL注入器的操作呢?我盲猜这个步骤应该位于步骤3中,那让我们从MP的入口处开始查看源码看看猜测是否正确。
MP入口
可能有些人不知道MP的具体入口从哪里看,其实很简单,我们可以直接去mybatis-plus-boot-starter
中resources
下的META-INF
文件夹下查看:(基础的Spring boot 自动装配机制这里不过多说明)
# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
可以看到配置的自动启动类是MybatisPlusAutoConfiguration
;
这个类由于实现了InitializingBean
接口,得到了afterPropertiesSet
方法,在Bean初始化后,会自动调用。 还有三个标注了 @ConditionalOnMissingBean
注解的方法,说明这些方法在没有配置对应对象时会由SpringBoot
创建Bean,并且保存到容器中。
所以sqlSessionFactory
方法在没有配置SqlSessionFactory
时会由SpringBoot
创建Bean,并且保存到容器中。
MybatisSqlSessionFactoryBean
我们可以发现进入sqlSessionFactory
方法后就会实例化MybatisSqlSessionFactoryBean
类,那么该类到底做了什么呢?
public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>
可以发现该类实现了三个接口FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>
:
-
FactoryBean
:说明用到了工厂模式 -
InitializingBean
:afterPropertiesSet
在属性设置完成时调用(在Bean创建完成时)调用 -
ApplicationListener
是一个监听器,监听的是ApplicationContext
初始化或者刷新事件,当初始化或者刷新时调用。
这里我们主要看初始化后调用的方法afterPropertiesSet
:可以发现在该方法中调用了buildSqlSessionFactory
方法。
buildSqlSessionFactory
那么buildSqlSessionFactory
方法做了些什么呢?简单的说就是创建一个SqlSessionFactory
实例,虽然里面还有很多其他步骤,但是不在本文谈论范围内。
我们直接看最重要的部分xmlMapperBuilder.parse()
:
parse
方法主要用来解析xml
文件,bindMapperForNamespace
方法则用来解析接口文件。
addMapper
是由前面MybatisConfiguration
调用的。
这里会解析出对应的类型,然后内部调用MybatisMapperRegistry
的方法:
内部最后是由MybatisMapperAnnotationBuilder
去解析的:
在这个方法的最后会进行基本的SQL方法注入:
可以发现最后又回到了我们最初说到的AbstractSqlInjector
类,该类帮助我们实现了基本SQL方法的自动注入。
到这里相信大家已经对SQL注入器的原理有了一个清楚的认识了,如果还不太理解的话,可以从MP入口处开始,根据截图的内容自行打断点熟悉下。