MyBatis
的拦截器采用责任链设计模式,多个拦截器之间的责任链是通过动态代理组织的。我们一般都会在拦截器中的intercept
方法中往往会有invocation.proceed()
语句,其作用是将拦截器责任链向后传递,本质上便是动态代理的invoke
。
前言
在日常开发中,为了能使数据进行分页。通常会向Spring
容器中注入MyBatis-plus
的分页PaginationInnerInterceptor
拦截器。此时码逻辑大致如下:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
其原理为将PaginationInnerInterceptor
加入MybatisPlusInterceptor
的内部拦截链中,并通过@Bean
将MybatisPlusInterceptor
注入到Mybatis
的拦截器中,从而在Sql
执行时实现对数据的分页操作。
如果对MybatisPlusInterceptor
插件原理和Spring
中Bean
对象的初始逻辑掌握不清晰的的话,很可能导致向Spring
容器中注入的自定义的Mybatis
插件不生效。
问题复盘
日常开发中,对于常用的分页插件外,对于项目我们通常会自定义一些Mybatis
插件,从而实现对Sql
的打印、监控、改写等。
为了复现Mybatis-plus
分页插件所导致的问题,此处我们首先自定义一个简单的Mybatis
的插件,其逻辑也很简单,仅是打印一段enter SqlInterceptor
其主要作用在于验证我们的拦截器是否执行。
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
@Slf4j
public class SqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("enter SqlInterceptor......");
return invocation.proceed();
}
上述代码中,我们自定义一个SqlInterceptor
的插件,接下来我们将其与Mybatis-plus
的分页插件一同注入到我们的Spring
容器中。
@Configuration
public class MybatisConfig {
@Bean
public SqlInterceptor sqlInterceptor() {
return new SqlInterceptor();
}
/**
* 配置 MyBatis-Plus 分页插件
* @return MybatisPlusInterceptor 包含分页插件的拦截器实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
通过上述操作,此时在Spring
容器中存在两个Mybaits
的插件,一个是我们自定义的SqlInterceptor
另一个则是Mybatis-plus
的分页插件。按照我们对Mybaits
的认识,在执行sql
时,上述插件应该可以顺利执行,即程序代码中既可以顺利实现数据分页,同时也能在控制台看到"enter SqlInterceptor......"
的提示信息。
为此,我们构建一个简单的控制器,用以检验Mybaits
插件的生效情况。
@GetMapping("t2")
public IPage<User> t2(Integer current ,Integer size){
IPage<User> page = userMapper.selectPage(new Page<>(current,size), null);
return page;
}
代码执行结果如下:
通过上述执行结果,不难发现在程序中数据可顺利实现分页,但却并未看到我们自定义的SqlInterceptor
的执行。换言之,我们自定义的SqlInterceptor
并未执行。
原因定位
对于这类Bean
不生效的例子,我们首先应该想到的原因应该是Bean
是否注入。换言之,我们所需要的Bean
是否注入到Spring
容器中。
回到我们本文的例子,我们在容器Bean
注入时,我们自定义的SqlInterceptor
和Mybatis-plus
提供的MybatisPlusInterceptor
采用了相同的注入方式,同时我们注入注入的分页
插件已经生效,显然相同代码不会出现一个Bean
成功注入,另一个Bean
注入失败的情况,看到此可能还是会有人持有怀疑精神。针对质疑,事实远比无依据的猜测更具说服力。
那面对Mybatis
茫茫多的代码,又该在何处进行断点
调试进行验证呢?众所众知,当Mybatis
内部在初始化 SqlSessionFactory
时,MyBatis
会读取并解析配置文件。在 XMLConfigBuilder
类的 parse
方法里,会调用 parseConfiguration
方法解析配置文件中的各个部分,其中就包含对插件配置的解析。具体逻辑如下:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 其他配置解析...
pluginElement(root.evalNode("plugins"));
// 其他配置解析...
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
进一步来看,pluginElement
方法的作用是解析 <plugins>
标签下的所有 <plugin>
标签,为每个插件创建实例并且添加到 InterceptorChain
中。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
而 InterceptorChain
信息则被维护至Configuration
之中,同时Configuration
被SqlSessionFactory
所持有。所以,如果要找寻Mybatis
中插件的初始化逻辑,我们首先应该找到SqlSessionFactory
的初始化位置,方能顺藤摸瓜的找到Interceptor
的注入的问题。
对于SpringBoot
整合Mybatis-plus
的项目而言,其通常会在 MybatisPlusAutoConfiguration
的配置类中完成Mybatis
相关所需Bean
的注入,具体来看,有关SqlSessionFactory
的构建逻辑如下:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
// .... 省略相关无关代码
// 添加拦截器
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
// .... 省略相关无关代码
}
可以看到Mybatis-plus
在向容器中添加SqlSessionFactory
中注入时,会将容器中全部的插件Interceptor
保存到SqlSessionFactory
之中,所以我们完全可以在!ObjectUtils.isEmpty(this.interceptors)
此处添加断点,以确定自定义的SqlInterceptor
是否注入容器。
通过图示,可以看到自定义SqlInterceptor
确实注入到容器中,既然SqlInterceptor
注入到容器中,为什么我们的SqlInterceptor
没能顺利执行呢?
Mybatis
插件执行逻辑
事实上,如果要搞清楚SqlInterceptor
为什么没能执行,我们需要对Mybatis
的插件的执行顺序有一定的了解。
在 MyBatis
里,拦截器
的执行顺序和注册顺序
是相关的,其在实际拦截逻辑执行时会形成类似洋葱模型的嵌套结构。例如,
<plugins>
<plugin interceptor="com.example.FirstInterceptor"/>
<plugin interceptor="com.example.SecondInterceptor"/>
</plugins>
在上述代码中我们先后定义了SecondInterceptor
和FirstInterceptor
两个拦截器,而在Mybaits
内部构建代理时,其逻辑大致如下:
- 构建调用
FirstInterceptor
的plugin
方法,对目标对象进行第一次包装。 - 调用
SecondInterceptor
的plugin
方法,对已经被FirstInterceptor
包装过的对象进行第二次包装。内部逻辑具体如下所示:
换言之,在执行器执行Sql
时,会按照 SecondInterceptor>FirstInterceptor>Executor>FirstInterceptor>SecondInterceptor
的顺序去执行的。而插件间的执行的传递则主要通过invocation.proceed()
语句来进行向下传递。具体到上述例子,拦截器执行顺序如下:
- 首先,调用
SecondInterceptor
的intercept
方法,此时SecondInterceptor
的intercept
方法开始执行,但在执行到调用下一个拦截器或者目标方法之前,不会结束。 - 然后,进入
FirstInterceptor
的intercept
方法,FirstInterceptor
的intercept
方法开始执行
自定义插件失效原因分析
回到我们本文主题,那为什么我们注册的SqlInterceptor
会失效呢?通过前面对Mybatis
插件执行顺序的介绍。我们知道对于Mybatis
中的插件而言,插件间的调用通过invocation.proceed()
来完成调用,且插件执行顺序其实和插件注册顺序相悖。 在结合我们之前看到的插件读取顺序,可以知道在程序内部,我们插件的执行应该为 MybatisPlusInterceptor -> SqlInterceptor
。
至此,我们自定义插件 SqlInterceptor
失效的原因其实已经呼之欲出了,其无非就是MybatisPlusInterceptor
内部未继续执行invocation.proceed()
来调用我们的自定义插件SqlInterceptor
。
为了验证我们的猜想,我们不妨在MybatisPlusInterceptor
进行断点观察:
可以看到,在MybatisPlusInterceptor
内部在执行插件时,其实是不会执行到 invocation.proceed()
的,这也就是引入MybatisPlusInterceptor
导致自定义插件失效的罪魁祸首。
解决之道
对于引入Mybaits-plus
导致自定义插件失效的解决方法其实有很多种,最简单的就是调换配置类
中插件注入顺序。例如,如下代码
@Configuration
public class MybatisConfig {
/**
* 配置 MyBatis-Plus 分页插件
* @return MybatisPlusInterceptor 包含分页插件的拦截器实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Bean
public SqlInterceptor sqlInterceptor() {
return new SqlInterceptor();
}
}
或者通过ConfigurationCustomizer
来指定拦截器顺序。
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
MyInterceptor myInterceptor = new MyInterceptor();
myInterceptor.setProperties(myInterceptorProperties.getProperties());
configuration.addInterceptor(myInterceptor);
};
}
在这还可以通过@Order
注解或者BeanDefinitionRegistryPostProcessor
来控制Mybaits
自定义拦截器
在容器中的注入顺序。
总之,解决注入Mybaits-plus
分之导致自定义失效的方式有很多种,但其核心逻辑完全在于控制Mybatis-Plus
插件先与我们注解进行注入。当然,如果自定义拦截器切入的拦截点与Mybaits-plus
插件不一样,则完全不用担心此问题插件不生效的问题~~~