测试短动画
本文将介绍基于Spring Boot 2.2.2.RELEASE实现H2数据库和MySQL数据库两个数据源的自由切换。在本文中,数据源实现使用阿里巴巴提供的Druid数据源。
Spring Boot2.2.2整合H2和MySQL自由切换数据源
1. 需求背景
在一些Web后台应用中,通常存在这样一种应用场景:当管理员第一次访问系统时,会自动跳转到系统初始化页面,要求管理员填写数据库主机地址,数据库名称,数据库管理员账户,数据库管理员密码以及系统的管理员账户和密码,然后点击安装按钮开始初始化后台数据,当后台数据初始化完成,页面将跳转到系统后台的登录页面。那么,如何使用Spring Boot完成这样一个功能呢?
在Spring Boot应用程序中,如果在类路径下存在某个数据库依赖(例如MySQL),则必须提供相应的数据源信息,否则应用程序将无法启动。如果想要在不配置数据源的情况下启动应用程序,可以参照下面的做法修改主类配置。
调整前:
@SpringBootApplicationpublic 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文件清单如下:
4.0.0org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE ... 1.85.1.471.2.17org.springframework.boot spring-boot-starter-data-jpaorg.springframework.boot spring-boot-configuration-processor trueorg.springframework.boot spring-boot-starter-webmysql mysql-connector-java ${mysql.version}runtimecom.h2database h2com.alibaba druid-spring-boot-starter 1.1.14
注:由于篇幅原因,省略了启动的一些配置项
3.1 配置文件
我们需要将系统配置文件拆分为两个application.yml和application-db.yml(也可以在一个配置文件中配置,分开配置为了结构清晰),此外在创建一个mysql.properties配置文件。下面简单的对这三个配置文件做一个介绍。
application.yml是应用程序的主配置文件(默认),主要放置应用程序的通用配置,例如:应用程序端口号,上下文路径,模板引擎,静态资源路径等等。application.yml配置清单如下:
server: servlet: context-path: / port: 80 max-http-header-size: 10000spring: 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: truexss: 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文件的代码清单如下:
@Configurationpublic 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 ThreadLocalDATASOURCE = 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, MaptargetDataSource){ 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){ MaptargetDataSource = 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文件的所有代码清单:
@Configurationpublic 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){ MaptargetDataSource = 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
扫码阅读更多