某天,由于业务需要,要对系统里的每张业务表增加每条记录的创建者,创建时间,最新更新者,最新更新时间这些审计信息字段,想到每张表和每个业务逻辑上都需要增加类似的代码片段,虽然很简单,但改动涉及的类很多,而且未来其他人增加新逻辑时,也非常容易忘记加上这段逻辑;所以想实现一个技术方案来统一处理审计信息字段填充,避免掺杂到各个业务逻辑中。于是,就想到了使用 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<Field> 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 增加插件声明,解决了。
<configuration
<plugins>
<plugin interceptor="com.example.AuditInterceptor">
</plugin>
</plugins>
</configuration>
复制代码
可是为什么呢?还是要回到 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<SqlSessionFactory> 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);
}
}
}
复制代码
参考资料:
1.http://mybatis.org/spring/zh/boot.html
2.http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
3.https://mybatis.org/mybatis-3/configuration.html#properties
4.https://mybatis.org/mybatis-3/configuration.html#settings
https://pagehelper.github.io/
评论