一、问题描述

笔者根据需求在开发过程中,需要在原项目的基础上(单数据源),新增一个数据源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

【Spring】Spring boot多数据源历险记的更多相关文章

  1. Spring Boot 多数据源自动切换

    在Spring Boot中使用单数据源的配置很简单,我们简单回忆下:只需要在application.properties进行基本的连接配置,在pom.xml引入基本的依赖即可. 那么多数据源的原理呢? ...

  2. Spring Boot多数据源

    我们在开发过程中可能需要用到多个数据源,我们有一个项目(MySQL)就是和别的项目(SQL Server)混合使用了.其中SQL Server是别的公司开发的,有些基本数据需要从他们平台进行调取,那么 ...

  3. Spring Boot多数据源配置(二)MongoDB

    在Spring Boot多数据源配置(一)durid.mysql.jpa 整合中已经讲过了Spring Boot如何配置mysql多数据源.本篇文章讲一下Spring Boot如何配置mongoDB多 ...

  4. (43). Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】

    在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库.那么这一节我们将要介绍怎么进行多数据源动态切换.添 ...

  5. (42)Spring Boot多数据源【从零开始学Spring Boot】

    我们在开发过程中可能需要用到多个数据源,我们有一个项目(MySQL)就是和别的项目(SQL Server)混合使用了.其中SQL Server是别的公司开发的,有些基本数据需要从他们平台进行调取,那么 ...

  6. 关于Spring Boot 多数据源的事务管理

    自己的一些理解:自从用了Spring Boot 以来,这近乎零配置和"约定大于配置"的设计范式用着确实爽,其实对零配置的理解是:应该说可以是零配置可以跑一个简单的项目,因为Spri ...

  7. Springboot spring data jpa 多数据源的配置01

    Springboot spring data jpa 多数据源的配置 (说明:这只是引入了多个数据源,他们各自管理各自的事务,并没有实现统一的事务控制) 例: user数据库   global 数据库 ...

  8. HBase 学习之路(十一)—— Spring/Spring Boot + Mybatis + Phoenix 整合

    一.前言 使用Spring+Mybatis操作Phoenix和操作其他的关系型数据库(如Mysql,Oracle)在配置上是基本相同的,下面会分别给出Spring/Spring Boot 整合步骤,完 ...

  9. HBase 系列(十一)—— Spring/Spring Boot + Mybatis + Phoenix 整合

    一.前言 使用 Spring+Mybatis 操作 Phoenix 和操作其他的关系型数据库(如 Mysql,Oracle)在配置上是基本相同的,下面会分别给出 Spring/Spring Boot ...

随机推荐

  1. VMware仅主机模式访问外网

    原文转载至:https://blog.csdn.net/eussi/article/details/79054622 保证VMware Network Adapter VMnet1是启用状态  将可以 ...

  2. ubuntu 16.04 LTS 安装 teamviewer 13

    背景介绍 由于需要做现场的远程支持,经协商后在现场的服务器上安装TeamViewer 以便后续操作. 本来以为很简单的一件事,谁知却稍微费了一番周折  :( 记录下来,希望提醒自己的同时也希望能够帮到 ...

  3. jQuery屏蔽浏览器的滚动事件,定义自己的滚轮事件

    1.首先应用jQuery库 ,不做详细介绍 2引用jQuery的mousewheel库,这里面是这个库的源码,使用时直接拷贝过去就可以了: (function(a){function d(b){var ...

  4. QEMU,KVM及QEMU-KVM介绍

    What's QEMU QEMU是一个主机上的VMM(virtual machine monitor),通过动态二进制转换来模拟CPU,并提供一系列的硬件模型,使guest os认为自己和硬件直接打交 ...

  5. PCB的初次窥探

    第一次画PCB经常用到的知识点 鼠标拖动+X      :左右转动(对称) +space:90度转动 +L      :顶层与底层的切换 Ctrl+M:测量 J + C:查找原件 交叉探针+原理图(P ...

  6. 基于Redis位图实现用户签到功能

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  7. CentOS 6.6下Cacti安装部署

    Cacti简介 本章结构 常见平台 常见的服务器监控软件 cacti,流量与性能监测为主----http://www.cacti.net/ nagios,服务与性能监测为主---http://www. ...

  8. CentOS 7.4 安装部署 iRedMail 邮件服务器

    在公司部署了一套开源的邮件网关Scrollout F1用来测试,由于Scrollout F1需要使用IMAP协议连接到邮件服务器上的隔离邮箱,抓取GOOD和BAD文件夹里的邮件进行贝叶斯学习,但公司的 ...

  9. Nginx相关笔记

    相关参考: 编译安装测试nginx            https://www.cnblogs.com/jimisun/p/8057156.html

  10. VB6 让程序结束后带有返回值

    第三方命令行程序运行完之后,批处理中可以随时通过errorlevel变量收取运行结果.而VB写的控制台程序却没有提供这样的功能.关于让控制台程序返回值的教程是本博客独家放出. 返回值,其实也就是进程的 ...