Springboot多資料來源配置--資料來源動態切換
在上一篇我們介紹了多資料來源,但是我們會發現在實際中我們很少直接獲取資料來源物件進行操作,我們常用的是jdbcTemplate或者是jpa進行操作資料庫。那麼這一節我們將要介紹怎麼進行多資料來源動態切換。新增本文實現的程式碼之後,只需要配置要資料來源就可以直接通過註解使用,在實際使用的時候特別的簡單。那麼本章主要分以下幾個步驟進行實戰。
本章大綱 寫道 (1)新建maven java project;(2)在pom.xml新增依賴包;
(3)編寫啟動類App.java
(4)編寫配置檔案application.properties;
(5)動態資料來源路由類;
(6)註冊多資料來源;
(7)測試類測試;
接下來我們看看每一步具體的實現吧:
(1)新建maven java project;
新建一個maven project,取名為:spring-boot-multi-ds
(2)在pom.xml新增依賴包;
在pom.xml檔案中加入依賴的庫檔案,主要是spring boot基本的,資料庫驅動,spring-jpa支援即可,具體pom.xml檔案如下:
<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"
<modelVersion>4.0.0</modelVersion>
<groupId>com.kfit</groupId>
<artifactId>spring-boot-multids</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-multids</name>
<url>http://maven.apache.org</
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- jdk版本號,這裡需要你本地進行的jdk進行修改,這裡angel使用的是1.8的版本. -->
<java.version>1.8</java.version>
</properties>
<!--
spring boot 父節點依賴,
引入這個之後相關的引入就不需要新增version配置,
spring boot會自動選擇最合適的版本進行新增。
在這裡使用的1.3.3版本,可能目前官方有最新的版本了,大家可以
使用最新的版本。
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>
<dependencies>
<!-- 單元測試包,在這裡沒有使用到. -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- spring boot web支援:mvc,aop...
這個是最基本的,基本每一個基本的demo都是需要引入的。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驅動.
我們的demo是多資料來源,在這裡使用Mysql資料庫.
-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- spring jpa
spring jpa中帶有自帶的tomcat資料連線池;
在程式碼中我們也需要用到.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>
在上面的配置檔案中都有相應的解釋,大家可以自己解讀下。
(3)編寫啟動類App.java
編寫spring boot的啟動類:
package com.kfit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* @author Angel(QQ:412887952)
* @version v.0.1
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
(4)編寫配置檔案application.properties;
在這裡主要是多資料來源和jpa的配置:
src/main/resources/application.properties:
########################################################
###配置檔案包括1個主資料來源和多個數據源,
###其中主資料來源在Spring中的beanName預設為dataSource,
###另外幾個資料來源的beanName分包為:ds1、ds2、ds3
###其中datasource的type屬性可以具體指定到我們需要的資料來源上面,
###不指定情況下預設為:org.apache.tomcat.jdbc.pool.DataSource
###當然你也可以把這些資料來源配置到主dataSource資料庫中,然後讀取資料庫生成多資料來源。當然這樣做的必要性並不大,難不成資料來源還會經常變嗎。
########################################################
# 主資料來源,預設的
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
# 更多資料來源
custom.datasource.names=ds1,ds2,ds3
#custom.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
custom.datasource.ds1.driverClassName =com.mysql.jdbc.Driver
custom.datasource.ds1.url=jdbc:mysql://localhost:3306/test1
custom.datasource.ds1.username=root
custom.datasource.ds1.password=root
#custom.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
custom.datasource.ds2.driverClassName =com.mysql.jdbc.Driver
custom.datasource.ds2.url=jdbc:mysql://localhost:3306/test
custom.datasource.ds2.username=root
custom.datasource.ds2.password=root
#custom.datasource.ds3.type=com.zaxxer.hikari.HikariDataSource
custom.datasource.ds3.driverClassName =com.mysql.jdbc.Driver
custom.datasource.ds3.url=jdbc:mysql://localhost:3306/test
custom.datasource.ds3.username=root
custom.datasource.ds3.password=root
# 下面為連線池的補充設定,應用到上面所有資料來源中
spring.datasource.maximum-pool-size=100
spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5
spring.datasource.validation-query=SELECT 1
spring.datasource.test-on-borrow=false
spring.datasource.test-while-idle=true
spring.datasource.time-between-eviction-runs-millis=18800
########################################################
### Java Persistence Api
########################################################
# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for each sql query
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
#[org.hibernate.cfg.ImprovedNamingStrategy #org.hibernate.cfg.DefaultNamingStrategy]
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.DefaultNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
(5)動態資料來源路由類;
動態資料來源能進行自動切換的核心就是spring底層提供了AbstractRoutingDataSource類進行資料來源的路由的,我們主要繼承這個類,實現裡面的方法即可實現我們想要的,這裡主要是實現方法:determineCurrentLookupKey(),而此方法只需要返回一個數據庫的名稱即可,所以我們核心的是有一個類來管理資料來源的執行緒池,這個類才是動態資料來源的核心處理類。還有另外就是我們使用aop技術在執行事務方法前進行資料來源的切換。所以這裡有幾個需要編碼的類,具體如下:
動態資料來源上下文>com.kfit.config.datasource.DynamicDataSourceContextHolder:
package com.kfit.config.datasource.dynamic;
import java.util.ArrayList;
import java.util.List;
/**
* 動態資料來源上下文.
*
* @author Angel(QQ:412887952)
* @version v.0.1
*/
public class DynamicDataSourceContextHolder {
/*
* 當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,
* 所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/*
* 管理所有的資料來源id;
* 主要是為了判斷資料來源是否存在;
*/
public static List<String> dataSourceIds = new ArrayList<String>();
/**
* 使用setDataSourceType設定當前的
* @param dataSourceType
*/
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
/**
* 判斷指定DataSrouce當前是否存在
*
* @param dataSourceId
* @return
* @author SHANHY
* @create 2016年1月24日
*/
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
資料來源路由類>com.kfit.config.datasource.dynamic.DynamicDataSource:
package com.kfit.config.datasource.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 動態資料來源.
* @author Angel(QQ:412887952)
* @version v.0.1
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/*
* 程式碼中的determineCurrentLookupKey方法取得一個字串,
* 該字串將與配置檔案中的相應字串進行匹配以定位資料來源,配置檔案,即applicationContext.xml檔案中需要要如下程式碼:(non-Javadoc)
* @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
*/
@Override
protected Object determineCurrentLookupKey() {
/*
* DynamicDataSourceContextHolder程式碼中使用setDataSourceType
* 設定當前的資料來源,在路由類中使用getDataSourceType進行獲取,
* 交給AbstractRoutingDataSource進行注入使用。
*/
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
指定資料來源註解類>com.kfit.config.datasource.dynamic.TargetDataSource:
package com.kfit.config.datasource.dynamic;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 在方法上使用,用於指定使用哪個資料來源
* @author Angel(QQ:412887952)
* @version v.0.1
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value();
}
切換資料來源Advice>com.kfit.config.datasource.dynamic.DynamicDataSourceAspect:
package com.kfit.config.datasource.dynamic;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 切換資料來源Advice
* @author Angel(QQ:412887952)
* @version v.0.1
*/
@Aspect
@Order(-10)//保證該AOP在@Transactional之前執行
@Component
public class DynamicDataSourceAspect {
/*
* @Before("@annotation(ds)")
* 的意思是:
*
* @Before:在方法執行之前進行執行:
* @annotation(targetDataSource):
* 會攔截註解targetDataSource的方法,否則不攔截;
*/
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint point, TargetDataSourcetargetDataSource) throws Throwable {
//獲取當前的指定的資料來源;
String dsId = targetDataSource.value();
//如果不在我們注入的所有的資料來源範圍之內,那麼輸出警告資訊,系統自動使用預設的資料來源。
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
System.err.println("資料來源[{}]不存在,使用預設資料來源 > {}"+targetDataSource.value()+point.getSignature());
} else {
System.out.println("Use DataSource : {} > {}"+targetDataSource.value()+point.getSignature());
//找到的話,那麼設定到動態資料來源上下文中。
DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.value());
}
}
@After("@annotation(targetDataSource)")
public void restoreDataSource(JoinPoint point, TargetDataSourcetargetDataSource) {
System.out.println("Revert DataSource : {} > {}"+targetDataSource.value()+point.getSignature());
//方法執行完畢之後,銷燬當前資料來源資訊,進行垃圾回收。
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
(6)註冊多資料來源;
以上都是動態資料來源在注入的時候使用的程式碼,其實很重要的一部分程式碼就是註冊我們在application.properties配置的多資料來源,這才是重點,這裡我們使用
ImportBeanDefinitionRegistrar進行註冊,具體的程式碼如下:
package com.kfit.config.datasource.dynamic;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
/**
* 動態資料來源註冊;
* @author Angel(QQ:412887952)
* @version v.0.1
*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
//如配置檔案中未指定資料來源型別,使用該預設值
private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
private ConversionService conversionService = new DefaultConversionService();
private PropertyValues dataSourcePropertyValues;
// 預設資料來源
private DataSource defaultDataSource;
private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();
/**
* 載入多資料來源配置
*/
@Override
public void setEnvironment(Environment environment) {
System.out.println("DynamicDataSourceRegister.setEnvironment()");
initDefaultDataSource(environment);
initCustomDataSources(environment);
}
/**
* 載入主資料來源配置.
* @param env
*/
private void initDefaultDataSource(Environment env){
// 讀取主資料來源