【Spring】Spring boot多数据源历险记

一、问题描述

笔者根据需求在开发过程中,需要在原项目的基础上(单数据源),新增一个数据源C,根据C数据源来实现业务。至于为什么不新建一个项目,大概是因为这只是个小功能,访问量不大,不需要单独申请个服务器。T^T

当笔者添加完数据源,写完业务逻辑之后,跑起来却发现报了个错。

Caused by: nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate 
[org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping]: Factory method 
'requestMappingHandlerMapping' threw exception; nested exception is org.springframework.beans.factory.
BeanCreationException: Error creating bean with name 'openEntityManagerInViewInterceptor': Initialization of bean failed; 
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching 
bean but found 2: customerEntityManagerFactory, orderEntityManagerFactory

描述的很清晰:就是openEntityManagerInViewInterceptor初始化Bean的时候,注入EntityManagerFactory失败。因为Spring发现了两个。于是不知道该注入哪个,从而导致报错,项目无法启动。

先说一下项目的相关架构,附上pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>multi-datasource</artifactId>
        <groupId>io.github.joemsu</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>multi-datasource-problem</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.github.joemsu</groupId>
            <artifactId>multi-datasource-dao</artifactId>
        </dependency>
    </dependencies>
</project>

二、代码再现

GitHub地址:Joemsu/multi-datasource

我们先来看一下如何实现的多数据源

2.1 数据源配置

@Configuration
public class DataSourceConfig {

  // 注意这里的@Primary,后面会提到
  @Primary
  @Bean(name = "customerDataSource")
  @ConfigurationProperties(prefix = "io.github.joemsu.customer.datasource")
  public DataSource customerDataSource() {
    return DataSourceBuilder.create().build();
  }

  @Bean(name = "orderDataSource")
  @ConfigurationProperties(prefix = "io.github.joemsu.order.datasource")
  public DataSource orderDataSource() {
    return DataSourceBuilder.create().build();
  }

}

数据源配置很简单,申明两个DataSource的bean,分别采用不同的数据源配置,@ConfigurationProperties从application.yml的文件里读取配置信息。

io:
  github:
    joemsu:
      customer:
        datasource:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/customer?characterEncoding=UTF-8&amp;useSSL=false
          username: root
          password: 123456
      order:
        datasource:
          url: jdbc:mysql://127.0.0.1:3306/orders?characterEncoding=UTF-8&amp;useSSL=false
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password: 123456
      jpa:
        properties:
          hibernate.hbm2ddl.auto: update
logging:
  level: debug

2.2 Spring Data Jpa配置

数据源一的EntityManagerFactory配置:

package io.github.joemsu.customer.config;

/**
 * @author joemsu 2017-12-11 下午3:29
 */
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "customerEntityManagerFactory",
        transactionManagerRef = "customerTransactionManager",
        basePackages = "io.github.joemsu.customer.dao")
public class CustomerRepositoryConfig {


    @Autowired(required = false)
    private PersistenceUnitManager persistenceUnitManager;

    @Bean
    @ConfigurationProperties("io.github.joemsu.jpa")
    public JpaProperties customerJpaProperties() {
        return new JpaProperties();
    }

    @Bean
    public EntityManagerFactoryBuilder customerEntityManagerFactoryBuilder(
            @Qualifier("customerJpaProperties") JpaProperties customerJpaProperties) {
        AbstractJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        return new EntityManagerFactoryBuilder(adapter,
                customerJpaProperties.getProperties(), this.persistenceUnitManager);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory(
            @Qualifier("customerEntityManagerFactoryBuilder") EntityManagerFactoryBuilder builder,
            @Qualifier("customerDataSource") DataSource customerDataSource) {
        return builder
                .dataSource(customerDataSource)
                .packages("io.github.joemsu.customer.dao")
                .persistenceUnit("customer")
                .build();
    }

    @Bean
    public JpaTransactionManager customerTransactionManager(@Qualifier("customerEntityManagerFactory") EntityManagerFactory customerEntityManagerFactory) {
        return new JpaTransactionManager(customerEntityManagerFactory);
    }
}

数据源二的EntityManagerFactory配置:

package io.github.joemsu.order.config;

/**
 * @author joemsu 2017-12-11 下午3:29
 */
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "orderEntityManagerFactory",
        transactionManagerRef = "orderTransactionManager",
        basePackages = "io.github.joemsu.order.dao")
public class OrderRepositoryConfig {

    @Autowired(required = false)
    private PersistenceUnitManager persistenceUnitManager;

    @Bean
    @ConfigurationProperties("io.github.joemsu.jpa")
    public JpaProperties orderJpaProperties() {
        return new JpaProperties();
    }

