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

news2024/11/26 20:42:05

背景

在项目中需要同时操作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);
    }

}

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入口处开始,根据截图的内容自行打断点熟悉下。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WebSocket的那些事(3-STOMP实操篇)

目录 一、序言二、STOMP详解1、STOMP简单介绍2、STOMP协议内容3、使用STOMP的好处 三、代码示例1、Maven依赖2、开启WebSocket消息代理3、控制器4、前端页面greeting.html 四、测试1、连接服务端2、发送消息 五、STOMP消息传播流程六、结语 一、序言 上节中我们在 WebSocket的…

(11)LCD1602液晶显示屏

LCD1602&#xff08;Liquid Crystal Display&#xff09;液晶显示屏是一种字符型液晶显示模块&#xff0c;可以显示ASCII码的标准字符和其它的一些内置特殊字符&#xff0c;还可以有8个自定义字符&#xff0c;自带芯片扫描 显示容量&#xff1a;162个字符&#xff0c;每个字符…

【C++】STL六大组件简介

STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。 1.STL的版本介绍 原始版本 Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本&#xff…

Unity里面CG和HLSL在写法上的一些区别

大家好&#xff0c;我是阿赵。这里继续讲URP相关的内容。 这次想讲的是CG和HLSL在写法上的一些区别。 一、为什么开始用HLSL 首先&#xff0c;基本上大家都知道的事情再说一遍。 三种Shader编程语言&#xff1a; 1、基于OpenGL的OpenGL Shading Language&#xff0c;缩写GLSL…

接口测试中postman环境和用例集

postman的环境使用 postman里有环境的设置&#xff0c;就是我们常说的用变量代替一个固定的值&#xff0c;这样做的好处是可以切换不同的域名、不同的环境变量&#xff0c;不同的线上线下账户等等场景。下面就看下怎么用吧。 创建一个Environment postman有一个envrionment&am…

Java是如何实现双亲委托机制的

Java 是一种面向对象的编程语言&#xff0c;它有一套独特的类加载机制。其中&#xff0c;双亲委托加载机制是 Java 类加载机制中的一个重要概念。本文将介绍 Java 的双亲委托加载机制是如何实现的&#xff0c;并解释其作用和优点。 Java 类加载机制 在 Java 中&#xff0c;类的…

瀑布流组件陷入商品重复怪圈?我是如何用心一解的!

目录 背景 瀑布流组件 什么是瀑布流组件 如何实现一个瀑布流组件 商品重复的原因 解决方案 方法一&#xff08;复杂&#xff0c;不推荐&#xff09;&#xff1a;标记位大法 方法二&#xff08;优雅&#xff0c;推荐&#xff09;&#xff1a;Promise 队列 大法 总结 背…

解决新思路#报错:ping: www.baidu.com: 未知的名称或服务--照着步骤来还是ping不通的可能原因

最近由ubantu转为centos7,配置hadoop&#xff0c;配置静态ip的过程中一直ping不通。之前配置ubantu的也是&#xff0c;终于这次在重启虚拟机和主机后发现了原因。 中途尝试过: 1.三次以上命令行反复操作 2.图形界面设置 3.看是否主机的网络适配器的网关与设置的IP地址产生冲突…

JavaScript实现计算100之间能被5整除的数的代码

以下为实现计算100之间能被5整除的数的程序代码和运行截图 目录 前言 一、计算100之间能被5整除的数 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 前言 1.若有选择&#xff0c;您可以在目录里进行快速查找&#xff1b; 2.本博文代码可以根据题…

2023最新100道渗透测试面试题(附答案)

眨眼间2023年快过去一半了&#xff0c;不知道大家有没有找到心仪的工作呀&#xff0c;今天我给大家整理了100道渗透测试面试题给大家&#xff0c;需要答案的话可以在评论区给我留言哦~ 第一套渗透面试题 什么是渗透测试&#xff1f;它的目的是什么&#xff1f; 渗透测试的五个…

