SpringBoot项目多数据源配置与MyBatis拦截器生效问题解析

在日常项目开发中,由于某些原因,一个服务的数据源可能来自不同的库,比如:

  1. 对接提供的中间库,需要查询需要的数据
  2. 同步数据,需要将一个库的数据同步到另一个库,做为同步工具的服务
  3. 对接第三方系统,由于时间等原因不方便提供接口,开放数据库提供一些查询视图

在实际项目中,多数据源的配置是常见需求。本博客将详细介绍如何在Spring Boot项目中配置多个数据源,并使用MyBatis进行整合。同时,我们将解决在多数据源配置下自定义拦截器不生效的问题。

配置多数据源方式

我所在项目使用的SpringBoot+Mybatis,首先需要添加不同数据源的配置,一般为了区分数据源,会是在单数据源的基础上再datasource节点和具体配置节点之间自定义数据源的名称,比如default表示默认数据源

ruby 复制代码
spring:
  thymeleaf:
    cache: false
  datasource:
  # 默认数据源配置
    default:
      name: dataSource
      url: jdbc:log4jdbc:Postgresql://ip:port/db?currentSchema=db&ApplicationName=test
      username: ENCRYPT#wwWQEj+qg=
      password: ENCRYPT#Jb0N1GHFwD+MWQRiSfHg==
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        validation-query: select 1
        min-idle: 20
        initial-size: 20
        max-active: 50
   # 第二数据源
    secondary:
      name: dataSource
      url: jdbc:log4jdbc:Postgresql://ip:port/db?currentSchema=db&ApplicationName=test
      username: ENCRYPT#qdTt8x5ss=
      password: ENCRYPT#YvZIPJii8D+MWQRiSfHg==
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        validation-query: select 1
        min-idle: 20
        initial-size: 20
        max-active: 50

因为有了多个数据源,需要手动配置创建数据源实例, 默认数据源添加@Primary注解进行标注。

less 复制代码
@Configuration
public class DataSourceConfig {
​
    /**
     * 构建主数据源
     * @return 数据源对象
     */
    @Primary
    @Bean(name = "defaultDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.default")
    public DataSource defaultDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }
​
    /**
     * 第二数据源
     * @return 数据源对象
     */
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new DruidDataSource();
    }
}

Mybatis配置

使用单数据源时,Mapper文件的扫描一般是在启动类上使用@MapperScan配置扫描路径的,但是多数据源则需要进行区分,扫描不同的路径;

SqlSessionFactory 在 MyBatis 中充当着重要的角色,它通过将配置信息加载到内存中,创建和管理 SqlSession 实例,以及配置和创建 Mapper 对象,为开发者提供了便捷的数据库操作接口。通常情况下,一个应用程序只需要创建一个 SqlSessionFactory 实例,并在整个应用程序的生命周期中共享该实例。多数据源场景下,不同的数据源也需要创建自己的SqlSessionFactory 实例

less 复制代码
@MapperScan(basePackages = {"com.xx.xx.*.mapper", "com.xx.xx.**.mapper",
    "com.xx.xx.report.provider.report.db"},
    sqlSessionFactoryRef = "defaultSqlSessionFactory", sqlSessionTemplateRef = "defaultSqlSessionTemplate")
@Configuration
public class DefaultMybatisConfig {
​
    @Autowired
    @Qualifier("dataSource")
    private DataSource defaultDataSource;
​
    @Resource
    private DictGroupTagInterceptor dictGroupTagInterceptor;
​
    @Resource
    private PageInterceptor pageInterceptor;
​
    @Resource
    private DefaultMybatisInterceptor defaultMyBatisInterceptor;
​
    @Bean(name = "defaultTransactionManager")
    @Primary
    public DataSourceTransactionManager defaultTransactionManager() {
        return new DataSourceTransactionManager(defaultDataSource);
    }
​
    @Primary
    @Bean(name = "defaultSqlSessionFactory")
    public SqlSessionFactory defaultSqlSessionFactory() throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(defaultDataSource);
        // Artery的分页插件,数据字典插件(用于动态替换模式名称)
        sessionFactory.setPlugins(pageInterceptor, dictGroupTagInterceptor, defaultMyBatisInterceptor);
        return sessionFactory.getObject();
    }