    @Bean
    public EntityManagerFactoryBuilder orderEntityManagerFactoryBuilder(
            @Qualifier("orderJpaProperties") JpaProperties orderJpaProperties) {
        AbstractJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        return new EntityManagerFactoryBuilder(adapter,
                orderJpaProperties.getProperties(), this.persistenceUnitManager);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory(
            @Qualifier("orderEntityManagerFactoryBuilder") EntityManagerFactoryBuilder builder,
            @Qualifier("orderDataSource") DataSource orderDataSource) {
        return builder
                .dataSource(orderDataSource)
                .packages("io.github.joemsu.order.dao")
                .persistenceUnit("orders")
                .build();
    }

    @Bean
    public JpaTransactionManager orderTransactionManager(@Qualifier("orderEntityManagerFactory") EntityManagerFactory orderEntityManager) {
        return new JpaTransactionManager(orderEntityManager);
    }
}

至于其他的代码可以去笔者的GitHub上看到,就不提了。

三、解决方案以及原因探究

3.1 解决方案一

像之前提到的,既然Spring不知道要注入哪一个,那么我们指定它来注入一个不就行了吗?于是,我在CustomerRepositoryConfigEntityManagerFactoryBuilder中添加了@Primary,告诉Spring在注入的时候优先选择添加了注解的这个,最终问题得以解决。

3.2 原因探究

虽然解决了问题,可以成功启动,但是这无疑是饮鸩止渴,因为不知道为什么要注入就不知道会出现什么问题,万一哪天出现了问题。。 (ಥ_ಥ)

openEntityManagerInViewInterceptor开始,一顿调试打断点之后,最终整理出了一套的调用过程由于涉及到了10来个class,这里贴出部分代码,其余的简单说一下:

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
        WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
public class WebMvcAutoConfiguration {
  
  @Configuration
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {

        @Bean
        @Primary
        @Override
        public RequestMappingHandlerMapping requestMappingHandlerMapping() {
            return super.requestMappingHandlerMapping();
        }
}

“罪魁祸首“就是Spring boot 的自动化配置,在开发者没有自动配置WebMvcConfigurationSupport的情况下,Spring boot的WebMvcAutoConfiguration会自动实现配置,在这配置里,有一个EnableWebMvcConfiguration配置类,里面申明了一个RequestMappingHandlerMappingbean。

  1. WebMvcAutoConfiguration.EnableWebMvcConfiguration ->requestMappingHandlerMapping()
  2. DelegatingWebMvcConfiguration ->requestMappingHandlerMapping(),在该方法里调用了RequestMappingHandlerMapping的setInterceptors(this.getInterceptors())
  3. this.getInterceptors()里有一个addInterceptors()方法,通过迭代器来添加拦截器,迭代器中就有JpaBaseConfiguration里的JpaWebConfigurationJpaWebMvcConfigurationaddInterceptors调用
  4. JpaWebMvcConfigurationaddInterceptors里面申明了OpenEntityManagerInViewInterceptorbean,该bean继承了EntityManagerFactoryAccessor。让我们来看一下里面的代码:
public abstract class EntityManagerFactoryAccessor implements BeanFactoryAware {
  // 实现了BeanFactoryAware的类会调用setBeanFactory方法
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    if (this.getEntityManagerFactory() == null) {
      if (!(beanFactory instanceof ListableBeanFactory)) {
        throw new IllegalStateException("Cannot retrieve EntityManagerFactory by persistence unit name in a non-listable BeanFactory: " + beanFactory);
      }
      ListableBeanFactory lbf = (ListableBeanFactory)beanFactory;
      //在ListableBeanFactory中找到EntityManagerFactory类型的class,也就是这里报的错
      this.setEntityManagerFactory(EntityManagerFactoryUtils.
                              findEntityManagerFactory(lbf, this.getPersistenceUnitName()));
    }

  }
}

那么这个OpenEntityManagerInViewInterceptor有什么用呢?

在该类上面的注解是这么说明的:

Spring web request interceptor that binds a JPA EntityManager to the thread for the entire processing of the request. Intended for the "Open EntityManager in View" pattern, i.e. to allow for lazy loading in web views despite the original transactions already being completed.

也就是说,在web的请求过来的时候,给当前的线程绑定一个EntityManager,用来处理web层的懒加载问题。

为此笔者做了一个测试:

/**
 * @author joemsu 2017-12-07 下午4:29
 */
@RestController
@RequestMapping("/")
public class TestController {

    private final CustomerOrderService customerOrderService;

    @Autowired
    public TestController(CustomerOrderService customerOrderService) {
        this.customerOrderService = customerOrderService;
    }

