1. 程式人生 > >【Spring】多數據源歷險記

【Spring】多數據源歷險記

yacc ryu customer ons 可能 namespace 解讀 容易 簡單

一、問題描述

筆者根據需求在開發過程中,需要在原項目的基礎上(單數據源),新增一個數據源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上。

另外推薦一個介紹的文章: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

【Spring】多數據源歷險記