最近在做一个项目,需要使用SpringBoot+Mybatis+Druid使用多数据源,前提条件是数据源的个数和名称不确定,是在application.yml文件中设定,在使用时根据条件动态切换。
这样就不能像Druid官网提到的,通过ConfigurationProperties注解创建多个DruidDataSource,因为这样属于硬编码,添加一个数据源就要再添加代码,我考虑的是只使用一套构建DataSource的代码,添加或删除数据源只需要修改配置文件。
Spring提供的AbstractRoutingDataSource提供了运行时动态切换DataSource的功能,但是AbstractRoutingDataSource对象中包含的DataSourceBuilder构建的仅仅是Spring
JDBC的DataSource,并不是我们使用的DruidDataSource,需要自行构建。
很重要:使用之前请在springboot的main类上加@SpringBootApplication(exclude =
{DataSourceAutoConfiguration.class })注解,关闭springboot的数据源自动注入,示例如下
/** * ClassName: ArmApplication * <p> * Description: ARM Application's boot
starter * </p> * date:2017年9月20日 * <p> * * @author limsh * @version 0.1 *
@sinceJDK 1.6 */ @SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class }) @ComponentScan(basePackages={
"com.**.**.**.**"}) @EnableTransactionManagement public class ArmApplication
extendsSpringBootServletInitializer implements DisposableBean{ private static
finalClass<ArmApplication> startApplication = ArmApplication.class; private
final staticLogger log = LoggerFactory.getLogger(ArmApplication.class); public
static voidmain(String[] args) { SpringApplication.run(startApplication, args);
log.info("Started ARM Application Succeed"); } @Override protected
SpringApplicationBuilderconfigure(SpringApplicationBuilder builder) { return
builder.sources(startApplication); } @Override public void destroy() throws
Exception {// 应用结束时执行 DistributedIdFactory.stop(); } }
我们构建一个名为SpringBootDruidMultiDB的SpringBoot项目,导入mybatis-spring-boot-starter和spring-boot-starter-web以及spring-boot-starter-test,为了使用Druid方便,项目还导入druid-spring-boot-starter。由于使用Log4j2记录日志,还添加了log4j2所需要的库,pom文件的配置如下
<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>
druid-spring-boot-starter</artifactId> <version>1.1.6</version> </dependency> <
dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>
mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>
spring-boot-starter-aop</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>org.apache.logging.log4j</groupId> <
artifactId>log4j-api</artifactId> <version>2.10.0</version> </dependency> <
dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</
artifactId> <version>2.10.0</version> </dependency> <dependency> <groupId>
com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.3.7</version>
</dependency> <dependency> <groupId>mysql</groupId> <artifactId>
mysql-connector-java</artifactId> <version>5.1.45</version> </dependency> <
dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <
version>1.2.43</version> </dependency> <dependency> <groupId>org.apache.commons
</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </
dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>
commons-collections4</artifactId> <version>4.1</version> </dependency> <
dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</
artifactId> <version>1.2</version> </dependency> </dependencies>
一、修改src/main/resources文件,添加多个数据源信息
#数据库主库信息 master: driverClassName: com.mysql.jdbc.Driver url:
jdbc:mysql://****:3306/**?useUnicode\=true&characterEncoding\=utf-8username: ***
password: *** initialSize: 5 minIdle: 5 maxActive: 20 # 配置获取连接等待超时的时间 maxWait:
60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 #
配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 validationQuery:
SELECT 1 FROM DUALtestWhileIdle: true testOnBorrow: true testOnReturn: false
poolPreparedStatements:true maxPoolPreparedStatementPerConnectionSize: 20
#设置连接池以及数据库从库库信息slave: driverClassName: com.mysql.jdbc.Driver url:
jdbc:mysql://***:3306/***?useUnicode\=true&characterEncoding\=utf-8username:
**** password: **** initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 #
配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 #
配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 validationQuery:
SELECT 1 FROM DUALtestWhileIdle: true testOnBorrow: true testOnReturn: false
poolPreparedStatements:true maxPoolPreparedStatementPerConnectionSize: 20
二、开始我参照单数据源的构建方式,想像下面的方式构建DruidDataSource数据源
* @Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
* @Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有两个DataSource类型的实例,需要指定名称注入)
*
@Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
/** * 数据库配置 * @author zhangk * @time 2018-05-23 */ @Configuration public
classDataBaseConfiguration{ @Value("${spring.datasource.type}") private Class<?
extendsDataSource> dataSourceType; @Bean(name="masterDataSource") @Primary
@ConfigurationProperties(prefix = "spring.datasource.master") public DataSource
masterDataSource() { return DataSourceBuilder.create().build(); } /** * 从库 *
@return */ @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix =
"spring.datasource.slave") public DataSource slaveDataSourceOne(){ return
DataSourceBuilder.create().build(); } @Bean public JdbcOperations
prodJdbcOperations(@Qualifier("masterDataSource") DataSource prodDataSource) {
return newJdbcTemplate(prodDataSource); } @Bean public JdbcOperations
devJdbcOperations(@Qualifier("slaveDataSource")DataSource devDataSource) {
return newJdbcTemplate(devDataSource); } }
这里prefix是类似于spring.datasource.master
的前缀,然而执行后发现生成的DruidDataSource对象的driverClassName,url,username,password等这些基本属性值。
注:只有使用@ConnectionProperties注解构建的DruidDataSource才可以正常赋值(类似下面的代码)。
三、Mybaties数据源配置,这里只支持一主一从的情况,如需一主多从,可自行稍加改造以下roundRobinDataSouceProxy()方法和后面的ReadWriteSplitRoutingDataSource类
/** * mybatis多数据源配置类:支持一主一从 * @author zhangk * @time 2018-05-23 */
@Configuration@ConditionalOnClass({EnableTransactionManagement.class}) @Import
({ DataBaseConfiguration.class}) @MapperScan(basePackages={"com.**.**.**.**.dao"
})public class MybatisConfiguration { private final JpaProperties jpaProperties;
@Autowired public MybatisConfiguration(JpaProperties jpaProperties) { this.
jpaProperties= jpaProperties; } @Value("${spring.datasource.type}") private
Class<?extends DataSource> dataSourceType; @Resource(name = "masterDataSource")
privateDataSource masterDataSource; @Resource(name = "slaveDataSource") private
DataSourceslaveDataSource; /** * 从库的数量 */ @Value(
"${spring.datasource.readSize}") private String dataSourceSize; @Bean
@ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory() throws
Exception { SqlSessionFactoryBean sqlSessionFactoryBean =new
SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
sqlSessionFactoryBean.setTypeAliasesPackage("com.yamei.account.rela.mgt.model");
sqlSessionFactoryBean.setMapperLocations(new
PathMatchingResourcePatternResolver() .getResources("classpath:mapping*//*.xml"
));
sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(
true); return sqlSessionFactoryBean.getObject(); } /** * 有多少个数据源就要配置多少个bean *
@return */ @Bean public AbstractRoutingDataSource roundRobinDataSouceProxy() {
ReadWriteSplitRoutingDataSource proxy =new ReadWriteSplitRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
targetDataSources.put(DataSourceContextHolder.MASTER,masterDataSource);
targetDataSources.put(DataSourceContextHolder.SLAVE,slaveDataSource);
proxy.setDefaultTargetDataSource(masterDataSource);
proxy.setTargetDataSources(targetDataSources); return proxy; } @Autowired
@Bean(name = "entityManagerFactoryBean") public
LocalContainerEntityManagerFactoryBeanentityManagerFactoryBean(
EntityManagerFactoryBuilder builder, AbstractRoutingDataSource
dynamicDataSource) {return builder.dataSource(dynamicDataSource) .properties(
jpaProperties.getProperties()) .packages("com.yamei.account.rela.mgt.model.*")
.persistenceUnit("masterPersistenceUnit") .build(); } @Autowired @Bean
@Primary public EntityManagerFactory entityManagerFactory
(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {return
entityManagerFactoryBean.getObject(); } @Autowired @Bean @Primary public
PlatformTransactionManagertransactionManager(EntityManagerFactory
entityManagerFactory) {return new
XATransactionManagerConfig(entityManagerFactory); } }
四、将数据源与当前线程绑定
/** * 本地线程,数据源上下文 * @author zhangk * */ public class
DataSourceContextHolder {public static final String MASTER_TRANSACTION =
"master_transaction"; public static final String MASTER_ANNOTATION =
"master_annotation"; public static final String MASTER = "master"; public
static finalString SLAVE = "slave"; private static final Log log = LogFactory.
getLog(DataSourceContextHolder.class); /** * 使用ThreadLocal把数据源与当前线程绑定 */
private staticThreadLocal<String> currentDataSource = new ThreadLocal<>();
public staticString getDataSource() { return currentDataSource.get(); } public
static voidsetDataSource(String dataSourceName) { log.debug("Set dataSource: ["
+ dataSourceName +"]"); String current = currentDataSource.get(); if (!(
MASTER_TRANSACTION.equals(current) || MASTER_ANNOTATION.equals(current))) {
currentDataSource.set(dataSourceName); } log.debug("===================="
+dataSourceName+"======================"); log.debug("Current dataSource is: ["
+currentDataSource.get() + "]"); } public static void clearDataSource() { log
.debug("Close dataSource: [" + currentDataSource.get() + "]"); log.debug(
"====================清除======================"); currentDataSource.remove(); } }
五、实现多数据源切换
/** * 多数据源切换 * @author zhangk * @time 2018-05-23 */ public class
ReadWriteSplitRoutingDataSourceextends AbstractRoutingDataSource { private
static finalLog log = LogFactory.getLog(ReadWriteSplitRoutingDataSource.class);
@Override protected Object determineCurrentLookupKey() { log.debug(
"-----------------获取连接"+DataSourceContextHolder.getDataSource()+
"----------------------"); return DataSourceContextHolder.getDataSource(); } }
六、多数据源切面控制
package com.yamei.account.rela.mgt.datasource; import
org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory;
importorg.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.
Aspect; import org.aspectj.lang.annotation.Before; import
org.aspectj.lang.annotation.Pointcut; import
org.springframework.core.annotation.Order; import
org.springframework.stereotype.Component; /** * 多数据源切面控制 * @author zhangk *
@time2018-05-23 */ @Aspect @Component public class DataSourceAop { private
finalLog log = LogFactory.getLog(DataSourceAop.class); @Pointcut("execution(*
com.**.account.rela.mgt.dao..*.select*(..)) ||execution(*
com.**.account.rela.mgt.dao..*.query*(..)) || execution(*
com.**.account.rela.mgt.dao..*.get*(..))") public void findMethod() { } @Before(
"findMethod()") public void beforeFindMethod() { DataSourceContextHolder.
setDataSource(DataSourceContextHolder.SLAVE); log.debug("Find method: [begin],
Current dataSource is: [" + DataSourceContextHolder.getDataSource() + "]"); }
@After("findMethod()") public void afterFindMethod() { String dataSource =
DataSourceContextHolder.getDataSource(); log.debug("Find method: [end], Current
dataSource is: ["+ dataSource + "]"); if (DataSourceContextHolder.SLAVE
.equals(dataSource)) { DataSourceContextHolder.clearDataSource(); } } @Pointcut(
"execution(* com.**.account.rela.mgt.dao..*.add*(..)) || execution(*
com.**.account.rela.mgt.dao..*.insert*(..)) || execution(*
com.**.account.rela.mgt.dao..*.edit*(..))") public void editMethod() { } @Before
("editMethod()") public void beforeEditMethod() { DataSourceContextHolder.
setDataSource(DataSourceContextHolder.MASTER); log.debug("Find method: [begin],
Current dataSource is: [" + DataSourceContextHolder.getDataSource() + "]"); }
@After("findMethod()") public void afterEditMethod() { String dataSource =
DataSourceContextHolder.getDataSource(); log.debug("Find method: [end], Current
dataSource is: ["+ dataSource + "]"); if (DataSourceContextHolder.MASTER
.equals(dataSource)) { DataSourceContextHolder.clearDataSource(); } }
//自定义注解@Pointcut("@annotation(Master)") public void masterAnnoMethod() { }
@Before("masterAnnoMethod()") public void beforeMaster() {
DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER_ANNOTATION)
; log.debug("Master annotated method: [begin], Current dataSource is: [" +
DataSourceContextHolder.getDataSource() + "]"); } @After("masterAnnoMethod()")
public voidafterMaster() { String dataSource = DataSourceContextHolder.
getDataSource(); log.debug("Master annotated method: [end], Current dataSource
is: ["+ dataSource + "]"); if (DataSourceContextHolder.MASTER_ANNOTATION
.equals(dataSource)) { DataSourceContextHolder.clearDataSource(); } } } package
com.yamei.account.rela.mgt.datasource; /** * @author zhangk * @date 2018-05-29
*/public @interface Master { }
七、解决一个问题,多数据源情况下事物,为了保证的事物的原子性,一致性。执行含有事物的方法的时候,会默认拿该方法中第一次取得的数据库连接去执行整个方法的所有数据库操作,如果第一次拿到的连接是主库的话这样不会有什么影响,但是如果第一次拿到是的是从库的话执行写操作的时候就会报错,所以这里要重写事物,在执行事物的时候强制使用主库,即重写JpaTransactionManager的doBegin和doCommit方法
/** * @Description * @Created by zhangk * @time 2018/5/29 0029 * @Modified
By: */ public class XATransactionManagerConfig extends JpaTransactionManager {
publicXATransactionManagerConfig() { } public XATransactionManagerConfig
(EntityManagerFactory emf) {super(emf); } @Override protected void doBegin
(Object transaction, TransactionDefinition definition) {
DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER_TRANSACTION
); logger.debug("JPA-Transaction: [begin], Current dataSource is: [" +
DataSourceContextHolder.getDataSource() + "]"); super.doBegin(transaction,
definition); } @Override protected void doCommit(DefaultTransactionStatus
status) { String dataSource = DataSourceContextHolder.getDataSource(); logger
.debug("JPA-Transaction: [commit], Current dataSource is: [" + dataSource + "]")
; super.doCommit(status); if (DataSourceContextHolder.MASTER_TRANSACTION
.equals(dataSource)) { DataSourceContextHolder.clearDataSource(); } } }
八、使用 直接在方法上加事物就行 还有一个自定义Master注解使用方法这里就不贴代码出来了直接@Master放到方法上面就好了
@Transactional(propagation=Propagation.REQUIRED,rollbackFor=
{ArmServiceException.class}) @Override public int addEnterpriseStaff
(ArmEnterpriseStaff staff, Long enterpriseId) throws ArmServiceException {
另外,本项目仅仅是SpringBoot + Mybatis +
Druid多数据源集成的一个简单Demo,可能有些地方理解不准确,有些做法不正确,如果读者有意见,也可以指出,作者感激不尽。
热门工具 换一换