为啥写的mybatis插件没用?一场mybatis插件加载机制的探索之旅
某天,由于业务需要,要对系统里的每张业务表增加每条记录的创建者,创建时间,最新更新者,最新更新时间这些审计信息字段,想到每张表和每个业务逻辑上都需要增加类似的代码片段,虽然很简单,但改动涉及的类很多,而且未来其他人增加新逻辑时,也非常容易忘记加上这段逻辑;所以想实现一个技术方案来统一处理审计信息字段填充,避免掺杂到各个业务逻辑中。于是,就想到了使用mybatis的插件机制,实现一个自定义的插件来填充创建者,创建时间,最新更新者,最新更新时间这些审计信息字段,代码片段如下:
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class AuditingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取当前的sql操作
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//获取对象属性
Object parameter = invocation.getArgs()[1];
List fields = getFieldList(parameter);
//获取当前用户
String userId = getUserId();
Date currentTime = new Date();
if (SqlCommandType.UPDATE == sqlCommandType) {
//在待插入或更新的对象里,设置最新更新者,最新更新时间
......
} else if (SqlCommandType.INSERT == sqlCommandType) {
//待插入或更新的对象里,设置创建者,创建时间,最新更新者,最新更新时间
......
}
//执行后续插入或更新操作
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
然后自信满满地等待着插件被加载,上面逻辑被执行。what?为啥写的插件没被加载,不是加了@Component吗?求助于搜索引擎,一顿乱搜和尝试,最终通过在mybatis-config.xml增加插件声明,解决了。
可是为什么呢?还是要回到mybatis源代码中去。
首先,由InterceptorChain.java出发,看到最终是由SqlSessionFactoryBean的afterPropertiesSet()实现,而据我们所知,afterPropertiesSet方法是Spring为bean提供了初始化方法,在bean初始化时被调用,由于在我们系统里(使用的是mybatis-spring-boot-starter),SqlSessionFactoryBean并不是作为一个bean注册到spring的,所以这里afterPropertiesSet()是被SqlSessionFactoryBean的getObject方法调用的。


接着,看一下SqlSessionFactoryBean是在哪里被创建的。查看了mybatis源码,发现它是在MybatisAutoConfiguration类中的sqlSessionFactory方法里创建的。
在MybatisAutoConfiguration会扫描所有Interceptor类的bean,然后在MybatisAutoConfiguration类中的sqlSessionFactory方法里,注入到SqlSessionFactoryBean里。
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
......
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
.......
return factory.getObject();
}
但是,由于在项目里定义了SqlSessionFactory的bean(如下)且上述方法声明了ConditionalOnMissingBean,所以导致MybatisAutoConfiguration类中的sqlSessionFactory方法没被调用。
不过,这里调用了SqlSessionFactoryBean的setConfigLocation方法,从而在加载mybatis-config.xml时,实现了Interceptor类的注入;也就是之前为什么在mybatis-config.xml增加插件声明能解决了问题的原因,有点误打误撞的意味。
@Bean(name = "mySessionFactory")
public SqlSessionFactory mySessionFactory(@Qualifier("myDataSource") DataSource dataSource) throws Exception{
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
factory.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
factory.setDataSource(dataSource);
return factory.getObject();
}

至此,真相大白了。
接下来,梳理一下mybatis-spring-boot-starter加载插件的几个地方:
1.如果未注册sqlSessionFactory bean的话,MybatisAutoConfiguration类中的sqlSessionFactory方法,会将所有Interceptor类的bean注入;
2.如果在第一种里,没有声明Interceptor类的bean的话,也可以通过实现ConfigurationCustomizer的bean,在ConfigurationCustomizer中实现的customize方法时,调用configuration.addInterceptor()方法加载插件;
3.如果在sqlSessionFactory bean里,有设置configLocation,那么可以通过mybatis配置文件里定义plugins方式注入;
后来,又研究了一下mybatis分页插件pagehelper,发现它的插件加载实现是这样的:在它的PageHelperAutoConfiguration里会注入所有SqlSessionFactory的Bean,然后通过sqlSessionFactory.getConfiguration().addInterceptor()方法,将创建的PageInterceptor插件注入到mybatis插件机制里。这又提供了一种注入mybatis插件的方案。
@Configuration
@ConditionalOnBean({SqlSessionFactory.class})
@EnableConfigurationProperties({PageHelperProperties.class})
@AutoConfigureAfter({MybatisAutoConfiguration.class})
public class PageHelperAutoConfiguration {
@Autowired
private List sqlSessionFactoryList;
@Autowired
private PageHelperProperties properties;
public PageHelperAutoConfiguration() {
}
@Bean
@ConfigurationProperties(
prefix = "pagehelper"
)
public Properties pageHelperProperties() {
return new Properties();
}
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.putAll(this.pageHelperProperties());
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
Iterator var3 = this.sqlSessionFactoryList.iterator();
while(var3.hasNext()) {
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory)var3.next();
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
参考资料: