【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&useSSL=false
username: root
password: 123456
order:
datasource:
url: jdbc:mysql://127.0.0.1:3306/orders?characterEncoding=UTF-8&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不知道要注入哪一个,那么我们指定它来注入一个不就行了吗?于是,我在CustomerRepositoryConfig
的EntityManagerFactoryBuilder
中添加了@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
配置类,里面申明了一个RequestMappingHandlerMapping
bean。
- WebMvcAutoConfiguration.EnableWebMvcConfiguration ->requestMappingHandlerMapping()
- DelegatingWebMvcConfiguration ->requestMappingHandlerMapping(),在该方法里调用了RequestMappingHandlerMapping的
setInterceptors(this.getInterceptors())
。 - 而
this.getInterceptors()
里有一个addInterceptors()
方法,通过迭代器来添加拦截器,迭代器中就有JpaBaseConfiguration
里的JpaWebConfiguration
的JpaWebMvcConfiguration
的addInterceptors
调用 - 在
JpaWebMvcConfiguration
的addInterceptors
里面申明了OpenEntityManagerInViewInterceptor
bean,该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,作者提到:
- Sometimes there's no primary
- 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, bothDataSources
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&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&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多数据源历险记的更多相关文章
- Spring Boot 多数据源自动切换
在Spring Boot中使用单数据源的配置很简单,我们简单回忆下:只需要在application.properties进行基本的连接配置,在pom.xml引入基本的依赖即可. 那么多数据源的原理呢? ...
- Spring Boot多数据源
我们在开发过程中可能需要用到多个数据源,我们有一个项目(MySQL)就是和别的项目(SQL Server)混合使用了.其中SQL Server是别的公司开发的,有些基本数据需要从他们平台进行调取,那么 ...
- Spring Boot多数据源配置(二)MongoDB
在Spring Boot多数据源配置(一)durid.mysql.jpa 整合中已经讲过了Spring Boot如何配置mysql多数据源.本篇文章讲一下Spring Boot如何配置mongoDB多 ...
- (43). Spring Boot动态数据源(多数据源自动切换)【从零开始学Spring Boot】
在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库.那么这一节我们将要介绍怎么进行多数据源动态切换.添 ...
- (42)Spring Boot多数据源【从零开始学Spring Boot】
我们在开发过程中可能需要用到多个数据源,我们有一个项目(MySQL)就是和别的项目(SQL Server)混合使用了.其中SQL Server是别的公司开发的,有些基本数据需要从他们平台进行调取,那么 ...
- 关于Spring Boot 多数据源的事务管理
自己的一些理解:自从用了Spring Boot 以来,这近乎零配置和"约定大于配置"的设计范式用着确实爽,其实对零配置的理解是:应该说可以是零配置可以跑一个简单的项目,因为Spri ...
- Springboot spring data jpa 多数据源的配置01
Springboot spring data jpa 多数据源的配置 (说明:这只是引入了多个数据源,他们各自管理各自的事务,并没有实现统一的事务控制) 例: user数据库 global 数据库 ...
- HBase 学习之路(十一)—— Spring/Spring Boot + Mybatis + Phoenix 整合
一.前言 使用Spring+Mybatis操作Phoenix和操作其他的关系型数据库(如Mysql,Oracle)在配置上是基本相同的,下面会分别给出Spring/Spring Boot 整合步骤,完 ...
- HBase 系列(十一)—— Spring/Spring Boot + Mybatis + Phoenix 整合
一.前言 使用 Spring+Mybatis 操作 Phoenix 和操作其他的关系型数据库(如 Mysql,Oracle)在配置上是基本相同的,下面会分别给出 Spring/Spring Boot ...
随机推荐
- Javascript中Closure及其相关概念
我相信学过Javascript这门语言的程序员应该都对Closure这个概念有所了解,然而网上以及各种Javascript书籍里面对Closure这个概念的定义有各种说法.我本人觉得很多地方对Clos ...
- php中的释放语句unset和释放函数mysql_free_result()
首先要强调的一点是unset在php中已经不再是一个函数了,既然不是函数,那么就没有了返回值,所以用的时候不能够用unset的返回值来做判断. 其次,在函数中,unset只能销毁局部变量,并不能销毁全 ...
- HFDS核心技术
HDFS 设计的前提与目标 HDFS体系结构1 HDFS体系结构2 HDFS特性与优点 高容错性保障机制 HDFS不适合的场景 HDFS2.0的新特征 HA-QJM Federation 快照 异构层 ...
- 关于oracle数据库压力测试
今天接到需求,需要对oracle数据库进行压力测试,就这几个字的需求. 然后查看了以下软件: 1.Benchmark Factory是一款专业的服务器性能测试工具,专为数据库测试和可扩展性测量而设计, ...
- 关于生成器---(yield)
生成器:是自定义的迭代器(自己用python代码写的迭代器),函数中见到yield的就是生成器 那么yield前后的变量又该怎么理解 看例子一 def counter(name): print('%s ...
- django-重写User模型
User模型有很多功能,验证什么的,重写需要满足下面的功能(基本上写注释的地方都是需要的) 开始: 创建一个重写user的app, 记得注册app startapp newauth from djan ...
- Flutter main future mirotask 的执行顺序
下面这段代码的输出是什么? import 'dart:async'; main() { print('main #1 of 2'); scheduleMicrotask(() => print( ...
- 使MySQL查询区分大小写的实现方法
发布:mdxy-dxy 字体:[增加 减小] 类型:转载 我们在MySQL中使用SELECT语句查询时,可不可以使查询区分大小写?今天从网络上找到了方法,现总结如下. 1.一种方法是可以设置表或行 ...
- Xshell图形界面启动
https://blog.csdn.net/qq_27843481/article/details/50539797 增加内存:https://jingyan.baidu.com/article/4d ...
- 开发者必看|Android 8.0 新特性及开发指南
背景介绍 谷歌2017 I/O开发者大会今年将于5月17-19日在美国加州举办.大会将跟往年一样发布最新的 Android 系统,今年为 Android 8.0.谷歌在今年3 月21日发布 Andro ...