​
​
    @Bean(name = "defaultSqlSessionTemplate")
    public SqlSessionTemplate defaultSqlSessionTemplate(
        @Qualifier("defaultSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

对于其他数据源的Mybatis配置也是一样,创建配置类进行配置即可

过程中遇到的问题

服务正常启动,对应数据源的访问也没有问题;一个业务场景需要用到业务表的创建时间,创建时间为null导致系统异常。

排查发现,业务表最近的业务数据的创建人,创建时间,修改人,修改时间字段值都是空的。

这些基础字段并不是通过业务数据的创建和修改赋值的,都是实现Mybatis的拦截器Interceptor进行实现的,说明拦截器并未生效。

判断当前操作是新增或者修改,根据当前登录人信息对基础字段赋值

scss 复制代码
@Override
public Object intercept(Invocation invocation) throws Throwable {
​
    Object[] args = invocation.getArgs();
    IUser currentUser = securityService.getCurrUserInfo();
    if(Objects.isNull(currentUser)){
        log.warn("没有登录的用户在操作数据库,请关注,参数:{},线程:{}",args,Thread.currentThread().getName());
        currentUser = new User();
        currentUser.setCorpId("unkown");
        currentUser.setId("unkown");
    }
    if (args.length > 1) {
        MappedStatement ms = (MappedStatement)args[0];
        try {
            if (SqlCommandType.INSERT.equals(ms.getSqlCommandType())) {
                preInsert(args[1], currentUser);
            } else if (SqlCommandType.UPDATE.equals(ms.getSqlCommandType())) {
                preUpdate(args[1], currentUser);
            }
        } catch (Exception e) {
            log.warn("intercept exception : " + e.getMessage(), e);
        }
    }
​
    return invocation.proceed();
}

那么系统单数据源时,自定义的拦截器是如何生效的呢?

原因就在于MybatisAutoConfiguration类 ,作用是自动配置 MyBatis 相关的组件,包括创建SqlSessionFactoryMybatisAutoConfiguration实现了InitializingBean进行初始化操作,在实例化时注入了Interceptor对象。

通过跟踪代码发现,SqlSessionFactory会在MybatisAutoConfiguration创建中创建实例,而这些自定义的拦截器会在创建SqlSessionFactory实例的时候进行设置。

其中注解@ConditionalOnMissingBean表示容器中不存在某个指定类型或名称的 Bean 时,才会生效。所以在SqlSessionFactory已经存在的实例情况之下并不会创建实例,也就解释了我们配置多数据源情况下自定义拦截器不生效的原因。

所以解决办法就很简单了,在我们自定义的Mybatis配置类中注入自定义的拦截器,设置到SqlSessionFactory当中;

less 复制代码
@Resource
private Interceptor[] interceptors;
​
@Primary
@Bean(name = "defaultSqlSessionFactory")
public SqlSessionFactory defaultSqlSessionFactory() throws Exception {
    final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    sessionFactory.setDataSource(defaultDataSource);
    sessionFactory.setPlugins(interceptors);
    // ...
    return sessionFactory.getObject();
}
相关推荐
Nturmoils5 分钟前
内网穿透原来这么简单:Natapp 从注册到公网访问完整教程
后端
Meta3911 分钟前
Java八股文之Spring Boot 中解决 MySQL 和 Elasticsearch (ES) 的数据一致性问题
java·spring boot·mysql
寻求出路的程序媛17 分钟前
分布式 & 高性能 & 高可用 体系、学习重点、面试点
分布式·后端·面试·性能优化
十年Java程序媛21 分钟前
@Async 深度避坑:事务丢失、MDC 上下文丢失、异常静默吞掉完整修复
java·spring boot
橘子编程22 分钟前
Java邮件发送全攻略:从入门到实战
java·开发语言·spring boot·spring·spring cloud·maven
掘金者阿豪28 分钟前
Let‘s Encrypt 证书到底会不会自动续期?从一次服务器迁移后的证书排查说起
后端
Meta3933 分钟前
Java八股文之Spring Boot中解决Redis和MySQL的数据一致性问题
java·spring boot·redis
Zane199433 分钟前
手写"判断key存不存在"的模板代码太烦?一文讲透 collections 四件套
后端·python
joinwell5235 分钟前
Agent 中断后,原任务如何安全接管?从恢复标记到效果事实
人工智能·后端·架构
sp421 小时前
羽量级的 Java Bean 实体校验器
后端