禁用自动配置

Spring Boot大量使用自动配置和默认配置,极大地减少了代码,通常只需要加上几个注解,并按照默认规则设定一下必要的配置即可。例如,配置JDBC,默认情况下,只需要配置一个spring.datasource

  1. spring:
  2. datasource:
  3. url: jdbc:hsqldb:file:testdb
  4. username: sa
  5. password:
  6. dirver-class-name: org.hsqldb.jdbc.JDBCDriver

Spring Boot就会自动创建出DataSourceJdbcTemplateDataSourceTransactionManager,非常方便。

但是,有时候,我们又必须要禁用某些自动配置。例如,系统有主从两个数据库,而Spring Boot的自动配置只能配一个,怎么办?

这个时候,针对DataSource相关的自动配置,就必须关掉。我们需要用exclude指定需要关掉的自动配置:

  1. @SpringBootApplication
  2. // 启动自动配置,但排除指定的自动配置:
  3. @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
  4. public class Application {
  5. ...
  6. }

现在,Spring Boot不再给我们自动创建DataSourceJdbcTemplateDataSourceTransactionManager了,要实现主从数据库支持,怎么办?

让我们一步一步开始编写支持主从数据库的功能。首先,我们需要把主从数据库配置写到application.yml中,仍然按照Spring Boot默认的格式写,但datasource改为datasource-masterdatasource-slave

  1. spring:
  2. datasource-master:
  3. url: jdbc:hsqldb:file:testdb
  4. username: sa
  5. password:
  6. dirver-class-name: org.hsqldb.jdbc.JDBCDriver
  7. datasource-slave:
  8. url: jdbc:hsqldb:file:testdb
  9. username: sa
  10. password:
  11. dirver-class-name: org.hsqldb.jdbc.JDBCDriver

注意到两个数据库实际上是同一个库。如果使用MySQL,可以创建一个只读用户,作为datasource-slave的用户来模拟一个从库。

下一步,我们分别创建两个HikariCP的DataSource

  1. public class MasterDataSourceConfiguration {
  2. @Bean("masterDataSourceProperties")
  3. @ConfigurationProperties("spring.datasource-master")
  4. DataSourceProperties dataSourceProperties() {
  5. return new DataSourceProperties();
  6. }
  7. @Bean("masterDataSource")
  8. DataSource dataSource(@Autowired @Qualifier("masterDataSourceProperties") DataSourceProperties props) {
  9. return props.initializeDataSourceBuilder().build();
  10. }
  11. }
  12. public class SlaveDataSourceConfiguration {
  13. @Bean("slaveDataSourceProperties")
  14. @ConfigurationProperties("spring.datasource-slave")
  15. DataSourceProperties dataSourceProperties() {
  16. return new DataSourceProperties();
  17. }
  18. @Bean("slaveDataSource")
  19. DataSource dataSource(@Autowired @Qualifier("slaveDataSourceProperties") DataSourceProperties props) {
  20. return props.initializeDataSourceBuilder().build();
  21. }
  22. }

注意到上述class并未添加@Configuration@Component,要使之生效,可以使用@Import导入:

  1. @SpringBootApplication
  2. @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
  3. @Import({ MasterDataSourceConfiguration.class, SlaveDataSourceConfiguration.class})
  4. public class Application {
  5. ...
  6. }

此外,上述两个DataSource的Bean名称分别为masterDataSourceslaveDataSource,我们还需要一个最终的@Primary标注的DataSource,它采用Spring提供的AbstractRoutingDataSource,代码实现如下:

  1. class RoutingDataSource extends AbstractRoutingDataSource {
  2. @Override
  3. protected Object determineCurrentLookupKey() {
  4. // 从ThreadLocal中取出key:
  5. return RoutingDataSourceContext.getDataSourceRoutingKey();
  6. }
  7. }