DirectX12 简单入门(一)

在很久以前写过关于DirectX9的一些应用&#xff0c;直到现在DirectX12已经普及了。写完几个DirectX12测试代码之后&#xff0c;写一篇DirectX12简单入门介绍一下基本概念&#xff0c;以及环境搭建和编程过程。 编程环境 与DirectX9不同&#xff0c;在DirectX12开发中微软将需…

『MySQL 实战 45 讲』“order by” 是怎么工作的

“order by” 是怎么工作的 首先创建一个表 CREATE TABLE t ( id int(11) NOT NULL, city varchar(16) NOT NULL, name varchar(16) NOT NULL, age int(11) NOT NULL, addr varchar(128) DEFAULT NULL, PRIMARY KEY (id), KEY city (city) ) ENGINEInnoDB;全字段排序 在 cit…

自己搭建go web 框架

思想base部分day1:封装gee封装context上下文封装tree路由树分组封装group与中间件封装文件解析封装封装错误处理 思想 web框架服务主要围绕着请求与响应来展开的 搭建一个web框架的核心思想 1 便捷添加响应路径与响应函数(base) 2 能够接收多种数据类型传入(上下文context) 3 构…

【Linux】Linux入门学习之常用命令五

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

支付系统设计五:对账系统设计01-总览

文章目录 前言一、对账系统构建二、执行流程三、获取支付渠道数据1.接口形式1.1 后台配置1.2 脚本编写1.2.1 模板1.2.2 解析脚本 2.FTP形式2.1 后台配置2.2 脚本编写2.2.1 模板2.2.2 解析脚本 四、获取支付平台数据五、数据比对1. 比对模型2. 比对器 总结 前言 从《支付系统设…

AE基础教程

一&#xff1a;粒子插件。 AEPR插件-Trapcode Suite V18.1.0 中文版 二&#xff1a;跟随手指特效。 1&#xff1a;空对象位置关键帧跟随手指。 2&#xff1a;发射粒子位置&#xff0c;按住Alt键&#xff0c;连接到空对象位置处。。 三&#xff1a;CtrI导入素材快捷键。 四&a…

Elasticsearch基础学习-常用查询和基本的JavaAPI操作ES

关于ES数据库的和核心倒排索引的介绍 一、Elasticsearch概述简介关于全文检索引擎关系型数据库的全文检索功能缺点全文检索的应用场景Elasticsearch 应用案例 二、Elasticsearch学习准备安装下载关于es检索的核心-倒排索引正向索引&#xff08;forward index&#xff09;倒排索…

辅助驾驶功能开发-功能规范篇(16)-2-领航辅助系统NAP-自动变道-1

书接上回 2.3.4.自动变道 当车辆处于导航引导模式NOA功能时(即车辆横向控制功能激活),且车速大于40km/h,驾驶员按下转向灯拨杆或系统判断当前有变道需要时,自动变道系统通过对车道线、自车道前方目标距离、邻近车道前后方目标距离等环境条件进行判断,在转向灯亮起3s后控…

看到这个数据库设计,我终于明白了我和其他软测人的差距

测试人员为什么要懂数据库设计&#xff1f; 更精准的掌握业务&#xff0c;针对接口测试、Web 测试&#xff0c;都是依照项目/产品需求进行用例设计&#xff0c;如果掌握数据库设计知识&#xff0c;能直接面对开发的数据表&#xff0c;更好、更精准的理解业务逻辑&#xff1b;有…

【滑动窗口】滑窗模板,在小小的算法题里滑呀滑呀滑

首先大家先喊出我们的口号&#xff1a;跟着模板搞&#xff0c;滑窗没烦恼&#xff01; 一.什么是滑动窗口&#xff1f; 滑动窗口算法是双指针算法的一种特定化的算法模型&#xff0c;常用于在特定的条件下求最大或者最小的字符串&#xff0c;特定的数组&#xff0c;以及字符序列…