Spring Boot2.2.2整合H2和MySQL自由切换数据源

2020-01-09 · 谭朝红 ·

本文将介绍基于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版本创建所需的工程,所需要的环境参数和工具如下:

名称 版本
JDK 1.8+
SpringBoot 2.2.2.RELEASE
Maven 3.2+
IntelliJ IDEA 2019.2
Druid 1.1.14
MySQL 5.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

Spring Boot2.2.2整合H2和MySQL自由切换数据源

本文将介绍基于Spring Boot 2.2.2.RELEASE实现H2数据库和MySQL数据库两个数据源的自由切换。在本文中,数据源实现使用阿里巴巴提供的Druid数据源