    //由于默认注入的是Customer的EntityManagerFactory,所以可以获取懒加载对象
    @RequestMapping("/session")
    public String session() {
        customerOrderService.getCustomerOne(1L);
        return "success";
    }

    /** 
    * 新开了一个线程,而EntityManger绑定的不是该线程,
    * 因此虽然注入的是customerEntityManagerFactory
    * 但还是抛出 LazyInitializationException异常
    */
    @RequestMapping("/nosession1")
    public String nosession1() {
        new Thread(() -> customerOrderService.getCustomerOne(1L)).start();
        return "could not initialize proxy - no Session";
    }

    /**
    * 虽然在当前请求开启了EntityManager
    * 但是注入的是customerEntityManagerFactory
    * 所以对Order的懒加载并没有用,抛出 LazyInitializationException异常
    */
    @RequestMapping("/nosession2")
    public String nosession2() {
        customerOrderService.getOrderOne(1L);
        return "could not initialize proxy - no Session";
    }
}

这里的CustomerOrderService调用了JPA Repository里的getOne()方法,采用了懒加载,这样就不用花费心思来进行@ManyToOne这种操作。具体的代码可以看Github上的项目。

3.3 解决方案二

既然知道了具体的原因,那么我们可以直接关掉OpenEntityManagerInViewInterceptor,具体方法如下:

spring:
  jpa:
    open-in-view: false

再进行尝试,果然不会再报错。

OpenEntityManagerInViewInterceptor帮我们在请求中开启了事务,使我们少做了很多事,但是在多数据源的情况下,并不十分实用。况且,笔者认为现在已经很少用到懒加载,最初的时候(笔者读大学的时候),会用到@ManyToOne,采用外键的形式,懒加载的方式从数据库获取对象。但是现在,在大数据的时代下,外键这种方式太损耗性能,已经渐渐被废弃,采用单表查询,封装DTO的方式。所以笔者觉得关闭也是一种的选择。

3.4 解决方法三(待验证)

笔者在搜索的时候,无意中在GitHub的Spring项目上发现了一个解决方案:https://github.com/spring-projects/spring-boot/issues/1702,作者提到:

  1. Sometimes there's no primary
  2. The bean is defined using a namespace and does not offer an easy way to expose it as a primary bean

看来多数据源情况下的问题也困扰了很多的开发者,于是该作者提交了一个分支,采用@ConditionalOnSingleCandidate的注解:在可能出现多个bean,但是只能注入一个的情况下,如果添加了该注解,那么该配置就不会生效,于是解决了无法启动的情况。但是问题也有:既然该自动化配置不能生效就意味着我们要自己写,也是一个比较麻烦的问题。T^T

据说在测试Spring boot的2.0.0 M7中已经有了该注解,但是笔者还没去验证过,有兴趣的园友们可以自己去尝试一下。

四、再掀波澜

照理说问题解决了,那么笔者应该美滋滋的提交一波然后测试,然而。。

笔者又看到了前面的配置DataSource的文件中有一个@Primary,于是手贱去掉,然后。。(ಥ_ಥ)

果然又报了一个错,这个问题调试很简单,有兴趣的园友可以自己去尝试一下,看一下DataSourceInitializer

然而,事情还没有这么简单。。

在查看GitHub上的issue的过程中,笔者看到了这一段话:

I see. The point here is that making one DataSource the primary one can be a source of errors as you could @Transactional (without an explicit qualifier) by accident and thus run transactions on the "wrong" one. In the scenario I have here, both DataSources should be treated equally and not referring to one explicitly is rather considered an error.

看完之后我在想:如果两个数据源一起操作,抛出了异常,是不是事务会出错?从理论上来说是肯定的,因为只能@Transactional只能注入一个TransactionManager,管理一个数据源。于是笔者做了一个demo进行了测试:

@Transactional(rollbackFor = Exception.class)
public void create() {
  Customer customer = new Customer();
  customer.setFirstName("John");
  customer.setLastName("Smith");
  this.customerRepository.save(customer);
  Order order = new Order();
  order.setCustomerId(123L);
  order.setOrderDate(new Date());
  this.orderRepository.save(order);
  throw new RuntimeException("11231");
}

运行完查看数据库后。。

跟笔者想的一样,只回滚了@Primary的数据,另一个数据源则直接插入了要回滚的数据。

后面的解决方法就是采用Atomikos,代码也扔在了我的GitHub上。

4.1 用Atomikos解决多数据源事务问题

JTA的思路是:通过事务管理器来协调多个资源, 而每个资源由资源管理器管理,事务管理器承担着所有事务参与单元的协调与控制。

/**
 * @author joemsu 2017-12-11 下午5:16
 */
@Configuration
public class DataSourceConfig {

