某天,由于业务需要,要对系统里的每张业务表增加每条记录的创建者,创建时间,最新更新者,最新更新时间这些审计信息字段,想到每张表和每个业务逻辑上都需要增加类似的代码片段,虽然很简单,但改动涉及的类很多,而且未来其他人增加新逻辑时,也非常容易忘记加上这段逻辑;所以想实现一个技术方案来统一处理审计信息字段填充,避免掺杂到各个业务逻辑中。于是,就想到了使用 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/
评论