Mybatis插件拦截器实现异常条件拦截检测

背景

实际开发过程中可能会因为各种原因导致对数据库操作的代码缺少where条件,对于查询操作可能会导致全表扫描,对于更新操作可能会导致整个表的字段错误更新。大多数场景都不是符合我们的预期的。

所以做了这样一个插件(可以根据配置application.yml动态开启/关闭插件。可以检测where条件是否合法来决定打印error日志or抛出异常)

插件原理

Mybatis 预留了org.apache.ibatis.plugin.Interceptor 接口,通过实现该接口,可以对Mybatis的执行流程进行拦截,接口的定义如下:

public interface Interceptor {
 
  // 插件的核心逻辑在这个方法中
  Object intercept(Invocation invocation) throws Throwable;
  // 使用当前的Interceptor创建代理,通常的实现都是 Plugin.wrap(target, this),wrap方法内使用 jdk 创建动态代理对象;
  Object plugin(Object target);
  // 可以获取Mybatis配置文件中中设置的参数
  void setProperties(Properties properties);
}

Mybatis插件可拦截的类为以下四个:

  • Executor

  • StatementHandler

  • ParameterHandler

  • ResultSetHandler

这些类的代理流程

Mybatis插件拦截器实现异常条件拦截检测_第1张图片

 在执行query方法之前都可以获取到我们要执行的原始sql,然后进行条件检测判断。所以我们可以织入我们代码的拦截类可以为 Executor、StatementHandler、ParameterHandler。这里选择了Executor。

mybatis的详细执行流程和动态代理实现sql代理等流程戳这里:编码技巧——数据加密(二)Mybatis拦截器

插件实现

插件代码:

common项目:

自定义条件检测配置类(把配置文件中的mybatis配置注入到该类中,实现配置和拦截器代码分离的作用。方便各个服务灵活接入)


/**
 * mybatis自定义条件检测配置类
 *
 * @see MybatisConditionDetectionInterceptor
 *
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "mybatis.condition.detection")
@ConditionalOnProperty(prefix = "mybatis.condition.detection", name = "enable", havingValue = "true")
public class MybatisConditionDetectionProperty {

    /**
     * 条件检测拦截器开关,默认不生效。
     * 仅在enable=true时条件检测拦截器才会生效
     */
    private Boolean enable = false;

    /**
     * 检测模式,出现非法条件时 1 打印error日志、2 抛出RunTimeException。默认为打印error日志
     * @see MybatisConditionDetectionEnum
     */
    private Integer detectionMode = 1;

    ;
}

Mybatis条件检测拦截器(根据配置类的属性实现异常检测、出现异常条件时反馈机制“打日志还是抛异常”)


/**
 * Mybatis条件检测拦截器
 * 可支持example方式生成的sql和动态sql的条件检测。
 * 参考:
 * 编码技巧——数据加密(二)Mybatis拦截器
 * 
 *
 * @date 2023/06/12
 * @see Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
 * @see Executor#query(MappedStatement, Object, RowBounds, ResultHandler)
 * @see Executor#update(MappedStatement, Object)
 */

