【MybBatis高级篇】MyBatis 拦截器

【MybBatis高级篇】MyBatis 拦截器

MyBatis 是一个流行的 Java 持久层框架,它提供了灵活的 SQL 映射和执行功能。有时候我们可能需要在运行时动态地修改 SQL 语句,例如添加一些条件(创建时间、修改时间)、排序、分页等。MyBatis 提供了一个强大的机制来实现这个需求,那就是拦截器(Interceptor)

拦截器介绍

拦截器是一种基于 AOP(面向切面编程)的技术,它可以在目标对象的方法执行前后插入自定义的逻辑。MyBatis 定义了四种类型的拦截器,分别是:

  • Executor:拦截执行器的方法,例如 update、query、commit、rollback 等。可以用来实现缓存、事务、分页等功能。
  • ParameterHandler:拦截参数处理器的方法,例如 setParameters 等。可以用来转换或加密参数等功能。
  • ResultSetHandler:拦截结果集处理器的方法,例如 handleResultSets、handleOutputParameters 等。可以用来转换或过滤结果集等功能。
  • StatementHandler:拦截语句处理器的方法,例如 prepare、parameterize、batch、update、query 等。可以用来修改 SQL 语句、添加参数、记录日志等功能。
拦截的类 拦截的方法
Executor update, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandler getParameterObject, setParameters
StatementHandler prepare, parameterize, batch, update, query
ResultSetHandler handleResultSets, handleOutputParameters

实现拦截器

1、定义一个实现 org.apache.ibatis.plugin.Interceptor 接口的拦截器类,并重写其中的 interceptpluginsetProperties 方法。

java 复制代码
public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    Object plugin(Object var1);
    void setProperties(Properties var1);
}
  • intercept(Invocation invocation) :从上面我们了解到interceptor能够拦截的四种类型对象,此处入参invocation便是指拦截到的对象。
    举例说明:拦截StatementHandler#query(Statement st,ResultHandler rh) 方法,那么Invocation就是该对象。
  • plugin(Object target) :这个方法的作用是就是让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理。
  • setProperties(Properties properties) : 拦截器需要一些变量对象,而且这个对象是支持可配置的。

2、添加 @Intercepts 注解,写上需要拦截的对象和方法,以及方法参数,例如 @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}),表示在 SQL 执行之前进行拦截处理

3、配置文件中添加拦截器

注册拦截器

1、xml方式

xml 复制代码
<plugins>
    <plugin interceptor="xxxx.CustomInterceptor"></plugin>
</plugins>

2、mybatis-spring-boot-start方式,只要使用@Component/@Bean把类注册到容器即可

java 复制代码
@Component
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {
 ...
}

应用

根据方法是否包含动态切换的注解标识,替换sql中包含的信息

yml

指定 xml 文件中需要替换的占位符标识:@dynamicSql 以及待替换日期条件。

yml 复制代码
spring:
  datasource:
    #   数据源基本配置
    url: jdbc:mysql://localhost:3306/order_db_1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
    type: com.alibaba.druid.pool.DruidDataSource
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis-plus:
  configuration:
    # 驼峰转换 从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: false
    # 是否开启缓存
    cache-enable: false
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    #call-setters-on-nulls: true
    # 打印sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/*.xml


# 动态sql配置
dynamicSql:
  placeholder: "@dynamicSql"
  date: "2023-07-31"

@DynamicSql

java 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicSql {
}

Dao 层代码

在需要进行 SQL 占位符替换的方法上加 @DynamicSql 注解。

java 复制代码
public interface DynamicSqlMapper  {
    @DynamicSql
    Long count();

    Long save();
}

xml

java 复制代码
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zysheep.mapper.DynamicSqlMapper">
    <select id="count" resultType="java.lang.Long">
        select count(1) from t_order_1 where create_time > @dynamicSql
    </select>
</mapper>

启动类

java 复制代码
@MapperScan(basePackages = "cn.zysheep.mapper")
@SpringBootApplication
public class DmApplication {
    public static void main(String[] args) {
        SpringApplication.run(DmApplication.class, args);
    }
}

