Bootstrap

为啥写的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);
        }
    }
}

参考资料:

1.

2.

3.

4.