前言 因为工作项目的新需求涉及到在SpringBoot中配置多个MySQL数据库,为了理解如何配置,学习了SpringBoot自动配置数据源的原理,在此记录。这篇文章先介绍SpringBoot配置单个数据源的原理,再介绍多个数据源的配置方法。
SpringBoot配置单个数据源 未指定spring.datasource.type时,SpringBoot的默认数据源 以下面application.yml为例:
1 2 3 4 5 6 7 8 9 10 11 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver server.port: 18081 mybatis: mapper-locations: classpath:mapper/*.xml
执行Junit Test看下SpringBoot使用的默认数据源是什么:
1 2 3 4 5 6 7 8 9 10 11 @RunWith (SpringRunner.class ) @SpringBootTest public class MysqlDemoApplicationTests { @Autowired DataSource dataSource; @Test public void contextLoads () throws SQLException { System.out.println("dataSource: " + dataSource.getClass()); } }
在未指定spring.datasource.type的情况下,SpringBoot会使用默认数据源,在1.X版本中,默认数据源是org.apache.tomcat.jdbc.pool.DataSource;在2.X版本中,默认数据源是com.zaxxer.hikari.HikariDataSource。
以下源码版本都是SpringBoot 2.3.0
SpringBoot是怎么指定数据源的?道理在DataSourceConfiguration这个类,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 abstract class DataSourceConfiguration { @SuppressWarnings ("unchecked" ) protected static <T> T createDataSource (DataSourceProperties properties, Class<? extends DataSource> type) { return (T) properties.initializeDataSourceBuilder().type(type).build(); } @Configuration (proxyBeanMethods = false ) @ConditionalOnClass (org.apache.tomcat.jdbc.pool.DataSource.class ) @ConditionalOnMissingBean (DataSource .class ) @ConditionalOnProperty (name = "spring.datasource.type" , havingValue = "org.apache.tomcat.jdbc.pool.DataSource" , matchIfMissing = true ) static class Tomcat { @Bean @ConfigurationProperties (prefix = "spring.datasource.tomcat" ) org.apache.tomcat.jdbc.pool.DataSource dataSource (DataSourceProperties properties) { org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties, org.apache.tomcat.jdbc.pool.DataSource.class ) ; DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl()); String validationQuery = databaseDriver.getValidationQuery(); if (validationQuery != null ) { dataSource.setTestOnBorrow(true ); dataSource.setValidationQuery(validationQuery); } return dataSource; } } @Configuration (proxyBeanMethods = false ) @ConditionalOnClass (HikariDataSource.class ) @ConditionalOnMissingBean (DataSource .class ) @ConditionalOnProperty (name = "spring.datasource.type" , havingValue = "com.zaxxer.hikari.HikariDataSource" , matchIfMissing = true ) static class Hikari { @Bean @ConfigurationProperties (prefix = "spring.datasource.hikari" ) HikariDataSource dataSource (DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class ) ; if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } } @Configuration (proxyBeanMethods = false ) @ConditionalOnClass (org.apache.commons.dbcp2.BasicDataSource.class ) @ConditionalOnMissingBean (DataSource .class ) @ConditionalOnProperty (name = "spring.datasource.type" , havingValue = "org.apache.commons.dbcp2.BasicDataSource" , matchIfMissing = true ) static class Dbcp2 { @Bean @ConfigurationProperties (prefix = "spring.datasource.dbcp2" ) org.apache.commons.dbcp2.BasicDataSource dataSource (DataSourceProperties properties) { return createDataSource(properties, org.apache.commons.dbcp2.BasicDataSource.class ) ; } } @Configuration (proxyBeanMethods = false ) @ConditionalOnMissingBean (DataSource.class ) @ConditionalOnProperty (name = "spring.datasource.type" ) static class Generic { @Bean DataSource dataSource (DataSourceProperties properties) { return properties.initializeDataSourceBuilder().build(); } } }
在前三个内部类Tomcat、Hikari、Dbcp2上的注解:
@Configuration(proxyBeanMethods = false):会装配Bean的配置类
@ConditionalOnClass(XXX.class):给定的XXX.class在系统中存在时,就实例化当前Bean
@ConditionalOnMissingBean(DataSource.class):若IOC容器中还不存在DataSource这个Bean,就在IOC容器中实例化DataSource Bean
@ConditionalOnProperty(name = “spring.datasource.type”, havingValue = “XXX”,matchIfMissing = true):配置中,是否有spring.datasource.type=XXX的配置。matchIfMissing = true表示,即使没有指定配置,该配置类也生效
@ConditionalOnProperty 注解中若没有配置 havingValue 属性,则处理方式为:若指定配置项值=false,则当前注解修饰的配置类不生效,否则生效。若没有配置 matchIfMissing 属性,则默认 matchIfMissing=true。
因此,以Tomcat类为例,这些注解表示,若系统存在org.apache.tomcat.jdbc.pool.DataSource.class,即使没有配置spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource,也是使用org.apache.tomcat.jdbc.pool.DataSource作为数据源。
当我们指定了spring.datasource.type时,就使最后一个类Generic生效,它会初始化spring.datasource.type配置项指定的数据源。
PS:为什么1.X和2.X两个版本默认的数据源不同? 因为在spring-boot-start-jdbc 1.X中,默认引入依赖tomcat-jdbc;而在spring-boot-start-jdbc 2.X中,默认引入依赖HikariCP
指定spring.datasource.type,使用Druid数据源 application.yml更新为:
1 2 3 4 5 6 7 8 9 10 11 12 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource server.port: 18081 mybatis: mapper-locations: classpath:mapper/*.xml
再执行上面的Junit Test,可以看到dataSource换成了com.alibaba.druid.pool.DruidDataSource。这个就是由上面的DataSourceConfiguration类下的Generic初始化的。
配置Druid数据库连接池参数并使其生效 application.yml更新为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 server.port: 18081 mybatis: mapper-locations: classpath:mapper/*.xml
但是,通过运行下面的Junit Test,我们可以看到,数据库连接池的属性如maxActive并没有生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RunWith (SpringRunner.class ) @SpringBootTest public class MysqlDemoApplicationTests { @Autowired DataSource dataSource; @Test public void contextLoads () throws SQLException { System.out.println("dataSource: " + dataSource.getClass()); Connection connection = dataSource.getConnection(); System.out.println("connection: " + connection); if (dataSource instanceof DruidDataSource) { DruidDataSource dDataSource = (DruidDataSource) dataSource; System.out.println("maxActive: " + dDataSource.getMaxActive()); System.out.println("minIdle: " + dDataSource.getMinIdle()); } connection.close(); } }
我们配置的是maxActive=20,minIdle=1,打印出来的是maxActive=8,minIdle=0。 什么原因呢?我们先看上面那个DataSourceConfiguration类下的Generic的方法;
1 2 3 4 @Bean DataSource dataSource (DataSourceProperties properties) { return properties.initializeDataSourceBuilder().build(); }
查看DataSourceProperties类的源码,它用注解@ConfigurationProperties(prefix = "spring.datasource")把”spring.datasource”下的属性映射到自己的同名属性中了,但在这个类中,和我们配置项同名的只有url、username、password、driver-class-name、type这5个属性,并没有maxActive这样的数据库连接池属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @ConfigurationProperties (prefix = "spring.datasource" )public class DataSourceProperties implements BeanClassLoaderAware , InitializingBean { private Class<? extends DataSource> type; private String driverClassName; private String url; private String username; private String password; public DataSourceBuilder<?> initializeDataSourceBuilder() { return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName()) .url(determineUrl()).username(determineUsername()).password(determinePassword()); } }
@ConfigurationProperties(prefix=”spring.datasource”)注解表示把”spring.datasource”下的属性映射到实体类的同名属性上。它的用法有:标注在类声明上,然后用其他注解如 @Component 或 @Bean 注入这个 Bean 即可生效;另一个是不标注在类声明上,而是用 @Bean 注入这个类的 Bean 时,标注在 @Bean 修饰的方法上也可生效。
再查看DataSourceBuilder类build()方法源码,可以看到这里只是反射调用DruidDataSource()构造函数,建立DruidDataSource对象,没有给这个对象属性赋值。DruidDataSource中的maxActive属性是有默认值的,这个默认值就是我们打印出的值。
1 2 3 4 5 6 7 public T build () { Class<? extends DataSource> type = getType(); DataSource result = BeanUtils.instantiateClass(type); maybeGetDriverClassName(); bind(result); return (T) result; }
那么怎样让我们的配置生效呢?SpringBoot的官方文档给了我们三种方法。 新建一个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties (prefix="spring.datasource" ) public DataSource dataSource () { return new DruidDataSource(); } }
三种方法都能使我们的配置生效。第二种和第三种方法的里面就是反射调用DruidDataSource()构造函数。因为@ConfigurationProperties注解的作用,我们的配置属性被映射到了DruidDataSource对象属性上,即使DruidDataSource是通过反射建立的也会生效。
关于第三种方法,若我们的url、username、password、driver-class-name、type五个配置和别的数据库连接池配置是分开的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: filters: stat maxActive: 20 initialSize: 1 maxWait: 60000
第三种方法就应该这样写:
1 2 3 4 5 6 7 8 9 10 11 12 @Bean @Primary @ConfigurationProperties ("spring.datasource" )public DataSourceProperties dataSourceProperties () { return new DataSourceProperties(); } @Bean @ConfigurationProperties ("druid" )public DataSource dataSource (DataSourceProperties properties) { return properties.initializeDataSourceBuilder().type(DruidDataSource.class ).build () ; }
原因就是,”spring.datasource.*“属性只映射到DataSourceProperties对象,properties反射创建DruidDataSource对象后,”druid.*“属性才映射到DruidDataSource对象。
SpringBoot+Mybatis配置多个数据源 这里指的是配置多个Mysql数据源。 配置文件application.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spring: datasource: db1: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 db2: url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource filters: stat minIdle: 1 maxActive: 20 initialSize: 1 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 3000000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20
理解了配置单个数据源的方法后,很容易写出多个数据源的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Configuration public class DataSourceConfig { @Bean @Primary @ConfigurationProperties ("spring.datasource.db1" ) public DataSourceProperties ds1DataSourceProperties () { return new DataSourceProperties(); } @Bean @Primary @ConfigurationProperties ("spring.datasource" ) public DataSource ds1DataSource () { return ds1DataSourceProperties().initializeDataSourceBuilder().type(DruidDataSource.class ).build () ; } @Bean @ConfigurationProperties ("spring.datasource.db2" ) public DataSourceProperties ds2DataSourceProperties () { return new DataSourceProperties(); } @Bean @ConfigurationProperties ("spring.datasource" ) public DataSource ds2DataSource () { return ds2DataSourceProperties().initializeDataSourceBuilder().type(DruidDataSource.class ).build () ; } }
用Junit Test验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RunWith (SpringRunner.class ) @SpringBootTest public class MysqlMultipleDemoApplicationTests { @Test public void contextLoads () { Object ds1DataSource = ApplicationContextUtil.getBean("ds1DataSource" ); if (ds1DataSource instanceof DruidDataSource) { DruidDataSource ds = (DruidDataSource) ds1DataSource; System.out.println(ds.getUrl()); } Object ds2DataSource = ApplicationContextUtil.getBean("ds2DataSource" ); if (ds2DataSource instanceof DruidDataSource) { DruidDataSource ds = (DruidDataSource) ds2DataSource; System.out.println(ds.getUrl()); } } }
打印的内容与我们的配置相对应。
如果是用JdbcTemplate,给不同的JdbcTemplate指定不同的数据源即可。但现在项目多是用Mybatis,有多个数据源存在时,需要把mapper.xml和mapper.java文件与数据源对应起来。 新建两个数据库的Mybatis配置类: 1.MybatisConfigDB1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration @MapperScan (basePackages ="com.example.demo.mysql.multiple.dao.test" , sqlSessionTemplateRef = "ds1SqlSessionTemplate" )public class MybatisConfigDB1 { @Primary @Bean ("ds1SqlSessionFactory" ) public SqlSessionFactory ds1SqlSessionFactory (@Qualifier("ds1DataSource" ) DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver(). getResources("classpath:mapper/test/*.xml" )); sqlSessionFactory.setTypeAliasesPackage("com.example.demo.mysql.multiple.domain.test" ); return sqlSessionFactory.getObject(); } @Primary @Bean (name = "ds1TransactionManager" ) public DataSourceTransactionManager ds1TransactionManager (@Qualifier("ds1DataSource" ) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Primary @Bean (name = "ds1SqlSessionTemplate" ) public SqlSessionTemplate ds1SqlSessionTemplate (@Qualifier("ds1SqlSessionFactory" ) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }
2.MybatisConfigDB2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Configuration @MapperScan (basePackages ="com.example.demo.mysql.multiple.dao.test2" , sqlSessionTemplateRef = "ds2SqlSessionTemplate" )public class MybatisConfigDB2 { @Bean ("ds2SqlSessionFactory" ) public SqlSessionFactory ds1SqlSessionFactory (@Qualifier("ds2DataSource" ) DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver(). getResources("classpath:mapper/test2/*.xml" )); sqlSessionFactory.setTypeAliasesPackage("com.example.demo.mysql.multiple.domain.test2" ); return sqlSessionFactory.getObject(); } @Bean (name = "ds2TransactionManager" ) public DataSourceTransactionManager ds1TransactionManager (@Qualifier("ds2DataSource" ) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean (name = "ds2SqlSessionTemplate" ) public SqlSessionTemplate ds1SqlSessionTemplate (@Qualifier("ds2SqlSessionFactory" ) SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }
可以自己写个Junit Test验证,看每个数据源下的mapper.java和mapper.xml是否有效。