RoutingDataSource本身并不是真正的DataSource,它通过Map关联一组DataSource,下面的代码创建了包含两个DataSourceRoutingDataSource,关联的key分别为masterDataSourceslaveDataSource

  1. public class RoutingDataSourceConfiguration {
  2. @Primary
  3. @Bean
  4. DataSource dataSource(
  5. @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
  6. @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {
  7. var ds = new RoutingDataSource();
  8. // 关联两个DataSource:
  9. ds.setTargetDataSources(Map.of(
  10. "masterDataSource", masterDataSource,
  11. "slaveDataSource", slaveDataSource));
  12. // 默认使用masterDataSource:
  13. ds.setDefaultTargetDataSource(masterDataSource);
  14. return ds;
  15. }
  16. @Bean
  17. JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
  18. return new JdbcTemplate(dataSource);
  19. }
  20. @Bean
  21. DataSourceTransactionManager dataSourceTransactionManager(@Autowired DataSource dataSource) {
  22. return new DataSourceTransactionManager(dataSource);
  23. }
  24. }

仍然需要自己创建JdbcTemplatePlatformTransactionManager,注入的是标记为@PrimaryRoutingDataSource

这样,我们通过如下的代码就可以切换RoutingDataSource底层使用的真正的DataSource

  1. RoutingDataSourceContext.setDataSourceRoutingKey("slaveDataSource");
  2. jdbcTemplate.query(...);

只不过写代码切换DataSource即麻烦又容易出错,更好的方式是通过注解配合AOP实现自动切换,这样,客户端代码实现如下:

  1. @Controller
  2. public class UserController {
  3. @RoutingWithSlave // <-- 指示在此方法中使用slave数据库
  4. @GetMapping("/profile")
  5. public ModelAndView profile(HttpSession session) {
  6. ...
  7. }
  8. }

实现上述功能需要编写一个@RoutingWithSlave注解,一个AOP织入和一个ThreadLocal来保存key。由于代码比较简单,这里我们不再详述。

如果我们想要确认是否真的切换了DataSource,可以覆写determineTargetDataSource()方法并打印出DataSource的名称:

  1. class RoutingDataSource extends AbstractRoutingDataSource {
  2. ...
  3. @Override
  4. protected DataSource determineTargetDataSource() {
  5. DataSource ds = super.determineTargetDataSource();
  6. logger.info("determin target datasource: {}", ds);
  7. return ds;
  8. }
  9. }

访问不同的URL,可以在日志中看到两个DataSource,分别是HikariPool-1hikariPool-2

  1. 2020-06-14 17:55:21.676 INFO 91561 --- [nio-8080-exec-7] c.i.learnjava.config.RoutingDataSource : determin target datasource: HikariDataSource (HikariPool-1)
  2. 2020-06-14 17:57:08.992 INFO 91561 --- [io-8080-exec-10] c.i.learnjava.config.RoutingDataSource : determin target datasource: HikariDataSource (HikariPool-2)

我们用一个图来表示创建的DataSource以及相关Bean的关系:

  1. ┌────────────────────┐ ┌──────────────────┐
  2. @Primary │<──────│ JdbcTemplate
  3. RoutingDataSource └──────────────────┘
  4. ┌────────────────┐ ┌──────────────────┐
  5. MasterDataSource │<──────│DataSource
  6. └────────────────┘ TransactionManager
  7. ┌────────────────┐ └──────────────────┘
  8. SlaveDataSource
  9. └────────────────┘
  10. └────────────────────┘

注意到DataSourceTransactionManagerJdbcTemplate引用的都是RoutingDataSource,所以,这种设计的一个限制就是:在一个请求中,一旦切换了内部数据源,在同一个事务中,不能再切到另一个,否则,DataSourceTransactionManagerJdbcTemplate操作的就不是同一个数据库连接。

练习

禁用自动配置 - 图1下载练习:禁用DataSourceAutoConfiguration并配置多数据源 (推荐使用IDE练习插件快速下载)

小结

可以通过@EnableAutoConfiguration(exclude = {...})指定禁用的自动配置;

可以通过@Import({...})导入自定义配置。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

禁用自动配置 - 图2 禁用自动配置 - 图3