在前面的文章 API开发(JWT数字签名) 一文中,我们利用SpringMVC 中的HandlerInterceptor接口用来拦截HTTP请求;而MyBatis里也提供了一个拦截器功能用来拦截sql语句。
MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能。
MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性,从而影响SQL的生成、执行和返回结果,如:来影响Mapper.xml到SQL语句的生成、执行SQL前对预编译的SQL执行参数的修改、SQL执行后返回结果到Mapper接口方法返参POJO对象的类型转换和封装等。
根据上面的对Mybatis拦截器作用的描述,可以分析其可能的用途;最常见的就是Mybatis自带的分页插件PageHelper或Rowbound参数,通过打印实际执行的SQL语句,发现我们的分页查询之前,先执行了COUNT(*)语句查询数量,然后再执行查询时修改了SQL语句即在我们写的SQL语句后拼接上了分页语句LIMIT(offset, pageSize);
此外,实际工作中,可以使用Mybatis拦截器来做一些数据过滤、数据加密脱敏、SQL执行时间性能监控和告警等;既然要准备使用它,下面先来了解下其原理;
默认情况下,MyBatis 允许使用插件来拦截的四种相关操作类方法:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
这几个接口之间的关系大概是这样的:
Mybatis整体执行流程:
核心对象
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
Interceptor 接口的定义如下所示:
public interface Interceptor {
//拦截器具体实现
Object intercept(Invocation invocation) throws Throwable;
//拦截器的代理类
Object plugin(Object target);
//添加属性
void setProperties(Properties properties);
}
相关注解:
@Intercepts // 描述:标志该类是一个拦截器
@Signature // 描述:指明该拦截器需要拦截哪一个接口的哪一个方法
// @Signature注解中属性:
type; // 四种类型接口中的某一个接口,如Executor.class;
method; // 对应接口中的某一个方法名,比如Executor的query方法;
args; // 对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法;
下面来看一个自定义的简单Interceptor示例:
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Properties;
@Component
//拦截StatementHandler类中参数类型为Statement的prepare方法(prepare=在预编译SQL前加入修改的逻辑)
//即拦截 Statement prepare(Connection var1, Integer var2) 方法
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Slf4j
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取原始sql
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
// 通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
// 优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
// 先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并修改sql
String mSql = sqlAnnotationEnhance(mappedStatement, boundSql);
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, mSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
/**
* 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并增强sql
* @param id 方法全路径
* @param sqlCommandType sql类型
* @param sql 所执行的sql语句
*/
private String sqlAnnotationEnhance(MappedStatement mappedStatement, BoundSql boundSql) throws ClassNotFoundException {
// 获取到原始sql语句
String sql = boundSql.getSql().toLowerCase();
// sql语句类型 select、delete、insert、update
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
// 数据库连接信息
// Configuration configuration = mappedStatement.getConfiguration();
// ComboPooledDataSource dataSource = (ComboPooledDataSource)configuration.getEnvironment().getDataSource();
// dataSource.getJdbcUrl();
// id为执行的mapper方法的全路径名,如com.cq.UserMapper.insertUser, 便于后续使用反射
String id = mappedStatement.getId();
// 获取当前所拦截的方法名称
String mName = id.substring(id.lastIndexOf(".") + 1);
// 通过类全路径获取Class对象
Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
// 获得参数集合
String paramString = null;
if (boundSql.getParameterObject() != null) {
paramString = boundSql.getParameterObject().toString();
}
// 遍历类中所有方法名称,并匹配上当前所拦截的方法
for (Method method : classType.getDeclaredMethods()) {
if (mName.equals(method.getName())) {
// 判断方法上是否带有自定义@InterceptAnnotation注解
InterceptAnnotation interceptorAnnotation = method.getAnnotation(InterceptAnnotation.class);
if (interceptorAnnotation != null && interceptorAnnotation.flag()) {
log.info("intercept func:{}, type:{}, origin SQL:{}", mName, sqlCommandType, sql);
// 场景1:分页功能: return sql + " limit 1";
if ("select".equals(sqlCommandType.toLowerCase())) {
if (!sql.toLowerCase().contains("limit")) {
sql = sql + " limit 1";
}
}
// 场景2:校验功能 :update/delete必须要有where条件,并且打印出where中的条件
if ("update".equals((sqlCommandType.toLowerCase())) || "delete".equals(sqlCommandType.toLowerCase())) {
if (!sql.toLowerCase().contains("where")) {
log.warn("update or delete not safe!");
}
}
// 场景3:分表: 根据userId哈希,替换interceptorAnnotation注解中的表名为new_table(同一数据源)
if (sql.toLowerCase().contains(interceptorAnnotation.value())) {
String userId = getValue(paramString, "userId");
if (userId != null) {
int num = Integer.parseInt(userId);
// 相同数据源中,模拟分5个表
String new_table = interceptorAnnotation.value().concat("_").concat(String.valueOf(num % 5));
log.info("set table: {}", data_source_id, new_table);
// 替换sql表名
sql = StringUtils.replace(sql, interceptorAnnotation.value(), new_table);
}
}
// 场景4:分库分表: 路由到不同数据源,从参数获取dataSourceType
String sourceType = getValue(paramString, "dataSourceType");
if (sourceType != null) {
// mybatis多数据源配置参考:https://blog.csdn.net/qq_15437629/article/details/131961992
DynamicDataSourceContextHolder.setDataSourceType(sourceType);
} else {
// 也可以根据id 哈希选数据源
}
log.info("new SQL:{}", sql);
return sql;
}
}
}
return sql;
}
String getValue(String param, String key) {
if (param == null) {
return null;
}
String[] keyValuePairs = param.substring(1, param.length() - 1).split(",");
for (String pair : keyValuePairs) {
String[] entry = pair.split("=");
if (entry[0].trim().equals(key)) {
return entry[1].trim();
}
}
return null;
}
}
自定义注解如下:
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InterceptAnnotation {
String value() default "";
/**
* true增强、false忽略
*/
boolean flag() default true;
}
添加插件:
@Component
public class DynamicPluginHelper {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Autowired
private MyPlugin myPlugin;
@PostConstruct
public void addMysqlInterceptor() {
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(myPlugin);
}
}
}
测试结果如下:
sql.MyPlugin : intercept func:findUser, type:SELECT, origin SQL:select * from t_user where id = ?
sql.MyPlugin : set table: t_user_4
sql.MyPlugin : new SQL:select * from t_user_4 where id = ? limit 1
错误描述:
There is no getter for property named 'delegate' in 'class com.sun.proxy.$Proxy32'
错误原因:
1、你有多个拦截器,拦截同一对象的同一行为。测试时避免其他拦截器的干扰可以先把注册的拦截器注释掉。
2、依赖包版本不对
3、拦截器配置类放置的位置不正确,导致包没找到
参考:
https://blog.csdn.net/minghao0508/article/details/124420953
https://blog.csdn.net/qq_36881887/article/details/111589294
https://www.cnblogs.com/simplejavahome/p/16617112.html
https://www.cnblogs.com/nefure/p/16948633.html
https://blog.csdn.net/u011602668/article/details/128735771