最近项目使用Mybatis拦截器对数据进行加解密,以下记录如何将拦截器集成到项目中以及在使用过程中踩过的一些小坑,与君共勉
MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性,从而影响SQL的生成、执行和返回结果,如:来影响Mapper.xml到SQL语句的生成、执行SQL前对预编译的SQL执行参数的修改、SQL执行后返回结果到Mapper接口方法返参POJO对象的类型转换和封装等。
从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:
创建拦截器并实现org.apache.ibatis.plugin.Interceptor接口
类上添加@Intercepts注解
两种方式使拦截器生效
##yaml增加以下配置
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
DOCTYPE configuration PUBLIC"-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="***.***Interceptor"/>
plugins>
configuration>
MyBatis拦截器默认可以拦截的类型只有四种,即四种接口类型Executor、StatementHandler、ParameterHandler和ResultSetHandler。对于我们的自定义拦截器必须使用MyBatis提供的@Intercepts注解来指明我们要拦截的是四种类型中的哪一种接口。
@Signature注解的参数:
参数 | 描述 |
---|---|
type | 四种类型接口中的某一个接口,如Executor.class 、StatementHandler.class、ParameterHandler.class、ResultSetHandler.class |
method | 对应接口中的某一个方法名,比如Executor的query方法 |
args | 对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法 |
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}),
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
MyBatis拦截器默认会按顺序拦截以下的四个接口中的所有方法:
org.apache.ibatis.executor.Executor //拦截执行器方法
org.apache.ibatis.executor.statement.StatementHandler //拦截SQL语法构建处理
org.apache.ibatis.executor.parameter.ParameterHandler //拦截参数处理
org.apache.ibatis.executor.resultset.ResultSetHandler //拦截结果集处理
具体是拦截这四个接口对应的实现类:
org.apache.ibatis.executor.CachingExecutor
org.apache.ibatis.executor.statement.RoutingStatementHandler
org.apache.ibatis.scripting.defaults.DefaultParameterHandler
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
以下演示数据加解密过程
首先通过更新拦截器执行数据加密
/**
* 这里拦截数据插入、修改、删除sql
* 在数据插入或更新时执行数据加密
*
*
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class}),
})
public class UpdateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
Object parameterObject = invocation.getArgs()[1];
boolean paramMapFlag = false;
if (parameterObject instanceof MapperMethod.ParamMap) { // 当sql类型时UPDATE时,参数稍微有点变化
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject;
Set set = paramMap.keySet();
for (Object key : set) {
paramMap.get(key);
parameterObject = paramMap.get(key);
if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
encrypt(parameterObject); //加密
paramMap.put(key, parameterObject);
paramMapFlag = true;
}
}
invocation.getArgs()[1] = paramMap;
if (paramMapFlag) {
return invocation.proceed();
}
}
encrypt(parameterObject); //加密
invocation.getArgs()[1] = parameterObject;
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
通过返回值拦截器进行数据解密
/**
* 数据解密拦截器,这里拦截返回值并解密
*/
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
decrypt(result); //返回值被拦截后,执行解密解密方法
return result;
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
不能使用依赖注入的方式,直接在拦截器里面注入其他的Bean
原因:拦截器的优先级高于Spring Bean容器的加载顺序
解决方案:通过ApplicationContext存储Bean,通过getBean()的方式获取
拦截器失效的部分场景,比如多数据源配置导致拦截器失效
通此时需要手动创建sessionFactory并指定拦截器
@Bean(name = "sessionFactory")
public SqlSessionFactory sessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
mybatisSqlSessionFactoryBean.setDataSource(dataSource);
mybatisSqlSessionFactoryBean.setPlugins(ResultInterceptor, UpdateInterceptor);
return mybatisSqlSessionFactoryBean.getObject();
}
可以使用Mybatis拦截器来做一些数据过滤、数据加密脱敏、SQL执行时间性能监控和告警,当然也会额外产生一些性能开销,合理利用拦截器将会大大缩减开发成本。