拦截器核心代码

java 复制代码
@Component
@Slf4j
@Intercepts({
        @Signature(type = StatementHandler.class,
                method = "prepare", args = {Connection.class, Integer.class})
})
public class DynamicSqlInterceptor implements Interceptor {

    @Value("${dynamicSql.placeholder}")
    private String placeholder;

    @Value("${dynamicSql.date}")
    private  String dynamicDate;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取 StatementHandler 对象也就是执行语句
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. MetaObject 是 MyBatis 提供的一个反射帮助类,可以优雅访问对象的属性,这里是对 statementHandler 对象进行反射处理,
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                new DefaultReflectorFactory());
        // 3. 通过 metaObject 反射获取 statementHandler 对象的成员变量 mappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // mappedStatement 对象的 id 方法返回执行的 mapper 方法的全路径名,如cn.zysheep.mapper.DynamicSqlMapper.count
        String id = mappedStatement.getId();
        // 4. 通过 id 获取到 Dao 层类的全限定名称,然后反射获取 Class 对象
        Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
        // 5. 获取包含原始 sql 语句的 BoundSql 对象
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.info("替换前---sql:{}", sql);
        // 拦截方法
        String mSql = null;
        // 6. 遍历 Dao 层类的方法
        for (Method method : classType.getMethods()) {
            // 7. 判断方法上是否有 DynamicSql 注解,有的话,就认为需要进行 sql 替换
            if (method.isAnnotationPresent(DynamicSql.class)) {
                mSql = sql.replaceAll(placeholder, String.format("'%s'", dynamicDate));
                break;
            }
        }
        if (StringUtils.isNotBlank(mSql)) {
            log.info("替换后---mSql:{}", mSql);
            // 8. 对 BoundSql 对象通过反射修改 SQL 语句。
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, mSql);
        }
        // 9. 执行修改后的 SQL 语句。
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 使用 Plugin.wrap 方法生成代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 获取配置文件中的属性值
    }
}

代码测试

java 复制代码
@SpringBootTest(classes = DmApplication.class)
public class DynamicTest {

    @Autowired
    private DynamicSqlMapper dynamicSqlMapper;

    @Test
    public void test() {
        Long count = dynamicSqlMapper.count();
        Assert.notNull(count, "count不能为null");
    }
}

拦截器应用场景

1、SQL 语句执行监控:可以拦截执行的 SQL 方法,打印执行的 SQL 语句、参数等信息,并且还能够记录执行的总耗时,可供后期的 SQL 分析时使用。

2、SQL 分页查询:MyBatis 中使用的 RowBounds 使用的内存分页,在分页前会查询所有符合条件的数据,在数据量大的情况下性能较差。通过拦截器,可以在查询前修改 SQL 语句,提前加上需要的分页参数。

3、公共字段的赋值:在数据库中通常会有 createTime , updateTime 等公共字段,这类字段可以通过拦截统一对参数进行的赋值,从而省去手工通过 set 方法赋值的繁琐过程。

4、数据权限过滤:在很多系统中,不同的用户可能拥有不同的数据访问权限,例如在多租户的系统中,要做到租户间的数据隔离,每个租户只能访问到自己的数据,通过拦截器改写 SQL 语句及参数,能够实现对数据的自动过滤。

5、SQL 语句替换:对 SQL 中条件或者特殊字符进行逻辑替换。(也是本文的应用场景)

相关推荐
程序员大金1 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
Tatakai2517 小时前
Mybatis Plus分页查询返回total为0问题
java·spring·bug·mybatis
A_cot17 小时前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑18 小时前
苍穹外卖学习笔记(七)
java·windows·笔记·学习·mybatis
二十雨辰21 小时前
[苍穹外卖]-12Apache POI入门与实战
java·spring boot·mybatis
Amagi.1 天前
Redis的内存淘汰策略
数据库·redis·mybatis
执键行天涯1 天前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
工业甲酰苯胺1 天前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
ggdpzhk1 天前
Mybatis 快速入门(maven)
oracle·maven·mybatis
Java小白笔记2 天前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis