本文將介紹基於Spring Boot 2.2.2.RELEASE實現H2資料庫和MySQL資料庫兩個數據源的自由切換。在本文中,數據源實現使用阿里巴巴提供的Druid數據源。
1. 需求背景
在一些Web後台應用中,通常存在這樣一種應用場景:當管理員第一次訪問系統時,會自動跳轉到系統初始化頁面,要求管理員填寫資料庫主機地址,資料庫名稱,資料庫管理員帳戶,資料庫管理員密碼以及系統的管理員帳戶和密碼,然後點擊安裝按鈕開始初始化後台數據,當後台數據初始化完成,頁面將跳轉到系統後台的登錄頁面。那麼,如何使用Spring Boot完成這樣一個功能呢?
在Spring Boot應用程式中,如果在類路徑下存在某個資料庫依賴(例如MySQL),則必須提供相應的數據源信息,否則應用程式將無法啟動。如果想要在不配置數據源的情況下啟動應用程式,可以參照下面的做法修改主類配置。
調整前:
@SpringBootApplication
public class MyApplication{
public static void main(String[] args){
SpringApplication.run(MyApplication.class,args);
}
}
調整後:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class MyApplication{
public static void main(String[] args){
SpringApplication.run(MyApplication.class,args);
}
}
exclude={DataSourceAutoConfiguration.class}的作用是告訴Spring Boot在啟動應用程式時,不自動配置數據源。
現在,我們可以正常啟動應用程式,但隨之帶來一個問題——系統將無可用的數據源。解決此問題的辦法有很多,在禁用自動配置數據源後,通常手動提供一個數據源配置類,自定義一些數據源配置項,但在配置數據源時,資料庫連接信息,用戶名和密碼等需要指定,如果是按照需求背景所描述,此時這些信息未知,該如何解決這個問題?
接下來,將介紹使用多數據源(或動態數據源)切換的技術解決這一問題。
2. 環境和工具
在本次案例中,將使用Spring Boot.2.2.RELEASE版本創建所需的工程,所需要的環境參數和工具如下:
名稱版本JDK1.8+SpringBoot2.2.2.RELEASEMaven3.2+IntelliJ IDEA2019.2Druid1.1.14MySQL5.1.47
3. 創建工程
使用IDEA創建一個Spring Boot工程,並修改pom.xml配置文件,pom.xml文件清單如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--Other configution -->
...
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.47</mysql.version>
<log4j.version>1.2.17</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.14</version>
</dependency>
</dependencies>
</project>
註:由於篇幅原因,省略了啟動的一些配置項
3.1 配置文件
我們需要將系統配置文件拆分為兩個application.yml和application-db.yml(也可以在一個配置文件中配置,分開配置為了結構清晰),此外在創建一個mysql.properties配置文件。下面簡單的對這三個配置文件做一個介紹。
application.yml是應用程式的主配置文件(默認),主要放置應用程式的通用配置,例如:應用程式埠號,上下文路徑,模板引擎,靜態資源路徑等等。application.yml配置清單如下:
server:
servlet:
context-path: /
port: 80
max-http-header-size: 10000
spring:
freemarker:
enabled: true
cache: false
charset: UTF-8
settings:
classic_compatible: true
template_exception_handler: rethrow
template_update_delay: 0
datetime_format: yyyy-MM-dd HH:mm
number_format: 0.##
template-loader-path:
- classpath:/templates/
suffix: .html
resources:
static-locations:
- classpath:/static/
application:
name: una
jpa:
generate-ddl: false
show-sql: true
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5Dialect
datasource:
druid:
initialSize: 5
#最小連接池數量
minIdle: 10
#最大連接池數量
maxActive: 20
#配置獲取連接等待超時的時間
maxWait: 60000
#配置檢測的間隔時間,檢測時需要關閉空閒的連接,單位為毫秒
timeBetweenEvictionRunsMillis: 60000
#配置連接池最小的生命周期,單位毫秒
minEvictableIdleTimeMillis: 300000
#配置連接池最大的生命周期,單位毫秒
maxEvictableIdleTimeMillis: 900000
#配置檢測連接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
#設置白名單,不填寫則允許所有訪問
allow:
url-pattern: /admin/druid/*
filter:
stat:
enabled: true
#慢SQL記錄
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
xss:
enabled: false
urlPatterns: /monitor/*
註:application.yml文件中,重點是druid的配置,後續將會使用@Value註解將這些配置項綁定到Java對象中。
application-db.yml配置文件用於配置和數據源相關的信息,在此配置文件中,配置了一個H2數據的連接信息和一個MySQL數據的連接信息,MySQL數據源只提供了數據源類型和驅動兩個配置項。application-db.yml配置清單如下:
spring:
datasource:
druid:
h2:
url: jdbc:h2:~/una_db
username: sa
password:
name: una_db
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.h2.Driver
mysql:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
注: h2數據源為默認的內存資料庫,當管理員第一次訪問應用或沒有初始化MySQL數據源信息時,使用該數據源連接資料庫。
mysql.properties文件用於存放管理員提交的MySQL資料庫連接信息,包括url(資料庫連接),username(管理員帳戶)和password(管理員密碼)。mysql.properties文件清單如下:
url=
username=
password=
3.2 修改應用主類
為了能夠手動配置數據源,需要禁用Spring Boot的自動配置數據源功能,在應用程式主類中,將@SpringBootApplication註解加上下面的配置:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
4. InstallUtils類
創建一個InstallUtils工具類並提供一個返回類型為布爾值isInstall()方法,該方法用於判單系統是否以及初始化。InstallUtils.java代碼清單如下:
public class InstallUtils {
public static Boolean isInstall() {
String installFile = InstallUtils.class.getResource("/").getPath()+"/install.back";
File file = new File(installFile);
if(file.exists()){
return true;
}else{
return false;
}
}
}
isInstall()方法比較簡單,它將查找類路徑下有無install.back文件。如果install.back文件存在,則返回true,否則返回false。在切換數據源的過程中,會根據此方法的返回值確定數據源的類型。
註:當系統初始化後,會向類路徑下寫入install.back文件
5. 配置文件屬性與Java對象綁定
在Spring Boot中,可以使用@ConfigurationProperties註解和@Value註解將.properties或.yml配置文件中的屬性綁定到Java對象,這極大的提高了編碼的靈活性。這裡通過一個表格,對@ConfigurationProperties和@Value註解的區別做一下說明:
@ConfigurationProperties@Value功能批量注入配置文件的屬性One by One鬆散語法支持不支持SPEL不支持支持JSR303數據校驗支持(例如郵箱驗證)不支持複雜類型封裝支持不支持
Druid配置屬性由於沒有涉及到複雜類型的封裝(另外是為了演示兩個註解的用法),所以使用@Value註解將application.yml中druid的配置屬性綁定到Java對象中。
新建一個DruidProperty.java文件,並使用@Configuration註解對其進行標記,然後使用@Value註解將配置屬性與DruidProperty類中的成員變量進行綁定。DruidProperty.java文件的代碼清單如下:
@Configuration
public class DruidProperty {
@Value( "${spring.datasource.druid.initialSize}" )
private int initialSize;
@Value ( "${spring.datasource.druid.minIdle}" )
private int minIdle;
@Value ( "${spring.datasource.druid.maxActive}" )
private int maxActive;
@Value("${spring.datasource.druid.maxWait}")
private int maxWait;
@Value ( "${spring.datasource.druid.timeBetweenEvictionRunsMillis}" )
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}")
private boolean testOnReturn;
public DruidDataSource druidDataSource(DruidDataSource dataSource){
dataSource.setInitialSize(initialSize);
dataSource.setMaxActive(maxActive);
dataSource.setMinIdle(minIdle);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
try {
dataSource.addFilters("stat,wall");
} catch (SQLException e) {
e.printStackTrace();
}
return dataSource;
}
}
註:在DruidProperty.java文件中提供了一個返回類型為DruidDataSource的druidDataSource()方法,此方法將使用類中的成員變量對傳入的數據源進行初始化,在配置數據源時會使用到。
6. DataSourceHolder
DataSourceHolder類用於記錄當前的數據源信息,其內部通過一個ThreadLocal常量來存儲數據源名稱,代碼如下:
public class DataSourceHolder {
private static final ThreadLocal<String> DATASOURCE = new ThreadLocal<>();
public static void setDatasource(String datasource){
DATASOURCE.set(datasource);
}
public static String getDatasource(){
if(InstallUtils.isInstall()){
return DataBaseType.MYSQL.name();
}else{
return DataBaseType.H2.name();
}
}
}
此外,創建一個Enum類DataBaseType用於設定數據源的類型名稱,DataBaseType.java代碼清單如下:
public enum DataBaseType {
H2,MYSQL
}
註:在DataSourceHolder的getDatasource方法中,根據InstallUtils.isInstall()方法的返回值確定當前數據源的類型。
7. 創建動態數據源
想要實現動態數據源,只需要自定義一個數據源類並繼承AbstractRoutingDataSource,然後覆蓋determineCurrentLookupKey()即可。在自定義數據源類中,還提供了一個構造函數,用於設置默認的數據源和目標數據源。自定義數據源DynamicDataSource類的代碼清單如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object,Object>targetDataSource){
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDatasource();
}
8.註冊自定義數據源
自定義數據源創建後,我們需要手動將數據源註冊到Spring Boot中才能生效。首先,創建一個DataSourceConfiguration類並用@Configuration註解進行標註,此操作是告訴SpringBoot,該類是一個配置類。接下來,在此類中配置自定義的數據源。
8.1 配置H2數據源
H2數據源是應用的默認數據源,在管理員第一次訪問後台或未初始化系統前,都將使用此數據源。H2數據源的配置代碼清單如下:
/**
* 默認的H2內存資料庫,在沒有安裝系統之前使用該資料庫
* @param druidProperty druid配置屬性
* @return DruidDataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.h2")
public DataSource h2DataSource(DruidProperty druidProperty){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperty.druidDataSource(dataSource);
}
在此配置中,@ConfigurationProperties(prefix=」spring.datasource.druid.h2」)的作用是將application-db.yml文件中前綴為「spring.datasource.druid.h2」的配置屬性綁定到DruidDataSource的成員變量上。
註:DruidProperty類已經使用@Value註解對其成員變量進行綁定,在此可以直接使用。
8.2 配置MySQL數據源
相比於H2數據源的配置,MySQL數據源的配置稍複雜一些。我們需要根據一定的條件來配置該數據源,例如,當管理員初始化後台數據後才配置此數據源。要實現這樣的一種設置,可以藉助SpringBoot的@ConditionalOnResource註解來實現。@ConditionalOnResource註解的原理是當存在某個資源文件時才註冊當前的Bean到SpringBoot中。此外,還需要將從前端獲取到的資料庫連接,用戶名和密碼手動設置到DruidDataSource上(前端提交的資料庫信息被存放到mysql.properties文件中)。MySQL數據源配置代碼清單如下:
/**
* 配置資料庫後使用該數據源
* @param druidProperty druid配置屬性
* @return DruidDataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
@ConditionalOnResource(resources = "classpath:install.back")
public DataSource mysqlDataSource(DruidProperty druidProperty){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
Properties properties = PropertiesUtils.getProperties("mysql.properties");
dataSource.setUrl(properties.getProperty("url"));
dataSource.setUsername(properties.getProperty("username"));
dataSource.setPassword(properties.getProperty("password"));
return druidProperty.druidDataSource(dataSource);
}
8.3 配置動態數據源
在動態數據源的配置中,我們需要使用@Primary註解指定該數據源是主數據源,H2數據源和MySQL數據源的切換將由此數據源完成。動態數據源的配置如下:
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource(DataSource h2DataSource,DataSource mysqlDataSource){
Map<Object,Object> targetDataSource = new HashMap<>(2);
targetDataSource.put(DataBaseType.H2.name(),h2DataSource);
targetDataSource.put(DataBaseType.MYSQL.name(),mysqlDataSource);
if(InstallUtils.isInstall()){
return new DynamicDataSource(mysqlDataSource,targetDataSource);
}else{
return new DynamicDataSource(h2DataSource,targetDataSource);
}
}
在此配置中,將根據InstallUtils.isInstall()方法確定哪一個數據源為默認的數據源。
8.4 完整的配置清單
下面是DataSourceConfiguration.java文件的所有代碼清單:
@Configuration
public class DataSourceConfiguration {
/**
* 默認的H2內存資料庫,在沒有安裝系統之前使用該資料庫
* @param druidProperty druid配置屬性
* @return DruidDataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.h2")
public DataSource h2DataSource(DruidProperty druidProperty){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperty.druidDataSource(dataSource);
}
/**
* 配置資料庫後使用該數據源
* @param druidProperty druid配置屬性
* @return DruidDataSource
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
@ConditionalOnResource(resources = "classpath:install.back")
public DataSource mysqlDataSource(DruidProperty druidProperty){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
Properties properties = PropertiesUtils.getProperties("mysql.properties");
dataSource.setUrl(properties.getProperty("url"));
dataSource.setUsername(properties.getProperty("username"));
dataSource.setPassword(properties.getProperty("password"));
return druidProperty.druidDataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource(DataSource h2DataSource,DataSource mysqlDataSource){
Map<Object,Object> targetDataSource = new HashMap<>(2);
targetDataSource.put(DataBaseType.H2.name(),h2DataSource);
targetDataSource.put(DataBaseType.MYSQL.name(),mysqlDataSource);
if(InstallUtils.isInstall()){
return new DynamicDataSource(mysqlDataSource,targetDataSource);
}else{
return new DynamicDataSource(h2DataSource,targetDataSource);
}
}
@Bean
public DruidStatInterceptor druidStatInterceptor(){
return new DruidStatInterceptor();
}
@Bean
@Scope("prototype")
public JdkRegexpMethodPointcut jdkRegexpMethodPointcut(){
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns("com.ramostear.blogdemo.*");
return pointcut;
}
@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor(DruidStatInterceptor druidStatInterceptor, JdkRegexpMethodPointcut pointcut){
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(druidStatInterceptor);
return advisor;
}
}
9. 工程結構
你可以通過下面的截圖了解整個項目的組成結構:
10. 測試應用
為了測試方便,我們先在本地計算機上創建一個install.back文件,然後啟動應用,訪問http://localhost/admin/druid/datasource.html ,進入Druid數據源監控面板,觀察數據源信息,然後將install.back文件拷貝到應用程式類路徑下(模仿系統初始化成功時寫入install.back文件),刷新應用程式,再觀察Druid數據源監控面板上數據源信息的變化。
最後,錄製了一個gif短片,你可以更直觀的看到整個測試過程。
補充
關於如何在生產環境中刷新Spring Boot應用,你可以點擊或複製下面連接訪問我的另一篇文章《在生產環境中重啟SpringBoot應用程式》:
https://www.ramostear.com/post/2019/16/21/39k9vuha.html