光有依赖管理还不够
前面说过Spring Boot Starter 解决了项目开发中两个很棘手的问题:
- 依赖管理
- 自动配置
第一个问题在前面的文章中已经详细介绍了。
只有依赖管理还不够。
我们还是用购买电脑
的例子来说明。
解决了依赖管理的问题,就相当于去电脑城购买电脑,商家给你一堆必要的组件,比如CPU、主板、显卡、内存等等,并保证组装一台电脑必要的组件都在这里,并且这些组件是相互兼容的。
但是离开机启动原神还差还远。
组装电脑不仅仅一堆必要的配件就够了,还需要把把它们组装起来,并做一些初始化或配置,比如安装驱动等等。
幸运的是,Spring Boot 已经帮我们完成了这一点,这就是今天说的Spring Boot 的自动配置机制。
什么是自动配置
还是拿前面章节我们说的 Spring 应用中集成 Hibernate 举例。
对于手动集成,首先我们需要选择并引入一个持久层的库,比如Hibernate或者MyBatis。
通常情况下,需要手动配置数据库连接、事务管理等必要的组件。
但是在 Spring Boot 中,当我们引入相关的依赖时,Spring Boot 会自动检测到classpath下这个数据库依赖,然后创建并配置 datasource、实体管理器entityManagerFactory、事务管理器transactionManager等 这些必要的bean。
这样无需再手动编写那些样板配置,因为 Spring Boot 已经自动帮我们完成了,这样就大大简化了开发过程。
这就好比组装电脑时,系统看到一堆组件中有鼠标,就会默认下载并安装鼠标驱动,并且按照一般用户习惯设置好鼠标,这样开机后,我们就可以直接使用鼠标进行操作了。
启动自动配置
然后我们唯一要做的只是启动自动配置机制,在启动类或配置类中使用注解 @EnableAutoConfiguration 或者 @SpringBootApplication。
@SpringBootApplication
public class SkybootApplication {
public static void main(String[] args) {
SpringApplication.run(SkybootApplication.class, args);
}
}
@SpringBootApplication
这个注解是复合注解,它包含了@EnableAutoConfiguration
.
开启了自动配置后,Spring Boot 就检测到Hibernate等持久层依赖后,就会配置持久层的bean,这是会用到数据库连接信息,比如 mysql 地址和账号密码。如果不配置,启动时会报错:
Failed to configure a DataSource: ‘url’ attribute is not specified and no embedded datasource could be configured.
所以第一步,我们需要在 application.properties
中配置数据库连接信息:
spring.datasource.url=jdbc:mysql://localhost:3306/jones
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
这样,我们的 Spring Boot 应用就能正常启动了。
根据启动日志分析
如果想要看 Spring Boot 应用启动了哪些自动配置,我们可以打开启动的 debug log。
通过启动时加 --debug
参数,我们就能打开debug log。
在IDEA中 Spring Boot 主类中右键运行:
Modify Run Configurations -> Modify options -> Program arguments -> 输入 --debug
添加参数启动后,我们可以观察debug日志,重点关注和数据库操作相关的部分:
Positive matches:
DataSourceAutoConfiguration
matched:- @ConditionalOnClass found required classes ‘javax.sql.DataSource’, ‘org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType’ (OnClassCondition)
- @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)
DataSourceConfiguration.Hikari
matched:- @ConditionalOnClass found required class
'com.zaxxer.hikari.HikariDataSource'
(OnClassCondition) - @ConditionalOnProperty (spring.datasource.type=com.zaxxer.hikari.HikariDataSource) matched (OnPropertyCondition)
- @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition)
- @ConditionalOnClass found required class
DataSourceTransactionManagerAutoConfiguration
matched:- @ConditionalOnClass found required classes ‘org.springframework.jdbc.core.JdbcTemplate’,
'org.springframework.transaction.TransactionManager'
(OnClassCondition)
- @ConditionalOnClass found required classes ‘org.springframework.jdbc.core.JdbcTemplate’,
我们可以大致看到数据库连接池 HikariDataSource
和 事务管理器 TransactionManager
。
后面的章节,我们会结合启动日志,深入分析自动配置的实现。
覆盖默认配置
Spring Boot 自动配置有一个很重要的原则,就是 约定大于配置
。意思是 Spring Boot 很多模块,我们几乎不需要配置,Spring Boot 会按照大部分项目的习惯配置这些模块。
如果这些自动配置不太满足我们的需求,这时候我们就需要覆盖默认的配置了。
拿前面自动配置 Hibernate 的例子来说,Spring Boot 的自动配置会自动帮我们创建 一个name为 dataSource
的bean,然后注入到 Spring 容器中。
如果我们不想用默认配置的 dataSource
,很简单,我们只要自定义个 name
为 dataSource
的 bean
就可以了,这样就会覆盖掉自动配置创建的那个dataSource bean,Spring 会使用我们手动配置的DataSource。
@Configuration
public class DataSourceConfiguration {
@Bean
public DataSource dataSource(){
// ...
}
}
禁用自动配置
自动配置这个功能,既然能开启,也就能关闭。
某些特殊场景,我们的classpatch下有持久层的依赖,但是我们不想使用 Spring Boot的自动配置。
那么我们可以禁用它,在黑名单exclude中设置不要自动配置的类
@Configuration
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class MyConfiguration {
}
这样就会自动的不加载这个配置类了。在日志中可以看到:
Exclusions:
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
除了在代码中配置,我们还可以在配置文件 application.properties
中配置,效果一样:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
不适用自动配置的场景
在分析 Spring Boot 自动配置的原理前,我们先玩一个游戏。
尝试想象一下没有 Spring Boot 自动配置这个东西,我们只能用 Spring 集成和配置第三方的框架。
想象你所在的公司有100个项目,有一天,你发现几乎每个项目都有这一段配置:
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
// 创建自定义的 DataSource
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.url("jdbc:mysql://localhost:3306/jones");
dataSourceBuilder.username("root");
dataSourceBuilder.password("123456");
return dataSourceBuilder.build();
}
}
这段代码的作用是手动创建一个 name
为 dataSource
的bean,并注入到 Spring容器中。
出于一个优秀开发人员的本能,你觉得可以把它们抽取成一个公共的配置,给所有的项目使用。
- 于是你创建了一个单独的项目,叫common-datasource
- 然后打包成jar,发布到公司的Nexus上。
- 原来的项目就可以删掉原来 创建
dataSource
那段代码了 - 原来的项目需要在
pom.xml
的<dependencies>
中引入这个jar依赖 - 在原来的项目的配置类中使用
@Import
注解把DataSourceConfig
- 这样这些项目就自动配置了
dataSource
bean
@Configuration
@Import({DataSourceConfig.class})
public class AppConfig {
}
这样你成功的把公司上百个项目中相同的一些代码都抽取到了同一个项目,并且可以直接引用它。
这样看上去很好,不过还不够太完美。
想象一下如果公司上百个项目,大部分用的是关系数据库,直接用上面的配置就行了。
- 但是少部分项目没有用关系型数据库,而是用的其它技术比如用的是Redis。那它就不应该使用 @Import({DataSourceConfig.class})。
- 这个配置类下面有很多Bean,但是我只想用其中的一个呢?你直接不声明,全部都不能用了。
@Configuration
public class DataSourceConfig {
@Bean
public EsSource EsSource(){
//...
}
@Bean
public RedisSource rediesSource(){
//...
}
@Bean
public DataSource dataSource() {
// 创建自定义的 DataSource
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.url("jdbc:mysql://localhost:3306/jones");
dataSourceBuilder.username("root");
dataSourceBuilder.password("123456");
return dataSourceBuilder.build();
}
}
对的,它不声明@Import({DataSourceConfig.class})就可以了。
或者把EsSource、RedisSource放到单独的配置类中,然后逐个导入:
@Configuration
@Import({RedisSourceConfig.class, EsSourceConfig.class})
public class AppConfig {
}
这么做没问题,但是太麻烦了,有没有更优雅的做法呢?
有条件的选择
有没有这样一个注解:
能不能所有的所有项目都可以无脑导入 @Import({DataSourceConfig.class})
但是要不要启用这个配置,需要看情况,这个看情况,就是@Conditional注解
@Conditional(SomeCondition.class)
当条件满足的时候,这个配置就生效!
有了这个有条件的注解,于是,公共配置类变成了
@Configuration
public class DataSourceConfig {
@Conditional(ConditionOne.class)
@Bean
public RedisSource redisSource() {
return new RedisSource();
}
@Conditional(ConditionTwo.class)
@Bean
public DataSource dataSource() {
}
}
这样,我们实现了所有地方都自动注入公共配置,但是看情况是否要启动配置。
很高明的地方是,这个条件(是否启用的逻辑)是跟着配置走的
。
这是不是一种开闭原则? 对扩展开放,对修改关闭。换句话说,当需要增加新功能时,应该通过扩展现有的代码来实现,而不是直接修改已有的代码。
条件注解
上面说的 @Conditional
就是 Spring框架提供的 条件注解
有了这个条件注解,我们的配置的灵活性大大提高了
看看它怎么使用
@Conditional(ConditionTwo.class)
@Bean
public DataSource dataSource() {
}
它的值需要时一个实现了org.springframework.context.annotation.Condition
表示一组条件,当达到这个条件时,Spring就会去创建这个bean并注册到工厂中取
伪代码
if (ConditionTwo.matched()) {
DataSource dataSource = new DataSource();
beanFactory.registerSingleton("dataSource", dataSource);
}
这个接口只有一个方法,是否匹配某个条件!
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
自定义条件
接下来看如果让我们来设计 Redis的自动配置时,我们会如何配置条件:
- 看classpath下是否与Jedis,通过反射new一个实例
- 看application.properties(或者其它配置方式注入进来)中是否有redis.flag这个配置项
public class JedisCondition implements Condition{
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean matched = jedisClassOnClassPath() && hasJedisInProperties(context);
log.info("JedisCondition 匹配结果:"+matched);
return matched;
}
private boolean hasJedisInProperties(ConditionContext context) {
return context.getEnvironment().containsProperty("redis.flag");
}
private boolean jedisClassOnClassPath() {
try {
Class.forName("redis.clients.jedis.Jedis");
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
}
}
然后使用这个条件:
@Slf4j
@Configuration
public class DataSourceConfig {
@Conditional(JedisCondition.class)
@Bean
public RedisSource redisSource() {
return new RedisSource();
}
public static class RedisSource{
public RedisSource(){
log.info("RedisSource 被初始化和加载了");
}
}
}
配置后,我们启动 Spring 应用,观察启动日志:
- 可以看到匹配的方法判定为true
- 配置类也被初始化了
- Spring Boot的自动配置项中也打印出了匹配结果:
这表示,我们的条件注解生效了,因为两个条件都满足了
- 类存在classpatch,因为依赖中添加了Jedis相关的依赖。
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>5.1.2</version> </dependency>
- 在
application.properties
中配置了redis.flag=true
我们尝试把redis.flag=true
从 application.properties
中移除,重启应用,发现没有匹配,bean也没有加载:
com.mt.skyboot.config.DataSourceConfig : JedisCondition 匹配结果:false
到这里,我们演示了如果自定义一个条件,并用它可以有条件的启用某个配置类!
Spring 内置的条件注解
在上面,我们自定义了一个条件注解:
public class JedisCondition implements Condition{
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean matched = jedisClassOnClassPath() && hasJedisInProperties(context);
log.info("JedisCondition 匹配结果:"+matched);
return matched;
}
private boolean hasJedisInProperties(ConditionContext context) {
return context.getEnvironment().containsProperty("redis.flag");
}
private boolean jedisClassOnClassPath() {
try {
Class.forName("redis.clients.jedis.Jedis");
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
}
}
它定义了两个条件:
- classpath下是否有某个类
- application.properties中是否有某个配置项
这么做当然没问题,但是其实这两个条件是非常通用的需求!
所以 Spring Boot 帮我们内置了这两个条件注解
@ConditionalOnClass
:用来判断 classpath下是否有某个类@ConditionalOnProperty
:用来判断 application.properties中是否有某个配置项
所以大胆的删掉前面自定义的JedisCondition类,直接用Spring Boot提供的条件注解就可以了!
@ConditionalOnClass(Jedis.class)
@ConditionalOnProperty(name = "redis.flag")
@Bean
public RedisSource redisSource() {
return new RedisSource();
}
重启应用,观察启动日志,可以看到被加载了!
不仅仅是这两个条件注解,Spring 还提供了很多条件注解,满足了大部分的需求:
- ConditionalOnBean
- ConditionalOnCheckpointRestore
- ConditionalOnClass
- ConditionalOnCloudPlatform
- ConditionalOnExpression
- ConditionalOnJava
- ConditionalOnJndi
- ConditionalOnMissingBean
- ConditionalOnMissingClass
- ConditionalOnNotWarDeployment
- ConditionalOnNotWebApplication
- ConditionalOnProperty
- ConditionalOnResource
- ConditionalOnSingleCandidate
- ConditionalOnThreading
- ConditionalOnWarDeployment
- ConditionalOnWebApplication
Hibernate 是如何自动配置的
回到我们最初的问题,Spring Boot 是如何对 Hibernate 完成自动配置的?
有了前面的铺垫,我们不难理解它的原理。
本质上,Spring Boot 提供的各种starter,也是这样自动配置的。
以DataSource
的自动配置为例.
Spring Boot 的启动日志中,和数据库有关的日志:
DataSourceAutoConfiguration
matched:- @ConditionalOnClass found required classes ‘javax.sql.DataSource’, ‘org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType’ (OnClassCondition)
- @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)
DataSourceConfiguration.Hikari
matched:- @ConditionalOnClass found required class ‘com.zaxxer.hikari.HikariDataSource’ (OnClassCondition)
- @ConditionalOnProperty (spring.datasource.type=com.zaxxer.hikari.HikariDataSource) matched (OnPropertyCondition)
- @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition)
可以看到,Spring Boot 自动帮我们配置了 数据库连接池 HikariDataSource
我们从头开始来梳理:
-
首先引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
上一篇文章我们分析了,它会把其它必要的依赖传递依赖进来,包括了 Spring Boot默认的数据库连接池
HikariCP
这个依赖。
HikariCP 是一个高性能的 JDBC 连接池库,被广泛应用于各种 Java 项目中。 -
启动自动配置
Spring Boot 启动类配置了@SpringBootApplication
注解,它是一个复合注解,包含了@EnableAutoConfiguration
,所以隐性的开启了自动配置特性。
在这个注解的背后,Spring Boot 会通过 SpringFactoriesLoader 加载 spring-boot-autoconfigure 模块中的自动配置类。在依赖中我们可以看到这个包,下面内置了几乎所有spring-boot-starter的自动配置类
-
自动配置类
DataSourceAutoConfiguration
在spring-boot-autoconfigure
模块中,有一个名为org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
的自动配置类,它负责自动配置数据源。这个类通常会检查类路径上是否存在某些特定的类或属性,以确定是否应该启用数据源自动配置。
这个配置类会成功加载,因为满足条件:
- 类路径下有DataSource这个类
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) @ConditionalOnMissingBean( type = {"io.r2dbc.spi.ConnectionFactory"} ) @EnableConfigurationProperties({DataSourceProperties.class}) @Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class}) public class DataSourceAutoConfiguration {
@EnableConfigurationProperties({DataSourceProperties.class})
会把application.properties中数据库连接的信息导入到配置中来:
所以它拿到了数据库连接信息:@ConfigurationProperties( prefix = "spring.datasource" ) public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
spring.datasource.url=jdbc:mysql://localhost:3306/jones spring.datasource.username=root spring.datasource.password=123456
- 类路径下有DataSource这个类
-
这个类有一个内部类
PooledDataSourceConfiguration
,它会负责配置数据库连接池@Conditional({PooledDataSourceCondition.class}) @ConditionalOnMissingBean({DataSource.class, XADataSource.class}) @Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class}) protected static class PooledDataSourceConfiguration { protected PooledDataSourceConfiguration() { } @Bean @ConditionalOnMissingBean({JdbcConnectionDetails.class}) PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) { return new PropertiesJdbcConnectionDetails(properties); } }
重点看:
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
因为Spring容易没有自定义DataSource
这个bean,所以满足条件,会导入几种数据库连接池的配置类,其中包含了Hikari
数据库连接池:DataSourceConfiguration.Hikari.class
-
Hikari是一个内部类,它上面的注解满足条件,所以会被加载
a.HikariDataSource.java
在classpath下面,第一步引入的
b. 容器中没有DataSource
的bean
c.spring.datasource.type
的值是com.zaxxer.hikari.HikariDataSource
,如果没配置,那就默认设置成它综上所述,条件都满足,所以就创建了
name
为dataSource
的HikariDataSource
的bean:自动配置HikariDataSource数据源的代码:
@ConditionalOnClass({HikariDataSource.class}) @ConditionalOnMissingBean({DataSource.class}) @ConditionalOnProperty( name = {"spring.datasource.type"}, havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true ) static class Hikari { Hikari() { } @Bean static HikariJdbcConnectionDetailsBeanPostProcessor jdbcConnectionDetailsHikariBeanPostProcessor(ObjectProvider<JdbcConnectionDetails> connectionDetailsProvider) { return new HikariJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider); } @Bean @ConfigurationProperties( prefix = "spring.datasource.hikari" ) HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) { HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(connectionDetails, HikariDataSource.class, properties.getClassLoader()); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
总结
到这里,我们分析了 Spring Boot 是如何自动配置 数据源 DataSource 的。总的说起来其实很简单:
- 启动自动配置
- 自动配置会扫描 classpath 下的依赖
- 扫描到特定的类存在(比如HikariDataSource.class)或者application.properties中配置了某些特定的配置项
- 就会自动创建某些bean(比如name为dataSource),并注入到Spring容器中
这是Spring Boot 系列专栏的第6篇,关注我,和我一起学透 Spring Boot.