@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class MybatisConditionDetectionInterceptor implements Interceptor {

    private static final String PSM_NAME_UNKNOWN = "unknown";

    private MybatisConditionDetectionProperty mybatisConditionDetectionProperty;

    private String psm;

    public MybatisConditionDetectionInterceptor(MybatisConditionDetectionProperty mybatisConditionDetectionProperty, String psm) {
        this.mybatisConditionDetectionProperty = mybatisConditionDetectionProperty;
        this.psm = StringTools.nonBlankOrElse(psm, PSM_NAME_UNKNOWN);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 如果没有配置或者条件检测配置未开启,直接执行sql。
        if (Objects.isNull(this.mybatisConditionDetectionProperty) || Boolean.FALSE.equals(this.mybatisConditionDetectionProperty.getEnable())) {
            return invocation.proceed();
        }
        //获取执行参数
        Object[] objects = invocation.getArgs();
        MappedStatement ms = (MappedStatement) objects[0];
        // 获取当前方法的全限定名 eg:com.bytedance.homed.decorate.construction.infrastructure.repository.mysql.mapper.CameraDataInfoMapper.insert
        String mapperMethodAllName = ms.getId();
        int lastIndex = mapperMethodAllName.lastIndexOf(".");
        // 获取当前类(接口)的全限定名 eg:com.bytedance.homed.decorate.construction.infrastructure.repository.mysql.mapper.CameraDataInfoMapper
        String mapperClassStr = mapperMethodAllName.substring(0, lastIndex);
        // 获取当前方法名 eg:insert
        String mapperClassMethodStr = mapperMethodAllName.substring((lastIndex + 1));
        // 获取类的所有方法
        Class mapperClass = Class.forName(mapperClassStr);
        // 根据配置获取出现异常条件时的提示方式
        MybatisConditionDetectionEnum conditionDetection = this.mybatisConditionDetectionProperty == null ?
                MybatisConditionDetectionEnum.ERROR_LOG :
                MybatisConditionDetectionEnum.valueOf(this.mybatisConditionDetectionProperty.getDetectionMode());
        Method[] methods = mapperClass.getMethods();
        for (Method method : methods) {
            // 如果匹配到当前方法,进行sql条件检验
            if (method.getName().equals(mapperClassMethodStr)) {
                BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                String executeSql = boundSql.getSql().toLowerCase(Locale.CHINA).replace("[\\t\\n\\r]", " ");

                // 如果是插入语句,不进行检测。
                if (method.getName().contains("insert")) {
                    continue;
                }
                // 如果是查询/更新/删除语句,检测是否包含where关键字和是否包含条件
                else if (method.getName().contains("delete") || method.getName().contains("update") || method.getName().contains("select")) {
                    if (!executeSql.contains("where")) {
                        if (MybatisConditionDetectionEnum.THROW_ERROR == conditionDetection) {
                            throw new RuntimeException("警告,该sql没有where条件,请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
                        } else {
                            log.error("注意,该sql没有where条件,请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
                        }

                    }
                    if (executeSql.contains("where")) {
                        int wherePosition = executeSql.indexOf("where");
                        String substring = StringUtils.trim(executeSql.substring(wherePosition + 5));
                        if (StringUtils.isBlank(substring)) {
                            if (MybatisConditionDetectionEnum.THROW_ERROR == conditionDetection) {
                                throw new RuntimeException("警告,该sql语句where后缺少条件!请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
                            } else {
                                log.error("警告,该sql语句where后缺少条件!请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
                            }
                        }
                    }
                }
                // 自定义sql方法并且不包含增删改查关键字的不予处理
                else {
                    continue;
                }
            }
        }
        //继续执行逻辑
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object o) {
        //获取代理权
        if (o instanceof Executor) {
            //如果是Executor(执行增删改查操作),否则拦截下来
            return Plugin.wrap(o, this);
        } else {
            return o;
        }
    }


    @Override
    public void setProperties(Properties properties) {
    }
}

条件检测插件自动装配类(根据条件检测配置类是否存在,灵活注入条件检测拦截器)


/**
 * Mybatis条件检测自动装配类
 *
 * @date 2022/06/12
 */
@Configuration
public class MybatisConditionDetectionConfiguration {

    @Value("${spring.application.name}")
    private String psm;

    @Bean
    @ConditionalOnBean(MybatisConditionDetectionProperties.class)
    public Interceptor mybatisConditionDetectionInterceptor(MybatisConditionDetectionProperties mybatisConditionDetectionProperties) {
        return new MybatisConditionDetectionInterceptor();
    }
}

调试截图:

动态sql条件检测调试

  • 测试插入数据,测试空条件更新数据             ​​​ Mybatis插件拦截器实现异常条件拦截检测_第2张图片Mybatis插件拦截器实现异常条件拦截检测_第3张图片
  • 测试有条件更新数据Mybatis插件拦截器实现异常条件拦截检测_第4张图片
  • 测试有条件查询语句        Mybatis插件拦截器实现异常条件拦截检测_第5张图片
  • 测试有条件删除语句

    Mybatis插件拦截器实现异常条件拦截检测_第6张图片

example方式生成的sql条件检测调试

  • 测试插入数据Mybatis插件拦截器实现异常条件拦截检测_第7张图片Mybatis插件拦截器实现异常条件拦截检测_第8张图片
  • 测试空条件更新数据

    Mybatis插件拦截器实现异常条件拦截检测_第9张图片

  • 测试有条件更新数据

    Mybatis插件拦截器实现异常条件拦截检测_第10张图片

  • 测试有条件查询数据

    Mybatis插件拦截器实现异常条件拦截检测_第11张图片

  • 测试有条件删除数据

    Mybatis插件拦截器实现异常条件拦截检测_第12张图片

    参考资料

    编码技巧——数据加密(二)Mybatis拦截器

    @Configuration注解深入详解

    @EnableConfigurationProperties注解

    关与 @EnableConfigurationProperties 注解

你可能感兴趣的:(mybatis,tomcat,java)