前言
最近项目使用Mybatis拦截器对数据进行加解密,以下记录如何将拦截器集成到项目中以及在使用过程中踩过的一些小坑,与君共勉
1.Myabtis拦截器是什么?
MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性,从而影响SQL的生成、执行和返回结果,如:来影响Mapper.xml到SQL语句的生成、执行SQL前对预编译的SQL执行参数的修改、SQL执行后返回结果到Mapper接口方法返参POJO对象的类型转换和封装等。
2. 基础介绍
2.1 核心对象
从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:
- Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
- SqlSessionFactory:SqlSession工厂。
- SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
- Executor:MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
- StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
- ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
- ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
- TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
- MappedStatement:MappedStatement维护了一条<select|update|delete|insert>节点的封装。
- SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
- BoundSql:表示动态生成的SQL语句以及相应的参数信息。
2. 2 执行流程
3. 如何使用
3. 1 创建拦截器
创建拦截器并实现org.apache.ibatis.plugin.Interceptor接口
类上添加@Intercepts注解
两种方式使拦截器生效
- 类上添加@Component注解,使其成为Spring管理的Bean(推荐这种方式,但是有坑,后面介绍)
- mybatis配置
##yaml增加以下配置
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
<!-- 创建mybatis-config.xml文件,内容如下-->
<?xml version="1.0" encoding="UTF-8" ?>
<!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>
3. 2 添加注解
MyBatis拦截器默认可以拦截的类型只有四种,即四种接口类型Executor、StatementHandler、ParameterHandler和ResultSetHandler。对于我们的自定义拦截器必须使用MyBatis提供的@Intercepts注解来指明我们要拦截的是四种类型中的哪一种接口。
- @Intercepts:标志该类是一个拦截器
- @Signature:指明该拦截器需要拦截哪一个接口的哪一个方法
@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})
})
3. 2.1 type
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
3. 3 代码示例
以下演示数据加解密过程
首先通过更新拦截器执行数据加密
/**
* 这里拦截数据插入、修改、删除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) {
}
}
4. 踩坑集合
-
不能使用依赖注入的方式,直接在拦截器里面注入其他的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();
}
5. 总结
可以使用Mybatis拦截器来做一些数据过滤、数据加密脱敏、SQL执行时间性能监控和告警,当然也会额外产生一些性能开销,合理利用拦截器将会大大缩减开发成本。