  @Bean
  @Primary
  @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.customer")
  public DataSource customerDataSource() {
    return new AtomikosDataSourceBean();
  }


  @Bean
  @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.order")
  public DataSource orderDataSource() {
    return new AtomikosDataSourceBean();
  }


  @Bean(destroyMethod = "close", initMethod = "init")
  public UserTransactionManager userTransactionManager() {
    UserTransactionManager userTransactionManager = new UserTransactionManager();
    userTransactionManager.setForceShutdown(false);
    return userTransactionManager;
  }

  /**
  * jta transactionManager
  *
  * @return
  */
  @Bean(name = "jtaTransactionManager")
  @Primary
  public JtaTransactionManager transactionManager() {
    JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
    jtaTransactionManager.setTransactionManager(userTransactionManager());
    return jtaTransactionManager;
  }
}

Spring boot 提供了一个spring-boot-starter-jta-atomikos,引入后稍微配置即可实现。最后将JtaTransactionManager设置为Primary,统一由它来进行事务管理

application.yml配置:

spring:
  jta:
    log-dir: ./
    atomikos:
      datasource:
        customer:
          xa-properties:
            url: jdbc:mysql://127.0.0.1:3306/customer?characterEncoding=UTF-8&amp;useSSL=false
            user: root
            password: "123456"
          xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
          unique-resource-name: customer
          max-pool-size: 25
          min-pool-size: 3
          max-lifetime: 20000
          borrow-connection-timeout: 10000
        order:
          xa-properties:
            url: jdbc:mysql://127.0.0.1:3306/orders?characterEncoding=UTF-8&amp;useSSL=false
            user: root
            password: "123456"
          xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
          unique-resource-name: order
          max-pool-size: 25
          min-pool-size: 3
          max-lifetime: 20000
          borrow-connection-timeout: 10000
    enabled: true

最后经过测试,在抛出异常后,两个数据源都发生了回滚。

另外推荐一个介绍的文章:JTA 深度历险

五、总结

诚然,Spring Boot帮我们简化了很多配置,但是对于不了解其底层实现的开发者来说,碰到问题解决起来也不容易,或许这就需要时间的沉淀来解决了吧。另外有解读不对的地方可以留言指正,最后谢谢各位园友观看,与大家共同进步!

参考链接:

http://www.importnew.com/25381.html

http://sadwxqezc.github.io/HuangHuanBlog/framework/2016/05/29/Spring%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E9%85%8D%E7%BD%AE.html

https://github.com/spring-projects/spring-boot/issues/5541

https://github.com/spring-projects/spring-boot/issues/1702

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏古时的风筝

用java开发微信公众号:公众号接入和access_token管理(二)

上一篇说了微信开发的准备工作,准备工作完成之后,就要开始步入正题了。其实微信公众号开发,说白了,就是要构造和发送http或https的请求组成,并根据请求的返回...

4176
来自专栏jeremy的技术点滴

Java监听目录文件变更

2807
来自专栏zhisheng

渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)

上篇文章写了 ElasticSearch 源码解析 —— 环境搭建 ,其中里面说了启动 打开 server 模块下的 Elasticsearch 类:org.e...

811
来自专栏开发之途

在 Android 设备上搭建 Web 服务器

一般而言,Android 应用在请求数据时都是以 Get 或 Post 等方式向远程服务器发起请求,那你有没有想过其实我们也可以在 Android 设备上搭建一...

1593
来自专栏Java编程

java web开发——购物车功能实现

之前没有接触过购物车的东东,也不知道购物车应该怎么做,所以在查询了很多资料,总结一下购物车的功能实现。

8132
来自专栏Hadoop实操

如何开发HBase Endpoint类型的Coprocessor以及部署使用

1162
来自专栏纯洁的微笑

Spring Boot 2.0 版的开源项目云收藏来了!

713
来自专栏菩提树下的杨过

redis 学习笔记(5)-Spring与Jedis的集成

首先不得不服Spring这个宇宙无敌的开源框架,几乎整合了所有流行的其它框架,http://projects.spring.io/spring-data/ 从这...

2117
来自专栏Java后端技术

使用Slf4j集成Log4j2构建项目日志系统的完美解决方案

  最近因为公司项目性能需要,我们考虑把以前基于的log4j的日志系统重构成基于Slf4j和log4j2的日志系统,因为,使用slf4j可以很好的保证我们的日志...

634
来自专栏java学习

使用intellij idea搭建MAVEN+SSM(Spring+SpringMVC+MyBatis)框架

Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-On...

885

扫码关注云+社区