【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
接口的拦截器类,并重写其中的 intercept
、plugin
和 setProperties
方法。
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 中条件或者特殊字符进行逻辑替换。(也是本文的应用场景)