1. 程式人生 > >Spring Boot多資料來源

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介面上。