• 【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分布式事务配置.html

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

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

  • 相关阅读:
    JMS(面向消息中间件)
    ActiveMQ消息中间件知识汇总
    linux安装mysql常见命令
    结果集耗尽时,检查是否关闭结果集时常用sql
    Spring注解驱动开发之事务概念
    nginx 基础
    HTTP原理
    MYSQL----cmake 数据库出错
    php安装Phalcon模块
    docker报错 Failed to start Docker Application Container Engine.
  • 原文地址:https://www.cnblogs.com/joemsu/p/8028210.html
Copyright © 2020-2023  润新知