[MybBatis Advanced Articles] MyBatis Interceptor

MyBatis is a popular Java persistence layer framework that provides flexible SQL mapping and execution capabilities. Sometimes we may need to dynamically modify SQL statements at runtime, such as adding some conditions (creation time, modification time), sorting, paging, etc. MyBatis provides a powerful mechanism to achieve this requirement, that is 拦截器(Interceptor).

Interceptor introduction

Interceptor is a technology based on AOP (aspect-oriented programming), which can insert custom logic before and after the method execution of the target object. MyBatis defines four types of interceptors, namely:

  • Executor: The method of intercepting the executor, such as update, query, commit, rollback, etc. It can be used to implement functions such as caching, transactions, and paging.
  • ParameterHandler: methods to intercept parameter handlers, such as setParameters, etc. It can be used to convert or encrypt parameters and other functions.
  • ResultSetHandler: The method of intercepting the result set processor, such as handleResultSets, handleOutputParameters, etc. Can be used to transform or filter the result set and other functions.
  • StatementHandler: The method of intercepting statement processors, such as prepare, parameterize, batch, update, query, etc. It can be used to modify SQL statements, add parameters, record logs and other functions.
intercepted class method of interception
Executor update, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandler getParameterObject, setParameters
StatementHandler prepare, parameterize, batch, update, query
ResultSetHandler handleResultSets, handleOutputParameters

Implement the interceptor

1. Define an org.apache.ibatis.plugin.Interceptor interceptor class that implements the interface, and rewrite the intercept, pluginand setPropertiesmethods in it.

public interface Interceptor {
    
    
    Object intercept(Invocation var1) throws Throwable;
    Object plugin(Object var1);
    void setProperties(Properties var1);
}
  • intercept(Invocation invocation): From the above, we understand the four types of objects that the interceptor can intercept. Here, the input parameter invocation refers to the intercepted object.
    For example: Intercept the StatementHandler#query(Statement st, ResultHandler rh) method, then Invocation is the object.
  • plugin(Object target): The function of this method is to let mybatis judge whether to intercept, and then make a decision whether to generate a proxy.
  • setProperties(Properties properties) : The interceptor needs some variable objects, and this object supports configurability.

2. Add @Interceptsannotations, write the objects and methods that need to be intercepted, and method parameters, for example @Intercepts({@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class, Integer.class})}), it means interception processing before SQL execution

3. Add an interceptor to the configuration file

register interceptor

1. XML method

<plugins>
    <plugin interceptor="xxxx.CustomInterceptor"></plugin>
</plugins>

2. Mybatis-spring-boot-start method, just use @Component/@Beanthe class to register to the container

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

application

Replace the information contained in sql according to whether the method contains dynamically switched annotation identifiers

yml

Specify the placeholder identifier to be replaced in the xml file: @dynamicSql and the date condition to be replaced.

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicSql {
    
    
}

Dao layer code

Add the @DynamicSql annotation to the method that needs to be replaced by the SQL placeholder.

public interface DynamicSqlMapper  {
    
    
    @DynamicSql
    Long count();

    Long save();
}

xml

<!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>

startup class

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

Interceptor core code

@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) {
    
    
        // 获取配置文件中的属性值
    }
}

code testing

@SpringBootTest(classes = DmApplication.class)
public class DynamicTest {
    
    

    @Autowired
    private DynamicSqlMapper dynamicSqlMapper;

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

insert image description here

Interceptor Application Scenarios

1. SQL statement execution monitoring: It can intercept the executed SQL method, print the executed SQL statement, parameters and other information, and also record the total execution time, which can be used for later SQL analysis.
2. SQL paging query: The memory paging used by RowBounds used in MyBatis will query all eligible data before paging, and the performance is poor when the amount of data is large. Through the interceptor, the SQL statement can be modified before the query, and the required paging parameters can be added in advance.
3. Assignment of public fields: There are usually public fields such as createTime and updateTime in the database. These fields can be assigned to parameters uniformly by intercepting them, thus eliminating the cumbersome process of manually assigning values ​​through the set method.
4. Data permission filtering: In many systems, different users may have different data access permissions. For example, in a multi-tenant system, to achieve data isolation between tenants, each tenant can only access its own data. By rewriting SQL statements and parameters through interceptors, automatic filtering of data can be achieved.
5. SQL statement replacement: logically replace conditions or special characters in SQL. (also the application scenario of this article)

Guess you like

Origin blog.csdn.net/qq_45297578/article/details/132029683