1. 概述
在开发时碰到一个需求,是给数据库中的某个字段加密,在查询后返回前端的要是解密后的数据,想到使用mybatis的拦截器解决
。
2. mybatis拦截器介绍
2.1 概述
MyBatis 是一个流行的 Java 持久层框架,它提供了灵活的 SQL 映射和执行功能。有时候我们可能需要在运行时动态地修改 SQL 语句,例如添加一些条件(创建时间、修改时间)、排序、分页等。MyBatis 提供了一个强大的机制来实现这个需求,那就是拦截器
(Interceptor)。
拦截器的作用就是我们可以拦截某些方法的调用,在目标方法前后
加上我们自己逻辑。
Mybatis拦截器设计的一个初衷是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。
2.2 使用方法
实现Interceptor 接口,并添加拦截注解 @Intercepts
2.2.1 四种拦截类型
Executor
:拦截执行器的方法,例如 update、query、commit、rollback 等。可以用来实现缓存、事务、分页等功能。ParameterHandler
:拦截参数的处理,例如 setParameters 等。可以用来转换或加密参数等功能。ResultHandler
:拦截结果集的处理,例如 handleResultSets、handleOutputParameters 等。可以用来转换或过滤结果集等功能。StatementHandler
:拦截Sql语法构建的处理,例如 prepare、parameterize、batch、update、query 等。可以用来修改 SQL 语句、添加参数、记录日志等功能。
2.2.2 @Intercepts注解
对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口,具体规则如下:
Intercepts
拦截器: 标识我的类是一个拦截器Signature
署名: 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法- type 对应四类接口中的某一个,比如是 Executor。
- method 对应接口中的哪类方法,比如 Executor 的 update 方法。
- args 对应接口中的哪一个方法,比如 Executor 中 query 因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法。
2.2.3 示例
@Intercepts({
@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),
@Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})
})
public class MyInterceptor implements Interceptor {
/**
* 这个方法很好理解
* 作用只有一个:我们不是拦截方法吗,拦截之后我们要做什么事情呢?
* 这个方法里面就是我们要做的事情
*
* 解释这个方法前,我们一定要理解方法参数 {@link Invocation} 是个什么鬼?
* 1 我们知道,mybatis拦截器默认只能拦截四种类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
* 2 不管是哪种代理,代理的目标对象就是我们要拦截对象,举例说明:
* 比如我们要拦截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法,
* 那么 Invocation 就是这个对象,Invocation 里面有三个参数 target method args
* target 就是 Executor
* method 就是 update
* args 就是 MappedStatement ms, Object parameter
*
* 如果还是不能理解,我再举一个需求案例:看下面方法代码里面的需求
*
* 该方法在运行时调用
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
/*
* 需求:我们需要对所有更新操作前打印查询语句的 sql 日志
* 那我就可以让我们的自定义拦截器 MyInterceptor 拦截 Executor 的 update 方法,在 update 执行前打印sql日志
* 比如我们拦截点是 Executor 的 update 方法 : int update(MappedStatement ms, Object parameter)
*
* 那当我们日志打印成功之后,我们是不是还需要调用这个query方法呢,如何如调用呢?
* 所以就出现了 Invocation 对象,它这个时候其实就是一个 Executor,而且 method 对应的就是 query 方法,我们
* 想要调用这个方法,只需要执行 invocation.proceed()
*/
/* 因为我拦截的就是Executor,所以我可以强转为 Executor,默认情况下,这个Executor 是个 SimpleExecutor */
Executor executor = (Executor)invocation.getTarget();
/*
* Executor 的 update 方法里面有一个参数 MappedStatement,它是包含了 sql 语句的,所以我获取这个对象
* 以下是伪代码,思路:
* 1 通过反射从 Executor 对象中获取 MappedStatement 对象
* 2 从 MappedStatement 对象中获取 SqlSource 对象
* 3 然后从 SqlSource 对象中获取获取 BoundSql 对象
* 4 最后通过 BoundSql#getSql 方法获取 sql
*/
MappedStatement mappedStatement = ReflectUtil.getMethodField(executor, MappedStatement.class);
SqlSource sqlSource = ReflectUtil.getField(mappedStatement, SqlSource.class);
BoundSql boundSql = sqlSource.getBoundSql(args);
String sql = boundSql.getSql();
logger.info(sql);
/*
* 现在日志已经打印,需要调用目标对象的方法完成 update 操作
* 我们直接调用 invocation.proceed() 方法
* 进入源码其实就是一个常见的反射调用 method.invoke(target, args)
* target 对应 Executor对象
* method 对应 Executor的update方法
* args 对应 Executor的update方法的参数
*/
return invocation.proceed();
}
/**
* 这个方法也很好理解
* 作用就只有一个:那就是Mybatis在创建拦截器代理时候会判断一次,当前这个类 MyInterceptor 到底需不需要生成一个代理进行拦截,
* 如果需要拦截,就生成一个代理对象,这个代理就是一个 {@link Plugin},它实现了jdk的动态代理接口 {@link InvocationHandler},
* 如果不需要代理,则直接返回目标对象本身
*
* Mybatis为什么会判断一次是否需要代理呢?
* 默认情况下,Mybatis只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
* 通过 {@link Intercepts} 和 {@link Signature} 两个注解共同完成
* 试想一下,如果我们开发人员在自定义拦截器上没有指明类型,或者随便写一个拦截点,比如Object,那Mybatis疯了,难道所有对象都去拦截
* 所以Mybatis会做一次判断,拦截点看看是不是这四个接口里面的方法,不是则不拦截,直接返回目标对象,如果是则需要生成一个代理
*
* 该方法在 mybatis 加载核心配置文件时被调用
*/
@Override
public Object plugin(Object target) {
/*
* 看了这个方法注释,就应该理解,这里的逻辑只有一个,就是让mybatis判断,要不要进行拦截,然后做出决定是否生成一个代理
*
* 下面代码什么鬼,就这一句就搞定了?
* Mybatis判断依据是利用反射,获取这个拦截器 MyInterceptor 的注解 Intercepts和Signature,然后解析里面的值,
* 1 先是判断要拦截的对象是四个类型中 Executor、StatementHandler、ParameterHandler、 ResultSetHandler 的哪一个
* 2 然后根据方法名称和参数(因为有重载)判断对哪一个方法进行拦截 Note:mybatis可以拦截这四个接口里面的任一一个方法
* 3 做出决定,是返回一个对象呢还是返回目标对象本身(目标对象本身就是四个接口的实现类,我们拦截的就是这四个类型)
*
* 好了,理解逻辑我们写代码吧~~~ What !!! 要使用反射,然后解析注解,然后根据参数类型,最后还要生成一个代理对象
* 我一个小白我怎么会这么高大上的代码嘛,怎么办?
*
* 那就是使用下面这句代码吧 哈哈
* mybatis 早就考虑了这里的复杂度,所以提供这个静态方法来实现上面的逻辑
*/
return Plugin.wrap(target, this);
}
/**
* 这个方法最好理解,如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,
* 类似Spring中的@Value("${}")从application.properties文件获取
* 这个时候我们就可以使用这个方法
*
* 如何使用?
* 只需要在 mybatis 配置文件中加入类似如下配置,然后 {@link Interceptor#setProperties(Properties)} 就可以获取参数
* <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor">
* <property name="username" value="LiuYork"/>
* <property name="password" value="123456"/>
* </plugin>
* 方法中获取参数:properties.getProperty("username");
*
* 问题:为什么要存在这个方法呢,比如直接使用 @Value("${}") 获取不就得了?
* 原因是 mybatis 框架本身就是一个可以独立使用的框架,没有像 Spring 这种做了很多依赖注入的功能
*
* 该方法在 mybatis 加载核心配置文件时被调用
*/
@Override
public void setProperties(Properties properties) {
String username = properties.getProperty("username");
String password = properties.getProperty("password");
// TODO: 2019/2/28 业务逻辑处理...
}
}
2.2.4 总结
3. 添加拦截器
在springboot中要给mybatis加上这个拦截器,有三种方法,前两种方法在启动项目时不会自动调用自定义拦截器的setProperties方法
。
拦截器顺序
- 不同拦截器顺序
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
- 对于同一个类型的拦截器的不同对象拦截顺序:
在 mybatis 核心配置文件根据配置的位置,拦截顺序是 从上往下
3.1 方法一
直接给自定义拦截器添加一个 @Component
注解,当调用sql时结果如下,可以看到拦截器生效了,但是启动时候并没有自动调用setProperties方法
。
@Component
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class MybatisInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//业务代码
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
3.2 方法二
在配置类里添加拦截器,这种方法结果同上,也不会自动调用setProperties方法。
@Configuration
public class MybatisConfig {
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(Configuration configuration) {
configuration.addInterceptor(new MybatisInterceptor());
}
};
}
}
3.3 方法三
这种方法就是跟以前的配置方法类似,在yml配置文件中指定mybatis的xml配置文件,
注意:config-location属性和configuration属性不能同时指定
mybatis:
config-location: classpath:mybatis.xml
type-aliases-package: me.zingon.pagehelper.model
mapper-locations: classpath:mapper/*.xml
mybatis.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>
<typeAliases>
<package name="me.zingon.pacargle.model"/>
</typeAliases>
<plugins>
<plugin interceptor="me.zingon.pagehelper.interceptor.MyPageInterceptor">
<property name="dialect" value="oracle"/>
</plugin>
</plugins>
</configuration>
可以看到,在启动项目的时候setProperties被自动调用了
注意:前两种方法可以在初始化自定义拦截器的时候通过 @Value 注解直接初始化需要的参数。
4. 问题解决
现在回归到刚开始的问题,如何实现给数据库中存储的字段加密
实现示例:
@Component
@Slf4j
// 拦截查询、更新、包括插入
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Executor executor = (Executor) invocation.getTarget();
MappedStatement arg = (MappedStatement) invocation.getArgs()[0];
String type = ((MappedStatement) invocation.getArgs()[0]).getSqlCommandType().toString();
if (!Objects.isNull(invocation.getArgs()[1])) {
if (type.equals("INSERT")) {
if (invocation.getArgs()[1] instanceof User) {
User user= (User) invocation.getArgs()[1];
// 这里可以重新赋值,然后重新设置加密后的数据
invocation.getArgs()[1] = user;
}
}
if (type.equals("UPDATE")) {
try {
Object object = ((MapperMethod.ParamMap) invocation.getArgs()[1]).get("et");
if (object instanceof User) {
User user= (User) object;
// 这里加密某个字段,然后把新的值放进去
((MapperMethod.ParamMap) invocation.getArgs()[1]).put("et", user);
}
} catch (Exception e) {
}
}
if(type.equals("SELECT")){
Object intercept = invocation.proceed();
if (intercept instanceof List){
List listObj = (List) intercept;
for (Object obj : listObj) {
if (obj instanceof User) {
User user= (User) obj;
// 这里解密字段,放回返回结果里
}
}
}
return intercept;
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}