Spring Boot2.2.2整合H2和MySQL自由切換數據源

ramostear 發佈 2020-01-09T09:15:17+00:00

測試短動畫本文將介紹基於Spring Boot 2.2.2.RELEASE實現H2資料庫和MySQL資料庫兩個數據源的自由切換。在本文中,數據源實現使用阿里巴巴提供的Druid數據源。Spring Boot2.2.2整合H2和MySQL自由切換數據源1.



本文將介紹基於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


關鍵字: