前言 因为工作项目的新需求涉及到在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是否有效。