Spring Boot多資料來源
轉:http://412887952-qq-com.iteye.com/blog/2302997
我們在開發過程中可能需要用到多個數據源,我們有一個專案(MySQL)就是和別的專案(SQL Server)混合使用了。其中SQL Server是別的公司開發的,有些基本資料需要從他們平臺進行調取,那麼在專案中就需要支援多資料來源,不然就只能獲取到自己的資料來源的資料了。當然還有很多其它方面的使用場景,多資料庫,比如有專門負責生成id的資料庫,或者主從庫分離等等。總之多資料來源可能在實際中還是需要用到的。
在Spring Boot中使用單資料來源的配置很簡單,我們簡單回憶下:只需要在application.properties進行基本的連線配置,在pom.xml引入基本的依賴即可。
那麼多資料來源的原理呢?其實很簡單,就是讀取配置檔案,根據配置檔案中的配置的資料來源數量,動態建立dataSource並註冊到Spring中。在上一節我們介紹了使用Java程式碼將物件註冊到Spring中,多資料來源就是基於這兒基礎進行動態建立的。本節大概需要這麼幾個步驟:
(1)新建maven java project;
(2)在pom.xml新增相關依賴;
(3)編寫app.java啟動類;
(4)編寫application.properties配置檔案;
(5)編寫多資料來源註冊檔案;
(6)編寫測試類
(7)測試
接下來讓我們按照這個步驟來進行編寫我們的程式碼吧。
(1)新建maven java project;
我們新建一個maven project進行測試,取名為:spring-boot-multids
(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.example</groupId> <artifactId>spring-boot-multids</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <!-- Inherit defaults from Spring Boot --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.0.RELEASE</version> </parent> <dependencies> <!-- spring boot web支援:mvc,aop... --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spirng data JPA依賴; --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 熱部署 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <!-- mysql驅動; --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> </project>
(3)編寫Application.java啟動類;
編寫spring boot的啟動類:
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
(4)編寫application.properties配置檔案;
在這裡主要是多資料來源的配置:
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
(5)編寫多資料來源註冊檔案;這個注入是最核心的部分,我們先看程式碼:
MultipleDataSourceBeanDefinitionRegistryPostProcessor:
package com.example.config.datasource.multids;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.sql.DataSource;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.core.env.Environment;
/**
*
* 動態建立多資料來源註冊到Spring中 介面:BeanDefinitionRegistryPostProcessor主要是注入bean
*
* 介面:介面 EnvironmentAware 重寫方法 setEnvironment
* 可以在工程啟動時,獲取到系統環境變數和application配置檔案中的變數。
*
* 方法的執行順序是: setEnvironment()-->postProcessBeanDefinitionRegistry() -->
* postProcessBeanFactory()
*
* @author Administrator
*
*/
@Configuration
public class MultipleDataSourceBeanDefinitionRegistryPostProcessor
implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
// 作用域物件.
private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
// bean名稱生成器.
private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
// 如配置檔案中未指定資料來源型別,使用該預設值
private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
// 存放DataSource配置的集合;
private Map<String, Map<String, Object>> dataSourceMap = new HashMap<String, Map<String, Object>>();
@Override
public void setEnvironment(Environment environment) {
System.out.println("MultipleDataSourceBeanDefinitionRegistryPostProcessor.setEnvironment()");
/*
* 獲取application.properties配置的多資料來源配置,新增到map中,
* 之後在postProcessBeanDefinitionRegistry進行註冊。
*/
// 獲取到字首是"custom.datasource." 的屬性列表值.
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment, "custom.datasource.");
// 獲取到所有資料來源的名稱.
String dsPrefixs = propertyResolver.getProperty("names");
String[] dsPrefixsArr = dsPrefixs.split(",");
for (String dsPrefix : dsPrefixsArr) {
/*
* 獲取到子屬性,對應一個map; 也就是這個map的key就是
*
* type、driver-class-name等;
*
*/
Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + ".");
// 存放到一個map集合中,之後在注入進行使用.
dataSourceMap.put(dsPrefix, dsMap);
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 設定為主資料來源;
beanFactory.getBeanDefinition("dataSource").setPrimary(true);
if (!dataSourceMap.isEmpty()) {
// 不為空的時候.
BeanDefinition bd = null;
Map<String, Object> dsMap = null;
MutablePropertyValues mpv = null;
for (Entry<String, Map<String, Object>> entry : dataSourceMap.entrySet()) {
bd = beanFactory.getBeanDefinition(entry.getKey());
mpv = bd.getPropertyValues();
dsMap = entry.getValue();
mpv.addPropertyValue("driverClassName", dsMap.get("driverClassName"));
mpv.addPropertyValue("url", dsMap.get("url"));
mpv.addPropertyValue("username", dsMap.get("username"));
mpv.addPropertyValue("password", dsMap.get("password"));
}
}
}
@SuppressWarnings("unchecked")
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
System.out.println("MultipleDataSourceBeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry()");
try {
if (!dataSourceMap.isEmpty()) {
// 不為空的時候,進行註冊bean.
for (Entry<String, Map<String, Object>> entry : dataSourceMap.entrySet()) {
Object type = entry.getValue().get("type");// 獲取資料來源型別,沒有設定為預設的資料來源.
if (type == null) {
type = DATASOURCE_TYPE_DEFAULT;
}
registerBean(registry, entry.getKey(),
(Class<? extends DataSource>) Class.forName(type.toString()));
}
}
} catch (ClassNotFoundException e) {
// 異常捕捉.
e.printStackTrace();
}
}
/**
* 註冊Bean到Spring
*
* @param registry
* @param name
* @param beanClass
*/
private void registerBean(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
// 單例還是原型等等...作用域物件.
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
// 可以自動生成name
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, registry));
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
}
以上程式碼的執行順序是:
setEnvironment()-->postProcessBeanDefinitionRegistry() --> postProcessBeanFactory()
在setEnvironment()方法中主要是讀取了application.properties的配置;
在postProcessBeanDefinitionRegistry()方法中主要註冊為spring的bean物件;
在postProcessBeanFactory()方法中主要是注入從setEnvironment方法中讀取的application.properties配置資訊。
需要注意的是這裡並沒有讀取其它相同的資料來源公共配置,這裡我們不做介紹,在下節介紹,主要是因為這節在實際中我們並不會這麼使用,這裡只是過渡下,方便下節進行講解。
(6)編寫測試類
我們編寫一個簡單的類進行測試下,到底我們的多資料來源是否注入成功了。
TestController:
package com.example.controller;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
// 沒有指定為主資料來源.
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("ds1")
private DataSource dataSource1;
@Autowired
@Qualifier("ds2")
private DataSource dataSource2;
private JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
System.out.println("TestController.setJdbcTemplate()");
jdbcTemplate.setDataSource(dataSource1);// 設定dataSource
this.jdbcTemplate = jdbcTemplate;
}
@RequestMapping("/get")
public String get() {
// 觀察控制檯的列印資訊.
System.out.println(dataSource);
return "ok";
}
@RequestMapping("/get1")
public String get1() {
// 觀察控制檯的列印資訊.
System.out.println(dataSource1);
return "ok.1";
}
@RequestMapping("/get2")
public String get2() {
// 觀察控制檯的列印資訊.
System.out.println(dataSource2);
return "ok.2";
}
@RequestMapping("/get3")
public String get3() {
// 觀察控制檯的列印資訊.
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource1);
System.out.println(jdbcTemplate.getDataSource());
System.out.println(jdbcTemplate);
/*
* Demo1只在test1中存在,test並沒有此資料庫; 需要自己自己進行復制,不然會報錯:Table 'test1.demo1'
* doesn't exist
*/
String sql = "select * from user_info";
jdbcTemplate.query(sql, new RowMapper<String>() {
@Override
public String mapRow(ResultSet rs, int rowNum) throws SQLException {
System.out.println(rs.getLong("uid") + "---" + rs.getString("name"));
return "";
}
});
return "ok.3";
}
@RequestMapping("/get4")
public String get4() {
// 觀察控制檯的列印資訊.
System.out.println(jdbcTemplate.getDataSource());
System.out.println(jdbcTemplate);
/*
* Demo1只在test1中存在,test並沒有此資料庫; 需要自己自己進行復制,不然會報錯:Table 'test1.demo1'
* doesn't exist
*/
String sql = "select *from user_info";
jdbcTemplate.query(sql, new RowMapper<String>() {
@Override
public String mapRow(ResultSet rs, int rowNum) throws SQLException {
System.out.println(rs.getLong("uid") + "---" + rs.getString("name"));
return "";
}
});
return "ok.4";
}
}
以上程式碼在實際開發中,是絕對不能這麼編寫的,這裡只是為了方便進行測試。(7)測試
Run As 執行app.java進行測試:
訪問:
http://127.0.0.1:8080/get
http://127.0.0.1:8080/get1
http://127.0.0.1:8080/get2
http://127.0.0.1:8080/get3
http://127.0.0.1:8080/get4
檢視控制檯的列印資訊。
然而我們在專案中不一定需要直接使用dataSource的,大家都習慣使用JDBC的jdbcTemplate、Mybatis的sqlSessionTemplate,再或者就是以Mybatis為例直接動態代理到Mapper介面上。