通过前面几个章节的介绍,相信大家已经对 Spring Framework 有了一个基本的认识,相比早期那些没有 Spring Framework 加持的项目而言,它让生产力产生了质的飞跃。但人们的追求是无止境的,这也驱动着技术的发展。开发者认为 Spring 还可以更好,于是 Spring Boot 诞生了。本章我们将一起了解一下 Spring Boot 的基础知识,还有它的两个重要功能——起步依赖与自动配置。
4.1 Spring Boot 基础知识
Spring Framework 提供了 IoC 容器、AOP、MVC 等众多功能,让开发者可以从烦琐的工作中抽离出来,只关注在自己的业务逻辑上。Perl 语言发明人 Larry Wal 说过一句名言:“懒惰,是程序员的第一大美德。” 当我们得到了一样东西,总会想着去追求更好的。而这个更好的东西就是 Spring Boot。有了 Spring Framework,为什么还需要搞出一个 Spring Boot?Spring Boot 又包含哪些东西呢?本节的内容将会回答这些问题。
4.1.1 为什么需要 Spring Boot
随着时间的推移,什么是烦琐的工作,这个定义也在发生变化。原先的参照物是 EJB 1. x 和 EJB 2. x,是徒手开发的 JSP 甚至是 CGI 程序;现在,创建一个基于 Spring 的项目本身变成了一件麻烦事——无论使用 Maven 还是 Gradle,要管理清楚这一堆依赖,避免出现冲突,已经是一场灾难了,我们永远都不知道哪个包里的同名类会带来什么“惊喜”。好不容易搞定了依赖,Spring Framework 的配置又该让人抓狂了,等到 Bean 的自动扫描和自动织入稍稍安抚了一下大家几近奔溃的内心,一大堆与业务逻辑没有太多关系的“模板”配置又“补了一刀”。当这些东西耗费的心智和开发业务逻辑相当,甚至超过业务逻辑时,开发者就该做点什么了。
就在广大开发者们快要接受这个事实,打算认命的时候,Spring 团队推出了一款代码生成器,它就是 Spring Roo 项目,官方介绍它是新一代的 Java 快速应用开发工具,在几分钟内就能构建一个完整的 Java 应用。但现实情况是大家不太买账,Spring Roo 一直都没能成为主流,截至本书写作之时,它的最新版本还是末次修改停留在 2017 年的 2.0.0 版本。虽然 Spring Roo 能帮忙生成各种代码和配置,但它们的数量并未减少。后来在笔者与 Spring 团队的 Josh Long 的一次交流过程中,他一语道破了真相,大意是:“如果一个东西可以生成出来,那为什么还要生成它呢?”
另一方面,Spring Framework 虽然解决了开发和测试的问题,但在整个系统的生命周期中,上线后的运维也占据了很大的比重,怎么样让系统具备更好的可运维性也是个重要的任务。怎么配置、怎么监控、怎么部署,都是要考虑的事情。
出于这些原因,Spring Boot 横空出世了,它解决了上面说到的各种痛点,再一次将生产力提升了一个台阶。正如 Spring Boot 项目首页上写的那样,Spring Boot 可以轻松创建独立的生产级 Spring 应用程序,拿来就能用了。这次,Spring Boot 站到了聚光灯下,成了新的主角。
4.1.2 Spring Boot 的组成部分
Spring Boot 提供了大量的功能,但其本身的核心主要是以下几点:
- 起步依赖
- 自动配置
- Spring Boot Actuator
- 命令行 CLI
在实际使用时,最后那项命令行 CLI 用得相对较少,因此本书并不会介绍它。此外,Spring Boot 同时支持 Java 与 Groovy,但在本书中,我们也不会涉及 Groovy 的内容。
-
起步依赖
起步依赖(starter dependency)的目的就是解决 4.1.1 节中提到的依赖管理难题:针对一个功能,需要引入哪些依赖、它们的版本又是什么、互相之间是否存在冲突、它们的间接依赖项之间是否存在冲突……现在我们可以把这些麻烦都交给 Spring Boot 的起步依赖来解决。
以我们在 1.2 节中创建的
HelloWorld
为例(即代码示例 1-1),我们只需在 Maven 的 POM 文件中引入org.springframework.boot:spring-boot-starter-web
这个依赖,Spring Boot 就知道我们的项目需要 Web 这个功能,它实际上为我们引入了大量相关的依赖项。通过mvn dependency:tree
可以查看 Maven 的依赖信息 ,其中就有如下的内容:+- org.springframework.boot:spring-boot-starter-web:jar:2.6.3:compile | +- org.springframework.boot:spring-boot-starter:jar:2.6.3:compile | | +- org.springframework.boot:spring-boot:jar:2.6.3:compile | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.6.3:compile | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.6.3:compile | | | +- ch.qos.logback:logback-classic:jar:1.2.10:compile | | | | \- ch.qos.logback:logback-core:jar:1.2.10:compile | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.1:compile | | | | \- org.apache.logging.log4j:log4j-api:jar:2.17.1:compile | | | \- org.slf4j:jul-to-slf4j:jar:1.7.33:compile | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile | | \- org.yaml:snakeyaml:jar:1.29:compile | +- org.springframework.boot:spring-boot-starter-json:jar:2.6.3:compile | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.1:compile | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.1:compile | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.13.1:compile | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.1:compile | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.1:compile | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.1:compile | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.6.3:compile | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.56:compile | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.56:compile | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.56:compile | +- org.springframework:spring-web:jar:5.3.15:compile | | \- org.springframework:spring-beans:jar:5.3.15:compile | \- org.springframework:spring-webmvc:jar:5.3.15:compile | +- org.springframework:spring-aop:jar:5.3.15:compile | +- org.springframework:spring-context:jar:5.3.15:compile | \- org.springframework:spring-expression:jar:5.3.15:compile
可以看到,起步依赖是以功能为单位来组织依赖的。要实现某个功能,一共需要哪些依赖,我们自己不清楚,但 Spring Boot 知道。我们会在 4.2 节详细讲解起步依赖。
-
自动配置
在没有使用 Spring Boot 时,对于一个 Web 项目,我们需要配置
DispatcherServlet
来处理请求,需要配置 Jackson JSON 来处理 JSON 的序列化,需要配置 Log4j2 或者 Logback 来打印日志……而在 1.2 节的HelloWorld
例子中,我们并没有配置这些东西,Spring Boot 自己完成了所有的配置,我们只需要编写REST
接口的逻辑就好了。这就是 Spring Boot 的自动配置,它能根据 CLASSPATH 中存在的类判断出引入了哪些依赖,并为这些依赖提供常规的默认配置,以此来消除模板化的配置。与此同时,Spring Boot 仍然给我们留下了很大的自由度,可以对配置进行各种定制,甚至能够排除自动配置。我们会在 4.3 节详细讲解自动配置。
-
Spring Boot Actuator
如果说前两项的目的是简化 Spring 项目的开发,那 Spring Boot Actuator 的目的则是提供一系列在生产环境运行时所需的特性,帮助大家监控并管理应用程序。通过 HTTP 端点或者 JMX,Spring Boot Actuator 可以实现健康检查、度量收集、审计、配置管理等功能。我们会在 5.1 节和 5.2 节详细讲解 Spring Boot Actuator。
4.1.3 解析 Spring Boot 工程
一个使用了 Spring Boot 的项目工程,本质上来说和只使用 Spring Framework 的工程是一样的,如果使用 Maven 来管理,那它就是个标准的 Maven 工程,大概的结构就像下面这样。
|-pom.xml
|-src
|-main
|-java
|-resources
|-test
|-java
|-resources
具体内容如下:
- pom.xml 中管理了整个项目的依赖和构建相关的信息;
src/main
中是生产的 Java 代码和相关资源文件;src/test
中是测试的 Java 代码和相关资源文件。
如果通过 Spring Initializr 来生成工程,它还会为我们生成用来启动项目的启动类,比如 HelloWorld
中的 Application
类,以及测试用的 ApplicationTest
类(这是个空的 JUnit 测试类)。其中 Application
类上加了 @SpringBootApplication
注解,表示这是应用的主类,在打包成可执行 Jar 包后,运行 Jar 包时会去调用这个主类的 main()
方法。
这里需要展开说明一下 POM 文件的内容,分为以下几个部分:
- 工程自身的 GroupId、ArtifactId 与 Version 等内容定义;
- 工程继承的
org.springframework.boot:spring-boot-starter-parent
定义; <dependencies/>
依赖项定义;<build/>
构建相关的配置定义。
org.springframework.boot:spring-boot-starter-parent
又继承了 org.springframework.boot:spring-boot-dependencies
,它通过 <dependencyManagement/>
定义了大量的依赖项,有了 <dependencyManagement/>
的加持,在我们自己的工程中,只需要在 <dependencies/>
中写入依赖项的 <groupId>
和 <artifactId>
就好了,无须指定版本,有冲突的依赖项也在 <dependencyManagement/>
中排除了,无须重复排除。
在 <build/>
中的 org.springframework.boot:spring-boot-maven-plugin
在打包时能够生成可执行 Jar 包,它也是在 org.springframework.boot:spring-boot-starter-parent
中定义的。
在一些特殊情况下,我们的工程无法直接继承 org.springframework.boot:spring-boot-starter-parent
,这时就可能失去 Spring Boot 的很多便利之处。为此,我们需要自己在 pom.xml 中做些额外的工作。
首先,增加 <dependencyManagement/>
,导入 org.springframework.boot:spring-boot-dependencies 中的依赖项,这样就能利用其中定义的依赖了:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接着,在 <build/>
中增加 org.springframework.boot:spring-boot-maven-plugin
,这样打包时就能用上 Spring Boot 的插件,打出可执行的 Jar 包:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.3</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
通过上述修改,我们就能在不继承 org.springframework.boot:spring-boot-starter-parent
的情况下继续让 Spring Boot 替我们管理依赖并构建可执行 Jar 包了。
4.2 起步依赖
在自己管理依赖时,我们要为工程引入 Web 相关的支持,需要配置一堆依赖,但我们常常会搞不清楚哪些是必需的,哪些是多余的,最后只能不管三七二十一从某个现在能跑的工程里胡乱复制一通。
有了 Spring Boot,情况就不一样了。Spring Boot 按照功能划分了很多起步依赖,大家只需要知道自己要什么功能,比如要实现 Web 功能、需要 JPA 支持等,具体引入什么依赖、分别是什么版本,都可以交给起步依赖来管理。
此外,管理依赖时不仅要避免出现 GroupId 和 ArtifactId 相同但 Version 不同的依赖,还要注意同一个依赖项因为版本升级替换了 GroupId 或 ArtifactId 的情况。对于前者 Maven 会仅保留一个依赖,但它未必是你想要的那个,而对于后者则更糟糕,Maven 会认为这是两个不同的依赖,它们都会被保留下来。但用了 Spring Boot 的起步依赖之后,此类问题就能得到缓解,同一版本的 Spring Boot 中的各个起步依赖所引入的依赖不会产生冲突,因为官方对这些依赖进行了严格的测试。
所以说起步依赖是帮助大家摆脱依赖管理困局的一大利器,这节就让我们来了解一下 Spring Boot 都提供了哪些起步依赖,它背后的实现原理又是什么样的。
4.2.1 Spring Boot 内置的起步依赖
Spring Boot 官方的起步依赖都遵循一样的命名规范,即都以 spring-boot-starter-
开头,其他第三方的起步依赖都应该 避免使用这个前缀,以免引起混淆。
Spring Boot 内置了超过 50 个不同的起步依赖,表 4-1 罗列了其中 10 个常用的起步依赖。
表 4-1 一些常用的 Spring Boot 起步依赖
名称 | 描述 |
---|---|
spring-boot-starter | Spring Boot 的核心功能,比如自动配置、配置加载等 |
spring-boot-starter-actuator | Spring Boot 提供的各种生产级特性 |
spring-boot-starter-aop | Spring AOP 相关支持 |
spring-boot-starter-data-jpa | Spring Data JPA 相关支持,默认使用 Hibernate 作为 JPA 实现 |
spring-boot-starter-data-redis | Spring Data Redis 相关支持,默认使用 Lettuce 作为 Redis 客户端 |
spring-boot-starter-jdbc | Spring 的 JDBC 支持 |
spring-boot-starter-logging | 日志依赖,默认使用 Logback |
spring-boot-starter-security | Spring Security 相关支持 |
spring-boot-starter-test | 在 Spring 项目中进行测试所需的相关库 |
spring-boot-starter-web | 构建 Web 项目所需的各种依赖,默认使用 Tomcat 作为内嵌容器 |
后续大家还会接触到很多起步依赖,比如 Spring Cloud 的各种组件,也有第三方的,比如 MyBatis 的 mybatis-spring-boot-starter
和 Druid 的 druid-spring-boot-starter
。
在引入了起步依赖后,如果我们希望修改某些依赖的版本,如何操作呢?可以在 Maven 的 <properties/>
中指定对应依赖的版本。通常这种情况是想要升级某些依赖、修复安全漏洞或使用新功能,但 Spring Boot 的依赖并未升级。例如,想要指定 Jackson 的版本来升级 Jackson Databind,就可以像下面这样:
<properties>
<jackson-bom.version>2.11.0</jackson-bom.version>
</properties>
具体的属性可以在 org.springframework.boot:spring-boot-dependencies
的 pom.xml 中寻找。当然,也可以再彻底一些,在项目 POM 文件的 <dependencies/>
中直接引入自己所需要的依赖,同时,在引入的起步依赖中排除刚才你所加的依赖。
Spring Boot 本身也提供了一些可以互相替换的起步依赖,例如,Log4j2 可以代替 Logback,Jetty 和 Netty 可以代替 Tomcat,如代码示例 4-1 所示。
代码示例 4-1 用 Log4j2 代替 Logback
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
4.2.2 起步依赖的实现原理
如果熟悉 Maven,那么相信大家已经猜到了,起步依赖背后使用的其实就是 Maven 的传递依赖机制 。看似只添加了一个依赖,但实际上通过传递依赖,我们已经引入了一堆的依赖。
我们可以在 Maven 的 <dependencyManagement/>
中统一定义依赖的信息,比如版本、排除的传递依赖项等,随后在 <dependencies/>
中添加这个依赖时就不用再重复配置这些信息了。起步依赖与其中定义的依赖项都是通过这种方式定义的,所以使用了起步依赖后就不用再考虑版本和应该排除哪些东西了。
以 2.3.0.RELEASE 版本的 org.springframework.boot:spring-boot-starter-web
为例,它的 POM 文件分为如下三部分:
- 起步依赖本身的描述信息
- 导入依赖管理项
- 具体依赖项
在 <dependencyManagement/>
中用 import
的方式导入 org.springframework.boot:spring-boot-dependencies
里配置的依赖信息,具体如下所示:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
随后,在 <dependencies/>
中配置具体要引入的依赖,而这些依赖所间接依赖的内容也会被传递进来,具体如下所示:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
而到了后面的版本,Spring Boot Starter 的内容变得更直接了,以 2.6.3 版本的 org.springframework.boot:spring-boot-starter-web
为例,其中去掉了 <dependencyManagement/>
的部分,所有依赖的版本信息直接硬编码写死在了 <dependencies/>
里。这两种方式对使用 Spring Boot 的开发者而言,在使用体验上并没有什么差异,所以我们不用在意这些细节。
4.3 自动配置
Spring Boot 可以根据 CLASSPATH、配置项等条件自动进行常规配置,省去了我们自己动手把一模一样的配置复制来复制去的麻烦。既然框架能猜到你想这么配,那它自己就能帮你搞定,如果它的配置不是我们想要的,再做些手动配置就好了。
我们已经在代码示例 1-1 中看到过 @SpringBootApplication
注解了,查看这个注解,可以发现它上面添加了 @EnableAutoConfiguration
,它可以开启自动配置功能。这两个注解上都有 exclude
属性,我们可以在其中排除一些不想启用的自动配置类。如果不想启用自动配置功能,也可以在配置文件中配置 spring.boot.enableautoconfiguration=false
,关闭该功能。
4.3.1 自动配置的实现原理
自动配置类其实就是添加了 @Configuration
的普通 Java 配置类,它利用 Spring Framework 4.0 加入的条件注解 @Conditional
来实现“根据特定条件启用相关配置类”,注解中传入的 Condition
类就是不同条件的判断逻辑。Spring Boot 内置了很多条件注解,表 4-2 中列举了 org.springframework.boot.autoconfigure.condition
包中的条件注解。
表 4-2 Spring Boot 内置的条件注解
条件注解 | 生效条件 |
---|---|
@ConditionalOnBean | 存在特定名称、特定类型、特定泛型参数或带有特定注解的 Bean |
@ConditionalOnMissingBean | 与前者相反,不存在特定 Bean |
@ConditionalOnClass | 存在特定的类 |
@ConditionalOnMissingClass | 与前者相反,不存在特定类 |
@ConditionalOnCloudPlatform | 运行在特定的云平台上,截至 2.6.3 版本,代表云平台的枚举类支持无云平台、CloudFoundry、Heroku、SAP、Kubernetes 和 Azure,可以通过 spring.main.cloud-platform 配置强制使用的云平台 |
@ConditionalOnExpression | 指定的 SpEL 表达式为真 |
@ConditionalOnJava | 运行在满足条件的 Java 上,可以比指定版本新,也可以比指定版本旧 |
@ConditionalOnJndi | 指定的 JNDI 位置必须存在一个,如没有指定,则需要存在 InitialContext |
@ConditionalOnProperty | 属性值满足特定条件,比如给定的属性值都不能为 false |
@ConditionalOnResource | 存在特定资源 |
@ConditionalOnSingleCandidate | 当前上下文中,特定类型的 Bean 有且仅有一个 |
@ConditionalOnWarDeployment | 应用程序是通过传统的 War 方式部署的,而非内嵌容器 |
@ConditionalOnWebApplication | 应用程序是一个 Web 应用程序 |
@ConditionalOnNotWebApplication | 与前者相反,应用程序不是一个 Web 应用程序 |
以 @ConditionalOnClass
注解为例,它的定义如下所示, @Target
指明该注解可用于类型和方法定义, @Rentention
指明注解的信息在运行时也能获取到,而其中最关键的就是 OnClassCondition
条件类,里面是具体的条件计算逻辑:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
了解了条件注解后,再来看看它们是如何与配置类结合使用的。以后续章节中会用到的 JdbcTemplateAutoConfiguration
为例,它的完整类定义代码如下所示:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {}
可以看到这个配置类的生效条件是存在 DataSource
和 JdbcTemplate
类,且在上下文中只能有一个 DataSource
。此外,这个自动配置需要在 DataSourceAutoConfiguration
之后再配置(可以用 @AutoConfigureBefore
、 @AutoConfigureAfter
和 @AutoConfigureOrder
来控制自动配置的顺序)。这个配置类还会同时导入 JdbcTemplateConfiguration
和 NamedParameterJdbcTemplateConfiguration
里的配置。
茶歇时间:通过
ImportSelector
选择性导入配置普通的配置类需要被扫描到才能生效,可是自动配置类并不在我们项目的扫描路径中,它们又是怎么被加载上来的呢?
秘密在于
@EnableAutoConfiguration
上的@Import(AutoConfigurationImportSelector.class)
,其中的AutoConfigurationImportSelector
类是ImportSelector
的实现,这个接口的作用就是根据特定条件决定可以导入哪些配置类,接口中的selectImports()
方法返回的就是可以导入的配置类名。
AutoConfigurationImportSelector
通过SpringFactoriesLoader
来加载/META-INF/spring.factories
里配置的自动配置类列表,所用的键是org.springframework.boot.autoconfigure.EnableAutoConfiguration
,值是以逗号分隔的自动配置类全限定类名(包含了完整包名与类名)清单。所以,只要在我们的类上增加
@SpringBootApplication
或者@EnableAutoConfiguration
后,Spring Boot 就会自动替我们加载所有的自动配置类。
自动配置固然帮我们做了很多事,降低了配置的复杂度,但总有些情况我们会想要强制禁用某些自动配置,这时就需要做以下处理:
- 在配置文件中使用
spring.autoconfigure.exclude
配置项,它的值是要排除的自动配置类的全限定类名; - 在
@SpringBootApplication
注解中添加exclude
配置,它的值是要排除的自动配置类。
4.3.2 配置项加载机制详解
如果自动配置的东西不满足我们的需要,我们可以自己动手进行配置,但在动手之前,可以先了解下 Spring Boot 的自动配置是否有给我们留下什么“开关参数”,用来定制配置内容。以 AOP 的配置为例,它就可以通过 spring.aop.proxy-target-class
属性来做微调:
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
static class JdkDynamicAutoProxyConfiguration {}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {}
在 2.3.1 节中,我们了解过了 Spring Framework 的 PropertySource
抽象机制,Spring Boot 将它又向前推进了一大步。
-
Spring Boot 的属性加载优先级
Spring Boot 有 18 种方式来加载属性,且存在覆盖关系,本节根据优先级列出其中的一部分:
(1) 测试类上的
@TestPropertySource
注解;(2) 测试类上的
@SpringBootTest
注解中的properties
属性,还有些其他@...Test
注解也有该属性;(3) 命令行参数(在 5.3 节中会讨论如何获取命令行参数);
(4)
java:comp/env
中的 JNDI 属性;(5)
System.getProperties()
中获取到的系统属性;(6) 操作系统环境变量;
(7)
RandomValuePropertySource
提供的random.*
属性(比如$
、$
、$
和$
);(8) 应用配置文件(有好几个地方可以配置,下面会详细说明);
(9) 配置类上的
@PropertySource
注解。如果存在同名的属性,越往前的位置优先级越高,例如
my.prop
出现在命令行上,又出现在配置文件里,那最终会使用命令行里的值。 -
Spring Boot 的配置文件
Spring Boot 还为我们提供了一套配置文件,默认以
application
作为主文件名,支持 Properties 格式(文件以.properties
结尾)和 YAML 格式(文件以.yml
结尾)。Spring Boot 会按如下优先级加载属性(以.properties
文件为例,.yml
文件的顺序是一样的):(1) 打包后的 Jar 包以外的
application-.properties
;(2) 打包后的 Jar 包以外的
application.properties
;(3) Jar 包内部的
application-.properties
;(4) Jar 包内部的
application.properties
。可以看到 Jar 包外部的文件比内部的优先级高,特定 Profile 的文件比公共的文件优先级高。
在 Spring Boot 2.4.0 之前,上述第 2 和第 3 个文件的优先级顺序是反的,所有 application-.properties 文件的顺序都要高于 application.properties,无论是否在 Jar 包外。从 2.4.0 开始,调整为 Jar 包外部的文件优先级更高。可以设置
spring.config.use-legacy-processing=true
来开启兼容逻辑,Spring Boot 3.0 里会移除这个开关。Spring Boot 会在如下几处位置寻找
application.properties
文件,并将其中的内容添加到 Spring 的Environment
中:- 当前目录的
/config
子目录; - 当前目录;
- CLASSPATH 中的
/config
目录; - CLASSPATH 根目录。
- 当前目录的
如果我们不想用 application
来做主文件名,可以通过 spring.config.name
来改变默认值。通过下面的方式可以将 application.properties
改为 spring.properties
:
▸ java -jar foo.jar --spring.config.name=spring
还可以通过 spring.config.location
来修改查找配置文件的路径,默认是下面这样的,用逗号分隔,越靠后的优先级越高:
classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/
如果同时存在 .propertries
文件和 .yml
文件,那么后者中配置的属性优先级更高。
-
类型安全的配置属性
通常,我们会在类中用
@Value("${}")
注解来访问属性,或者在 XML 文件中使用${}
占位符。在配置中,可能会有大量的属性需要一一对应到类的成员变量上,Spring Boot 提供了一种结构化且类型安全的方式来处理配置属性(configuration properties)——使用@ConfigurationProperties
注解。下面的代码是
DataSourceProperties
类的一部分,这是一个典型的配置属性类(当然,它也是一个 POJO),Spring Boot 会把环境中以spring.datasource
打头的属性都绑定到类的成员变量上,并且完成对应的类型转换。例如,spring.datasource.url
就会绑定到url
上。@ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean { private ClassLoader classLoader; private String name; private boolean generateUniqueName = true; private Class<? extends DataSource> type; private String driverClassName; private String url; // 以下省略 }
如果为类加上
@ConstructorBinding
注解,还可以通过构造方法完成绑定,不过这种做法相对而言并不常用。ConfigurationPropertiesAutoConfiguration
自动配置类添加了@EnableConfigurationProperties
注解,开启了对@ConfigurationProperties
的支持。我们可以通过添加@EnableConfigurationProperties (DataSourceProperties.class)
注解这样的方式将绑定后的DataSourceProperties
注册为 Bean,此时的 Bean 名称为“属性前缀 - 配置类的全限定类名”,例如spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
;也可以直接用@Component
注解或其他标准 Bean 配置方式将其注册为 Bean,以供其他 Bean 注入使用。除了添加到类上,
@ConfigurationProperties
注解也可以被加到带有@Bean
注解的方法上,这样就能为方法返回的 Bean 对象绑定上下文中的属性了。Spring Boot 在绑定属性时非常灵活,几乎可以说怎么写都能绑上,它一共支持四种属性命名形式:
- 短横线分隔,推荐的写法,比如
spring.datasource.driver-class-name
; - 驼峰式,比如
spring.datasource.driverClassName
; - 下划线分隔,比如
spring.datasource.driver_class_name
; - 全大写且用下划线分隔,比如
SPRING_DATASOURCE_DRIVERCLASSNAME
。
- 短横线分隔,推荐的写法,比如
前三种形式多用于 .properties
文件、 .yml
文件和 Java 系统属性的配置方式,第四种则更多出现在系统的环境变量中。而 @ConfigurationProperties
中的 prefix
属性只能使用第一种形式。
4.4 编写我们自己的自动配置与起步依赖
既然 Spring Boot 为我们提供了这么灵活强大的自动配置与起步依赖功能,那我们是否也可以参考其实现原理,实现专属于自己的自动配置与起步依赖呢?答案是肯定的。不仅如此,我们还可以对实现稍作修改,让它适用于非 Spring Boot 环境,甚至是低版本的 Spring Framework 环境。
4.4.1 编写自己的自动配置
根据 4.3.1 节的描述,我们很容易就能想到,要编写自己的自动配置,只需要以下三个步骤:
(1) 编写常规的配置类;
(2) 为配置类增加生效条件与顺序;
(3) 在 /META-INF/spring.factories
文件中添加自动配置类。
从第 4 章的例子开始,我们将正式开始开发二进制奶茶店的代码。作为贯穿专栏的案例,它几乎会串联起所有的重要知识点,方便大家理解并加深印象。
需求描述 二进制奶茶店新店开张,有很多准备工作要做,因此在尚未做好对外营业的准备时,不能开门迎客。现在,我们需要将具体的准备情况和每天的营业时间信息找个地方统一管理起来,以便合理安排门店的营业工作。
在 Spring Initializr 中,选择新建一个 Spring Boot 2.6.3 版本的 Maven 工程,具体信息如表 4-3 所示。点击生成按钮后,就能获得一个 binarytea.zip
压缩文件,这个文件被解压后即是原始工程。我们将这个工程放在 ch4/binarytea 目录中。
表 4-3 BinaryTea 的项目信息
条目 | 内容 |
---|---|
项目 | Maven Project |
语言 | Java |
Spring Boot 版本 | 2.6.3 |
Group | learning.spring |
Artifact | binarytea |
名称 | BinaryTea |
Java 包名 | learning.spring.binarytea |
打包方式 | Jar |
Java 版本 | 11 |
-
编写配置类并指定条件
考虑到在 Spring Boot 项目里可以很方便地从配置文件里加载配置,我们可以把具体的准备情况和每天的营业时间都放在配置文件里,通过对应配置项来控制程序的运行逻辑。
编写一个简单的
ShopConfiguration
类,上面增加了@Cofiguration
注解,表示这是一个配置类。这个配置类生效的条件是binarytea.ready
属性的值为true
,也就是店铺已经准备就绪了,除此之外的值或者不存在该属性时ShopConfiguration
都不会生效。ShopConfiguration
类的代码大致如代码示例 4-2 所示。代码示例 4-2
ShopConfiguration
自动配置类定义package learning.spring.config; // 省略import部分 @Configuration @EnableConfigurationProperties(BinaryTeaProperties.class) @ConditionalOnProperty(name = "binarytea.ready", havingValue = "true") public class ShopConfiguration { }
它的作用是创建一个
BinaryTeaProperties
的 Bean,并将就绪状态和营业时间绑定到 Bean 的成员上。BinaryTeaProperties
的代码大致如代码示例 4-3 所示。代码示例 4-3
BinaryTeaProperties
代码片段@ConfigurationProperties("binarytea") public class BinaryTeaProperties { private boolean ready; private String openHours; // 省略Getter和Setter }
以
binarytea
打头的属性值会被绑定到BinaryTeaProperties
的ready
和openHours
成员上。例如,application.properties
文件包含如下内容,它们就会被绑定上去:binarytea.ready=true binarytea.open-hours=8:30-22:00
-
配置 spring.factories 文件
为了让 Spring Boot 能找到我们写的这个配置类,我们需要在工程的
src/resources
目录中创建META-INF/spring.factories
文件,其内容如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=learning.spring.config.ShopConfiguration
由于工程生成的包名是
learning.spring.binarytea
,所以默认会扫描这个包下的类。出于演示的目的,我们不希望 Spring Boot 工程自动扫描到ShopConfiguration
类,所以特意将它放在learning.spring.config
包中。Spring Boot 的自动配置机制会通过spring.factories
文件里的配置,找到我们的ShopConfiguration
类。 -
测试
要测试我们的自动配置是否生效,只要看 Spring 上下文中是否存在
BinaryTeaProperties
类型的 Bean。@SpringBootTest
注解提供了基本的 Spring Boot 工程测试能力,classes
属性的值是该测试类依赖的配置类,properties
属性中以键值对的形式提供了属性配置,代替了在测试文件夹中提供的application.properties
,我们还可以根据测试需要调整属性值。如果店铺已经准备好开门营业了,规定每天的营业时间是早 8 点 30 分至晚 10 点,检查整个自动配置功能是否符合预期的测试代码应该如代码示例 4-4 所示。
代码示例 4-4
ShopConfigurationEnableTest
测试类package learning.spring.config; // 省略import部分 @SpringBootTest(classes = BinaryTeaApplication.class, properties = { "binarytea.ready=true", "binarytea.open-hours=8:30-22:00" }) public class ShopConfigurationEnableTest { @Autowired private ApplicationContext applicationContext; @Test void testPropertiesBeanAvailable() { assertNotNull(applicationContext.getBean(BinaryTeaProperties.class)); assertTrue(applicationContext .containsBean("binarytea-learning.spring.binarytea.BinaryTeaProperties")); } @Test void testPropertyValues() { BinaryTeaProperties properties = applicationContext.getBean(BinaryTeaProperties.class); assertTrue(properties.isReady()); assertEquals("8:30-22:00", properties.getOpenHours()); } }
其中,
testPropertiesBeanAvailable()
方法的作用是检查 Spring 上下文中是否存在BinaryTeaProperties
类型的 Bean,Bean 的名字是否如 4.3.2 节所描述的那样;testPropertyValues()
方法的作用是检查属性内容是否被正确绑定到成员变量中。如果店铺还没准备好,那么自动配置类不应该生效。我们可以通过
ShopConfigurationDisableTest
类来测试,其中会检查binarytea.ready
属性值,并确认上下文中不存在BinaryTeaProperties
类型的 Bean,具体代码如代码示例 4-5 所示。代码示例 4-5
ShopConfigurationDisableTest
测试类代码片段@SpringBootTest(classes = BinaryTeaApplication.class, properties = { "binarytea.ready=false" }) public class ShopConfigurationDisableTest { @Autowired private ApplicationContext applicationContext; @Test void testPropertiesBeanUnavailable() { assertEquals("false", applicationContext.getEnvironment().getProperty("binarytea.ready")); assertFalse(applicationContext.containsBean("binarytea-learning.spring.binarytea.BinaryTeaProperties")); } }
算上生成工程时自动生成的
BinaryTeaApplicationTests
类中的contextLoads()
测试方法,通过mvn test
命令执行测试后,如果测试全部成功,我们可以看到类似下面的结果:[INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
4.4.2 脱离 Spring Boot 实现自动配置
上一节中,我们依赖 Spring Boot 提供的一些能力,实现了一个自动配置类。可是,如果没有 Spring Boot,又该怎么办?甚至出于某些原因,我们要在 Spring Framework 4. x 或者更低的版本上也做些自动配置,又该怎么办呢?
这里需要解决两个问题:
- 如何找到配置类;
- 如何实现配置类上的条件。
第一个问题相对容易解决,只需要根据当前工程的情况进行调整,让我们的配置类位于工程会扫描的包里(比如 @ComponentScan
配置的扫描目标里),或者把我们的配置类追加到工程的扫描范围里。第二个问题则要复杂一些,如果我们用的是 4. x 版本的 Spring Framework,那么它本身就有 @Conditional
注解,我们完全可以按照 Spring Boot 中那些条件注解的实现,按需复制过来;如果用的是 3. x 版本的 Spring Framework,就只能通过自定义 BeanFactoryPostProcessor
,根据一定的条件,再决定是否用编程的方式注册 Bean。
假设我们根据配置来决定 HelloWorld
程序输出的语言种类,如代码示例 4-6,在 learning.spring.speaker
包中定义接口与类。
代码示例 4-6 Speaker
接口及其实现代码片段
public interface Speaker {
String speak();
}
public class ChineseSpeaker implements Speaker {
@Override
public String speak() {
return "你好,我爱Spring。";
}
}
public class EnglishSpeaker implements Speaker {
@Override
public String speak() {
return "Hello, I love Spring.";
}
}
-
定义配置类并实现条件判断
如果工程的 Spring Bean 扫描路径是
learning.spring.helloworld
,那就在这个包下放一个配置类,具体如代码示例 4-7 所示,其中创建了SpeakerBeanFactoryPostProcessor
,同时也让容器加载了application.properties
文件 。代码示例 4-7 用于添加
BeanFactoryPostProcessor
的配置类代码片段@Configuration @PropertySource("classpath:/application.properties") public class AutoConfiguration { @Bean public static SpeakerBeanFactoryPostProcessor speakerBeanFactoryPostProcessor() { return new SpeakerBeanFactoryPostProcessor(); } }
SpeakerBeanFactoryPostProcessor
的工作比较多,有如下几种。- 根据
spring.speaker.enable
开关确定是否自动配置speaker
Bean。 - 根据
spring.speaker.language
动态确定使用哪个Speaker
接口的实现。 - 将确定的
Speaker
实现注册到 Spring 上下文中。
- 根据
具体获取属性进行判断和注册的代码如代码示例 4-8 所示。
代码示例 4-8 SpeakerBeanFactoryPostProcessor
处理逻辑
public class SpeakerBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
private static final Log log = LogFactory.getLog(SpeakerBeanFactoryPostProcessor.class);
// 为了获得配置属性,注入Environment
private Environment environment;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 获取属性值
String enable = environment.getProperty("spring.speaker.enable");
String language = environment.getProperty("spring.speaker.language", "English");
String clazz = "learning.spring.speaker." + language + "Speaker";
// 开关为true则注册Bean,否则结束
if (!"true".equalsIgnoreCase(enable)) {
return;
}
// 如果目标类不存在,结束处理
if (!ClassUtils.isPresent(clazz, SpeakerBeanFactoryPostProcessor.class.getClassLoader())) {
return;
}
if (beanFactory instanceof BeanDefinitionRegistry) {
registerBeanDefinition((BeanDefinitionRegistry) beanFactory, clazz);
} else {
registerBean(beanFactory, clazz);
}
}
// 省略其他代码
}
代码示例 4-8 的 postProcessBeanFactory()
方法最后,根据 BeanFactory
的类型选择了不同的 Bean 注册方式,实际情况中会更倾向于使用 BeanDefinitionRegistry
来注册 Bean 定义。两种不同的注册方法如代码示例 4-9 所示。
代码示例 4-9 SpeakerBeanFactoryPostProcessor
中的 Bean 注册逻辑
public class SpeakerBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
// 如果是BeanDefinitionRegistry,可以注册BeanDefinition
private void registerBeanDefinition(BeanDefinitionRegistry beanFactory, String clazz) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClassName(clazz);
beanFactory.registerBeanDefinition("speaker", beanDefinition);
}
// 如果只能识别成ConfigurableListableBeanFactory,直接注册一个Bean实例
private void registerBean(ConfigurableListableBeanFactory beanFactory, String clazz) {
try {
Speaker speaker = (Speaker) ClassUtils.forName(clazz, SpeakerBeanFactoryPostProcessor.class.
getClassLoader()).getDeclaredConstructor().newInstance();
beanFactory.registerSingleton("speaker", speaker);
} catch (Exception e) {
log.error("Can not create Speaker.", e);
}
}
// 省略其他代码
}
-
运行与测试
为了简化演示工程,假设
learning.spring.helloworld
中是需要运行的代码,我们可以直接在main()
方法中创建 Spring 上下文,具体如代码示例 4-10 所示。代码示例 4-10
Application
类定义@Configuration @ComponentScan("learning.spring.helloworld") public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class); Speaker speaker = applicationContext.getBean("speaker", Speaker.class); System.out.println(speaker.speak()); } }
配套的
application.properties
文件仅包含两个配置,具体如下:spring.speaker.enable=true spring.speaker.language=Chinese
程序的运行效果就是输出一句中文:
你好,我爱Spring。
如果将
spring.speaker.language
删除,或者改为English
,输出则为英文:Hello, I love Spring.
与之前一样,我们也提供了基本的测试代码。由于配置内容不同,不同的组合需要我们编写不同的测试类,比如,和上述的运行一样,要求正常输出中文,可以使用代码示例 4-11 的测试代码。
代码示例 4-11
ChineseAutoConfigurationTest
中文测试类@SpringJUnitConfig(AutoConfiguration.class) @TestPropertySource(properties = { "spring.speaker.enable=true", "spring.speaker.language=Chinese" }) public class ChineseAutoConfigurationTest { @Autowired private ApplicationContext applicationContext; @Test void testHasChineseSpeaker() { assertTrue(applicationContext.containsBean("speaker")); Speaker speaker = applicationContext.getBean("speaker", Speaker.class); assertEquals(ChineseSpeaker.class, speaker.getClass()); } }
@SpringJUnitConfig
注解是 Spring 的测试框架提供的组合注解,可以代替之前看到过的@ExtendWith(SpringExtension.class)
,同时配置一些 Spring 相关的内容。@TestPropertySource
注解可以指定属性文件的位置,也可以直接提供属性键值对。如果要关闭开关,则可以像代码示例 4-12 那样。
代码示例 4-12
DisableAutoConfigurationTest
关闭开关的测试@SpringJUnitConfig(AutoConfiguration.class) @TestPropertySource(properties = {"spring.speaker.enable=false"}) public class DisableAutoConfigurationTest { @Autowired private ApplicationContext applicationContext; @Test void testHasNoSpeaker() { assertFalse(applicationContext.containsBean("speaker")); } }
如果
spring.speaker.language
的值我们不支持,那只需要调整@TestPropertySource
中提供的属性值,其他测试代码和断言逻辑与DisableAutoConfigurationTest
完全一样,具体如代码示例 4-13 所示。代码示例 4-13
WrongAutoConfigurationTest
错误语言的测试@SpringJUnitConfig(AutoConfiguration.class) @TestPropertySource(properties = { "spring.speaker.enable=true", "spring.speaker.language=Japanese" }) public class WrongAutoConfigurationTest { // 具体代码省略,同DisableAutoConfigurationTest }
在 IDEA 中运行所有这些测试类的效果如图 4-1 所示。
图 4-1 IDEA 中的测试运行效果
4.4.3 编写自己的起步依赖
在通常情况下,起步依赖主要由两部分内容组成:
(1) 所需要管理的依赖项;
(2) 对应功能的自动配置。
-
依赖项该如何管理
如 4.2.2 节所述,Spring Boot 的起步依赖本身就是一个 Maven 模块,所以将要管理的依赖项直接放在它的 pom.xml 中即可,即放在
<dependencies/>
中。对于自动配置,我们一般都会单独编写一个模块,把相关自动配置类和spring.factories
等文件放在一起。前者就是起步依赖自身,即starter
模块,后者就是autoconfigure
模块。由于 Spring Boot 官方的起步依赖都使用
spring-boot
开头,因而我们的自定义起步依赖需要 避免 使用这个前缀。大家可以根据要实现的功能,或者要引入的内容来设计前缀,后缀使用-spring-boot-starter
,例如binarytea-spring-boot-starter
。 -
自动配置模块该如何定义
在
autoconfigure
模块中一般还会包含所需的属性配置,通常是带有@ConfigurationProperties
的类,这些属性的前缀最好和starter
中的保持一致,不要和 Spring Boot 内置的自动配置使用的spring
、server
等前缀混在一起。而autoconfigure
模块的命名后缀可以使用-spring-boot-autoconfigure
,例如binarytea-spring-boot-autoconfigure
。在准备完自动配置模块
binarytea-spring-boot-autoconfigure
后,务必将它也放到binaryteaspring-boot-starter
的<dependencies/>
中去,这样就能和其他起步依赖一样,在使用时只需引入starter
模块就可以了。当然,如果两者的内容都十分简单,也可以将它们合并到一起,直接放到starter
中。顺便再强调一下,起步依赖本身就是一个普通的 Maven 模块,因此无论是否用在 Spring Boot 工程里,它的实现和功能都不会有太大差异。
4.5 小结
本章我们了解了为何在 Spring Framework 已经成为事实行业标准的情况下,Spring 团队仍然孕育出了 Spring Boot 这么一个炙手可热的项目,大有“不用 Spring Boot 就不算开发 Spring 项目”的意思。我们也一同学习了 Spring Boot 中使用最广泛的两个功能——起步依赖与自动配置,了解了它们的基本使用和二者背后的实现原理。在此过程之中,对于 Spring Boot 是如何加载配置项的,它的加载位置与优先级顺序等一系列问题,我们都做了简单的说明。
章节的最后,大家一起动手解决了几个问题,即如何编写自己的起步依赖与自动配置,还把问题延展了一下:如果没有 Spring Boot 又该如何实现呢?
下一章,我们会进一步展开说明 Spring Boot 中与生产运行相关的功能,并学习 Spring Boot Actuator、监控与部署相关的一些话题。
二进制奶茶店项目开发小结
本章我们正式开始搭建二进制奶茶店的示例工程,主要做了两件事:
(1) 通过 Spring Initializr 初始化了二进制奶茶店的 BinaryTea 工程,完成了项目骨架的搭建;
(2) 编写了一个读取是否开门营业和营业时间配置的自动配置类。
这只是整个示例的“第一步”,后续章节中的演示都会基于本章的工程展开。