背景:
最近由于公司要做统一的数据变更记录,以前是基于Aop来做的,这样效率很低,而且在做批量处理(insert,update,delete)操作时基本不可用。所以我打算使用CDC(如Canal,Maxwell等工具)来监听mysql的binlog来做。但是不是所有的表都会有user_id字段,所以我们须要在sql上做一些处理,因为公司现在统一用的是mybatis,那么现在我觉得比较好的方式就是在mybatis上进行拦截改造sql.将userId从应用层获取到并写入到须要执行的sql上(只对insert,update,delete记录)。
如:有如下sql: update table set a= 1 where name =3
改造的结果就是:/** userId:1,traceId:123456**/ update table set a= 1 where name =3
这样我们就可以记录一次操作改了哪些数据,改数据的人是哪个。
开始干:
这里面有几个技术点,且都不怎么复杂,今天我们只聊mybatis拦截器。其实写一个拦截器还是很简单的,网上有很多的代码。代码写完后,突然发现有些项目的自定义mybatis拦截器没有生效。于是就开始google研究了一下,发现是因为我们这些不生效的项目使用了PageHelper.于是找了一些大神的解决方案,和拦截器的顺序有关。先说一下结论:
MyBatis的拦截器采用责任链设计模式,多个拦截器之间的责任链是通过动态代理组织的。我们一般都会在拦截器中的intercept方法中往往会有invocation.proceed()语句,其作用是将拦截器责任链向后传递,本质上便是动态代理的invoke。
PageHelper在intercept方法中执行完后没有执行invocation.proceed(),意味着这玩意儿没有继续传递责任链(可能他有自己的想法)。所以他就没有进入我们自己的拦截器。
注意,敲黑板:
A.不是所有的拦截器都必须要指定先后顺序。
拦截器的调用顺序分为两大种,第一种是拦截的不同对象,例如拦截 Executor 和 拦截 StatementHandler 就属于不同的拦截对象, 这两类的拦截器在整体执行的逻辑上是不同的,在 Executor 中的 query 方法执行过程中会调用StatementHandler。
所以StatementHandler 属于 Executor 执行过程中的一个子过程。 所以这两种不同类别的插件在配置时,一定是先执行 Executor 的拦截器,然后才会轮到 StatementHandler。所以这种情况下配置拦截器的顺序就不重要了,在 MyBatis 逻辑上就已经控制了先后顺序。
所以如果你一个是Executor 类型的拦截器,一个是StatementHandler类型的拦截器,你可以不用管他顺序,也就是说你只须要定义好类型都Executor的拦截器顺序。
B.类型都为Executor的拦截器顺序问题:
如果你的拦截器定义的顺序是这样的(你可以通过获取sqlSessionFactory.getConfiguration()去查看里面的InterceptorChain然后看到各个interceptor的顺序):
<plugins>
<plugin interceptor="com.github.pagehelper.ExecutorQueryInterceptor1"/>
<plugin interceptor="com.github.pagehelper.ExecutorQueryInterceptor2"/>
<plugin interceptor="com.github.pagehelper.ExecutorQueryInterceptor3"/>
</plugins>
他执行的顺序不是先执行1,2,3,而执行的顺序是3,2,1。
Interceptor3:{
Interceptor2: {
Interceptor1: {
target: Executor
}
}
}
从这个结构应该就很容易能看出来,将来执行的时候肯定是按照 3>2>1>Executor>1>2>3 的顺序去执行的。 可能有些人不知道为什么3>2>1>Executor之后会有1>2>3,这是因为使用代理时,调用完代理方法后,还能继续进行其他处理。处理结束后,将代理方法的返回值继续往外返回即可。
C(解决方案).因为PageHelper是Excetor类型的拦截器,所以按照前面两条的理论,我们如果想要在PageHelper拦截器前面执行,就必须要将我们自己的拦截器添加到他的拦截器后面。
那该怎么做呢?
我们可以通过这种方式来做:
我们去看PageHelperAutoConfiguration的代码是不是发现,该类上面有一个@AutoConfigureAfter(MybatisAutoConfiguration.class)注解,这表明他是在MybatisAutoConfiguration加载完成后,才执行自己的加载。那么我们是不是可以也可以构建一个类似的代码呢,虽然我们不是一个starter,但是我们可以通过这种操作来实现我们的须求。
(1)在src/main/resources/META-INF目录下面,创建一个spring.factories的文件
(2)spring.factories里的内容是:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.llyt.exculd.TestLogAutoConfiguration
这个com.llyt.exculd.TestLogAutoConfiguration,就是你自己的配置类的全路径。该类的代码在后面。
(3) TestLogAutoConfiguration代码:
@Configuration
@AutoConfigureAfter(PageHelperAutoConfiguration.class)
public class TestLogAutoConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addMyInterceptor() {
ExampleOnePlugin e = new ExampleOnePlugin();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(e);
}
}
}
至此,这种方法就OK了。但是你可能会执行不成功,该类的addMyInterceptor方法总是先于PageHelperAutoConfiguration的addPageInterceptor()方法执行,这就意味着你的拦截器总是添加到在pageHelper拦截前面的,那么他总是在PageHelper拦截器后面执行。
如果出现这种情况,说明你可能在spring boot主类上配置了
@ComponentScan("****"),且该类会被这个扫描到,这个就是导致的原因所在。
这里面有一个知识点就是,不是配置了@AutoConfigureAfter(A.class)就一定表示该类一定在A类后面执行。
如果配置类在 spring.factories 中配置了且而如果你的类被自己 Spring Boot 启动类扫描到了,那么该类会被会优先扫描到,配置类对顺序有要求时就会出错。
那么该怎么解决呢?
解决的方法有两个:
a.使用骚操作。
如果你将自己的配置类放到特别的包下,不使用 Spring Boot 启动类扫描。完全通过 spring.factories 读取配置就可以实现这个目的。
比如,你@ComponentScan扫描的包是com.bb.cc,那么你就将该配置类放在com.bb.dd包下面。
b.如果你觉得上面这种不习惯,可以用使用excludeFilters :
@ComponentScan(basePackages = {"com.llyt"}, excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com.llyt.exculd.*"))
将你的配置类放在com.llyt.exculd包下面就行了。
至此,mybatis拦截器的不生效的问题,搞完了。
参考文献:
http://xtong.tech/2018/08/01/MyBatis%E6%8B%A6%E6%88%AA%E5%99%A8%E5%9B%A0pagehelper%E8%80%8C%E5%A4%B1%E6%95%88%E7%9A%84%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3/
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md