当前位置: 首页 > news >正文

深度剖析Mybatis-plus Injector SQL注入器

背景

在项目中需要同时操作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);}@Overridepublic 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 {@Overridepublic 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 {@Autowiredprivate UserMapper userMapper;@Testvoid contextLoads() {User user = userMapper.findOneById(1);System.out.println(user);}}

image-20230509222905227

由此可见我们的自定义SQL注入成功,至此大功告成。

深度剖析

你以为就这么结束了?都看了这篇文章了,还不直接把SQL注入器彻底搞懂?

SQL是如何注入的?

那么SQL到底是如何注入的呢?我们先来看SQL注入器的顶级接口ISqlInjector,怎么知道它是顶级接口的?在万能的IDEA里面直接通过我们自定义的注入器查看其UML图就知道了;

image-20230509223300796

或者我们可以直接查看MP的源码,在injector目录下:

image-20230509223835910

如果这个时候你查看了methods目录下的内容,就会发现其实这些内容就等同于我们自定义的SQL方法,这些都是MP帮我们实现好的内置SQL方法。

回归正题,那么ISqlInjector的作用是什么呢?它只做一件事,那就是检查SQL是否注入,已经注入过则不再注入。

public interface ISqlInjector {// 检查SQL是否注入,已经注入过则不再注入。void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}

按照UML图,我们接下来应该看其下一层抽象类AbstractSqlInjector。该类主要为两个方法:

  1. inspectInject:实现顶层接口方法,分为以下几个步骤:

    • 通过反射获取实体类对象
    • 获取mapperRegistry缓存用于对比,防止覆盖
    • 封装 TableInfo 存储表信息对象
    • 循环注入自定义方法
  2. 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来看看:这里主要看下数据库信息的获取以及方法的注入;

获取数据库信息

实体类如下:

image-20230515235153526

首先我们可以看到TableInfoHelper.initTableInfo方法获取到了数据库表的相关信息,那么它是如何做到的呢?我们进入刚方法内查看:

image-20230515231638703

通过查看代码,我们可以看到初始化数据库信息的方法主要为initTableName以及initTableFields

image-20230515234610634

initTableName的作用是获取表名信息,主要通过实体上的@TableName注解拿到表名,所以我们这里拿到了User类名。

image-20230515235012657

initTableFields的作用主要为获取主键及其他字段信息:

image-20230515235721112

至此,数据库信息则获取成功,可供注入SQL方法时使用。

方法注入

获取完表信息后,可以发现通过getMethodList方法获取了所有自定义方法,这些方法都是哪里来的呢?都是从AbstractSqlInjector抽象类的子类中获取的,比如默认的SQL注入器DefaultSqlInjector以及我们自定的Inject

image-20230515231659455

获取所有自定义方法后,可以发现通过调用AbstractMethodinject方法实现了SQL的自动注入,这里也把上文获取到的数据库表对象传入用来进行SQL的封装。

image-20230516001106880

injectMappedStatement方法则需要每个SQL类根据各自需求重写,最后将生成好的MappedStatement对象加入到全局配置类对象中。

image-20230516001326434

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

image-20230511232744408

SqlMethod就是一个枚举类,存储了两个关键的元素:

  • BaseMapper中的方法名
  • 方法名对应的sql语句模板

看到这两个元素,相信大家应该已经知道SQL自动生成的本质了:根据不同的方法来提供一些通用的模板,项目启动后再将其加载进mappedStatement

SqlSource

SqlSource对象里面则是通过解析SQL模板、以及传入的表信息和主键信息构建出了一条SQL语句:

image-20230511232305994

可能会有人疑惑这里的表信息是从何而来,其实这些表信息就是在SQL注入的时候获取的表信息,然后传到AbstractMethod中的,所以在重写injectMappedStatement方法的时候就可以使用到了。

Mapper文件被添加的过程

看完上述内容相信大家应该都知道了SQL注入器的基本原理了,那么SQL注入器是在哪里添加到Mybatis中的呢?如果不太清楚的话,我们带着问题往下看。

首先我们回顾下Mybatis 的执行流程,一般可以分为以下几个步骤:

  1. 加载配置文件:在应用启动时,Mybatis 会读取配置文件(mybatis-config.xml)并解析其中的配置信息,例如数据库连接信息、映射器信息等。
  2. 创建 SqlSessionFactory:通过SqlSessionFactoryBuilder 类加载配置文件中的信息,并创建 SqlSessionFactory 对象。SqlSessionFactory 是一个重量级的对象,它的作用是创建 SqlSession 对象,SqlSession 是用于执行 SQL 语句的核心对象。
  3. 创建 SqlSession:通过 SqlSessionFactoryopenSession 方法创建 SqlSession 对象。在执行 SQL 操作时,我们需要通过 SqlSession 对象获取到对应的 Mapper 接口,然后调用该接口中定义的方法来执行 SQL 语句。
  4. 获取 Mapper 接口:在 Mybatis 中,我们通常通过 Mapper 接口的方式执行 SQL 操作。因此,在获取 Mapper 接口之前,我们需要先配置映射关系,即在配置文件中指定 Mapper 接口所对应的 XML 文件或注解类。在创建 SqlSession 对象后,我们可以通过 SqlSessiongetMapper 方法获取到对应的 Mapper 接口。
  5. 执行 SQL 语句:当我们获取到 Mapper 接口后,就可以通过调用其方法执行 SQL 语句了。在执行 SQL 语句前,Mybatis 会将 Mapper 接口中定义的 SQL 语句转换成 MappedStatement 对象,并将其中的参数信息传递给 Executor 对象执行 SQL 语句。
  6. 处理 SQL 语句的执行结果:在执行 SQL 语句后,Mybatis 会将查询结果封装成对应的 Java 对象并返回。

image-20230516233435018

看完上述流程后,你觉得会在那个步骤进行添加SQL注入器的操作呢?我盲猜这个步骤应该位于步骤3中,那让我们从MP的入口处开始查看源码看看猜测是否正确。

MP入口

可能有些人不知道MP的具体入口从哪里看,其实很简单,我们可以直接去mybatis-plus-boot-starterresources下的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

image-20230517231912803

这个类由于实现了InitializingBean接口,得到了afterPropertiesSet方法,在Bean初始化后,会自动调用。 还有三个标注了 @ConditionalOnMissingBean 注解的方法,说明这些方法在没有配置对应对象时会由SpringBoot创建Bean,并且保存到容器中。

所以sqlSessionFactory方法在没有配置SqlSessionFactory时会由SpringBoot创建Bean,并且保存到容器中。

image-20230517233859758

MybatisSqlSessionFactoryBean

我们可以发现进入sqlSessionFactory方法后就会实例化MybatisSqlSessionFactoryBean类,那么该类到底做了什么呢?

public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> 

可以发现该类实现了三个接口FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>

  1. FactoryBean:说明用到了工厂模式

  2. InitializingBeanafterPropertiesSet 在属性设置完成时调用(在Bean创建完成时)调用

  3. ApplicationListener是一个监听器,监听的是ApplicationContext初始化或者刷新事件,当初始化或者刷新时调用。

这里我们主要看初始化后调用的方法afterPropertiesSet:可以发现在该方法中调用了buildSqlSessionFactory方法。

image-20230517234511544

buildSqlSessionFactory

那么buildSqlSessionFactory方法做了些什么呢?简单的说就是创建一个SqlSessionFactory实例,虽然里面还有很多其他步骤,但是不在本文谈论范围内。

我们直接看最重要的部分xmlMapperBuilder.parse()

image-20230517235132284

parse方法主要用来解析xml文件,bindMapperForNamespace方法则用来解析接口文件。

image-20230517235328652

addMapper是由前面MybatisConfiguration调用的。

image-20230517235429934

这里会解析出对应的类型,然后内部调用MybatisMapperRegistry的方法:

image-20230517235738954

内部最后是由MybatisMapperAnnotationBuilder去解析的:

image-20230517235754248

在这个方法的最后会进行基本的SQL方法注入:

image-20230517235913171

可以发现最后又回到了我们最初说到的AbstractSqlInjector类,该类帮助我们实现了基本SQL方法的自动注入。

image-20230517235949503

到这里相信大家已经对SQL注入器的原理有了一个清楚的认识了,如果还不太理解的话,可以从MP入口处开始,根据截图的内容自行打断点熟悉下。

相关文章:

深度剖析Mybatis-plus Injector SQL注入器

背景 在项目中需要同时操作Sql Server 以及 MySQL 数据库&#xff0c;可能平时直接使用 BaseMapper中提供的方法习惯 了&#xff0c;不用的话总感觉影响开发效率&#xff0c;但是两个数据库的SQL语法稍微有点差别&#xff0c;有些暴露的方法并不能直接使用&#xff0c;所以便想…...

【Mysql实战】使用存储过程和计算同比环比

背景 同环比&#xff0c;是基本的数据分析方法。在各类调研表中屡见不鲜&#xff0c;如果人工向前追溯统计数据&#xff0c;可想而知工作量是非常大的。 标题复制10行&#xff0c;并且每行大于10个字符【源码解析】SpringBoot接口参数【Mysql实战】使用存储过程和计算同比环比…...

ChatGPT的前世今生,到如今AI领域的竞争格局,本文带你一路回看!

73年前&#xff0c;“机器思维”的概念第一次被计算机科学之父艾伦图灵&#xff08;Alan Turing&#xff09;提出&#xff0c;从此&#xff0c;通过图灵测试成为了人类在AI领域为之奋斗的里程碑目标。 73年后的今天&#xff0c;在AI历经了数十年的不断进化、迭代后&#xff0c…...

如何在JavaScript中获取当前时间yyyymmddhhmmss? (六种实现方式)

## 介绍 在编写JavaScript代码时&#xff0c;我们经常需要获取当前日期和时间。在本文中&#xff0c;我们将介绍几种获取当前时间并将其格式化为 yyyymmddhhmmss 的字符串的方法。 方法一&#xff1a;使用Date对象 在JavaScript中&#xff0c;我们可以使用 Date 对象来获取当…...

一、走进easyUI的世界

1.什么是easyUI&#xff1f; jQuery EasyUI是一组基于jQuery的UI插件集合体&#xff0c;而jQuery EasyUI的目标就是帮助web开发者更轻松的打造出功能丰富并且美观的UI界面。开发者不需要编写复杂的javascript&#xff0c;也不需要对css样式有深入的了解&#xff0c;开发者需要…...

2023 上半年软件设计师知识点复习总纲

前言&#xff1a;全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;以下简称IT职业资格考试&#xff09;是由中华人民共和国人事部主管&#xff0c;国家计算机网络与信息安全管理中心主办的一项国家级、权威性的计算机职业技能水平认证考试。主要…...

深入理解Java虚拟机:JVM高级特性与最佳实践-总结-3

深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践-总结-3 垃圾收集器与内存分配策略垃圾收集算法标记-清除算法标记-复制算法标记-整理算法 垃圾收集器与内存分配策略 垃圾收集算法 标记-清除算法 最基础的垃圾收集算法是“标记-清除”&#xff08;Mark-Sweep&#xff…...

vue3 cesium datav 可视化大屏

目录 0. 预览效果 1. 代码库包 2. 技术点 3. 一些注意事项&#xff08;配置参数&#xff09; 4. 相关代码详情 0. 预览效果 包含的功能&#xff1a; ① 地球按照一定速度自转 ② 修改加载的geojson面样式 ③ 添加 文字 标注&#xff01; 1. 代码库包 直接采用vue-cli5 创建…...

python内置函数,推导式

abs&#xff1a;取绝对值 data abs&#xff08;-10&#xff09; pow&#xff1a;次方 data pow&#xff08;2&#xff0c;5&#xff09; sum&#xff1a;求和 num_list p[1,2,10,20] res sum(num_list) divmod取商和余数&#xff1a; v1&#xff0c;v2 divmod&…...

【Flink】DataStream API使用之Flink支持的数据类型

Flink的使用过程中&#xff0c;我们的数据都是定义好的 UserBehavior 类型&#xff0c;那还有没有其他更灵活的类型可以用呢&#xff1f;Flink 支持的数据类型到底有哪些&#xff1f; 1. Flink 的类型系统 Flink 作为一个分布式处理框架&#xff0c;处理的是以数据对象作为元…...

QT实现固高运动控制卡示波器

目录 一、固高示波器 二、基于QCustomPlot实现示波器 三、完整源码 一、固高示波器 固高运动控制卡自带的软件有一个示波器功能&#xff0c;可以实时显示速度的波形&#xff0c;可辅助分析电机的运行状态。但是我们基于sdk开发了自己的软件&#xff0c;无法再使用该功能&…...

洛谷P1157详解(两种解法,一看就会)

一、问题引出 组合的输出 题目描述 排列与组合是常用的数学方法&#xff0c;其中组合就是从 n n n 个元素中抽出 r r r 个元素&#xff08;不分顺序且 r ≤ n r \le n r≤n&#xff09;&#xff0c;我们可以简单地将 n n n 个元素理解为自然数 1 , 2 , … , n 1,2,\dot…...

JavaScript异步编程和回调

目录 1、编程语言中的异步 2、JavaScript 3、回调 &#xff13;.&#xff11;在回调中处理错误 &#xff13;.&#xff12;回调的问题 &#xff13;.&#xff12;回调的替代方案 1、编程语言中的异步 默认情况下&#xff0c;JavaScript是同步的&#xff0c;并且是单线程…...

Qt开发笔记(Qt5.9.9下载安装环境搭建win10)

#1 Qt下载网站&#xff08;国内、国外镜像&#xff09; #2 Qt5.9.9安装选项 #3 配置系统环境变量 #4 创建测试项目 #1 Qt下载网站&#xff08;国内、国外镜像&#xff09; 官方下载地址&#xff08;慢&#xff09;&#xff1a;http://download.qt.io/ 国内镜像网站 这里给大家…...

使用Plist编辑器——简单入门指南

本指南将介绍如何使用Plist编辑器。您将学习如何打开、编辑和保存plist文件&#xff0c;并了解plist文件的基本结构和用途。跟随这个简单的入门指南&#xff0c;您将掌握如何使用Plist编辑器轻松管理您的plist文件。 plist文件是一种常见的配置文件格式&#xff0c;用于存储应…...

Python常用的开发工具合集

​ Python是一种功能强大且易于学习的编程语言&#xff0c;被广泛应用于数据科学、机器学习、Web开发等领域。随着Python在各个领域的应用越来越广泛&#xff0c;越来越多的Python开发工具也涌现出来。但是&#xff0c;对于新手来说&#xff0c;选择一款合适的Python开发工具可…...

机器学习之线性回归

往期目录 python在线性规划中的应用 文章目录 一、线性回归算法概述1.1 什么是线性回归&#xff1f;1.2 线性回归算法原理1.3 线性回归的应用场景 二、线性回归算法Python实现2.1 导入必要的库2.2 随机生成数据集2.3 拟合模型2.4 预测结果2.5 结果可视化 三、完整代码 线性回归…...

中国系统正式发声!所有用户永久免费,网友:再见了,CentOS!

点关注公众号&#xff0c;回复“1024”获取2TB学习资源&#xff01; 如果说&#xff1a;没有操作系统会怎么样&#xff1f; 对于个PC来说&#xff0c;无论是台式机、笔记本、平板等等&#xff0c;一切都变的一无是处&#xff0c;这些硬件对我们来说&#xff0c;和一堆废铁没什么…...

Oracle数据库坏块类故障

正常的数据块有其特有的固定格式&#xff0c;如果某数据块内部出现了混乱而导致Oracle无法读取&#xff0c;则可称其为坏块。数据库坏块的影响范围可大可小&#xff0c;严重时会导致数据库无法打开。当数据库出现坏块时&#xff0c;一般出现ORA-01578错误、ORA-10632错误或者OR…...

andorid之摄像头驱动流程--MTK平台

camera成像原理&#xff1a; 景物通过镜头生产光学图像投射到sensor表面上&#xff0c;然后转为模拟电信号&#xff0c;经过数模变成数字图像信号&#xff0c;在经过DSP加工出来&#xff0c;然后在通过IO接口传输到CPU处理。 由于摄像头满足总线、驱动、设备模型&#xff0c;…...

IGP(Interior Gateway Protocol,内部网关协议)

IGP&#xff08;Interior Gateway Protocol&#xff0c;内部网关协议&#xff09; 是一种用于在一个自治系统&#xff08;AS&#xff09;内部传递路由信息的路由协议&#xff0c;主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用

1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...

Docker 本地安装 mysql 数据库

Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker &#xff1b;并安装。 基础操作不再赘述。 打开 macOS 终端&#xff0c;开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈&#xff0c;并不断增加特征维度持续测试」的做法&#xff0c;体现了一种逐步建模与迭代验证的实验思路&#xff0c;在金融欺诈检测中非常有价值&#xff0c;本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...

MFE(微前端) Module Federation:Webpack.config.js文件中每个属性的含义解释

以Module Federation 插件详为例&#xff0c;Webpack.config.js它可能的配置和含义如下&#xff1a; 前言 Module Federation 的Webpack.config.js核心配置包括&#xff1a; name filename&#xff08;定义应用标识&#xff09; remotes&#xff08;引用远程模块&#xff0…...

SpringAI实战:ChatModel智能对话全解

一、引言&#xff1a;Spring AI 与 Chat Model 的核心价值 &#x1f680; 在 Java 生态中集成大模型能力&#xff0c;Spring AI 提供了高效的解决方案 &#x1f916;。其中 Chat Model 作为核心交互组件&#xff0c;通过标准化接口简化了与大语言模型&#xff08;LLM&#xff0…...

【FTP】ftp文件传输会丢包吗?批量几百个文件传输,有一些文件没有传输完整,如何解决?

FTP&#xff08;File Transfer Protocol&#xff09;本身是一个基于 TCP 的协议&#xff0c;理论上不会丢包。但 FTP 文件传输过程中仍可能出现文件不完整、丢失或损坏的情况&#xff0c;主要原因包括&#xff1a; ✅ 一、FTP传输可能“丢包”或文件不完整的原因 原因描述网络…...