使用Spring Boot构建多维度配置层

03 多维配置:如何使用 Spring Boot 中的配置体系?

配置体系是基于 Spring Boot 框架开发应用程序的基础,而自动配置也是该框架的核心功能之一。今天我将带领大家梳理使用 Spring Boot 配置体系的系统方法。我们先从创建和运行第一个 Web 应用程序开始吧。

创建第一个 Spring Boot Web 应用程序

基于 Spring Boot 创建 Web 应用程序的方法有很多,但最简单、最直接的方法是使用 Spring 官方提供的 Spring Initializer 初始化模板。

初始化使用操作:直接访问 Spring Initializer 网站(http://start.spring.io/),选择创建一个 Maven 项目并指定相应的 Group 和 Artifact,然后在添加的依赖中选择 Spring Web,点击生成即可。界面效果下图所示:

使用 Spring Initializer 创建 Web 应用程序示意图

当然,对于有一定开发经验的同学而言,我们完全可以基于 Maven 本身的功能特性和结构,来生成上图中的代码工程。

接下来,我们参考 02 讲中关于 Controller 的创建基本方法,来为这个代码工程添加一些支持 RESTful 风格的 HTTP 端点,在这里我们同样创建一个 CustomerController 类,如下所示:

java 复制代码
@RestController
@RequestMapping(value="customers")
public class CustomerController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
     public CustomerTicket getCustomerTicketById(@PathVariable Long id) {        
        CustomerTicket customerTicket = new CustomerTicket();
        customerTicket.setId(1L);
        customerTicket.setAccountId(100L);
        customerTicket.setOrderNumber("Order00001");
        customerTicket.setDescription("DemoOrder");
        customerTicket.setCreateTime(new Date());

         return customerTicket;
    }
}

请注意,这里是为了演示方便,我们才使用了硬编码完成了一个 HTTP GET 请求的响应处理。

现在 RESTful 端点已经开发完成,我们需要对这个应用程序进行打包。基于 Spring Boot 和 Maven,当我们使用 mvn package 命令构建整个应用程序时,将得到一个 customerservice-0.0.1-SNAPSHOT.jar 文件,而这个 jar 文件就是可以直接运行的可执行文件,内置了 Tomcat Web 服务器。也就是说,我们可以通过如下命令直接运行这个 Spring Boot 应用程序:

bash 复制代码
java --jar customerservice-0.0.1-SNAPSHOT.jar

那么,如何验证服务是否启动成功,以及 HTTP 请求是否得到正确响应呢?在 03 讲中,我们引入 Postman 来演示如何通过 HTTP 协议暴露的端点进行远程服务访问。

Postman 提供了强大的 Web API 和 HTTP 请求调试功能,界面简洁明晰,操作也比较方便快捷和人性化。Postman 能够发送任何类型的 HTTP 请求(如 GET、HEAD、POST、PUT 等),并能附带任何数量的参数和 HTTP 请求头(Header)。

这时我们通过 Postman 访问"http://localhost:8083/customers/1"端点,可以得到如下图所示的HTTP响应结果,说明整个服务已经启动成功。

好了,现在我们已经明白如何构建、打包以及运行一个简单的 Web 应用程序了,这是一切开发工作的起点,后续所有的案例代码我们都将通过这种方式展现在你面前,包括接下来要介绍的 Spring Boot 配置体系也是一样。

Spring Boot 中的配置体系

在 Spring Boot 中,其核心设计理念是对配置信息的管理采用约定优于配置。在这一理念下,则意味着开发人员所需要设置的配置信息数量比使用传统 Spring 框架时还大大减少。当然,今天我们关注的主要是如何理解并使用 Spring Boot 中的配置信息组织方式,这里就需要引出一个核心的概念,即 Profile。

配置文件与 Profile

Profile 本质上代表一种用于组织配置信息的维度,在不同场景下可以代表不同的含义。例如,如果 Profile 代表的是一种状态,我们可以使用 open、halfopen、close 等值来分别代表全开、半开和关闭等。再比如系统需要设置一系列的模板,每个模板中保存着一系列配置项,那么也可以针对这些模板分别创建 Profile。这里的状态或模版的定义完全由开发人员自主设计,我们可以根据需要自定义各种 Profile,这就是 Profile 的基本含义。

另一方面,为了达到集中化管理的目的,Spring Boot 对配置文件的命名也做了一定的约定,分别使用 label 和 profile 概念来指定配置信息的版本以及运行环境,其中 label 表示配置版本控制信息,而 profile 则用来指定该配置文件所对应的环境。在 Spring Boot 中,配置文件同时支持 .properties 和 .yml 两种文件格式,结合 label 和 profile 概念,如下所示的配置文件命名都是常见和合法的:

复制代码
/{application}.yml
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

Yaml 的语法和其他高级语言类似,并且可以非常直观地表达各种列表、清单、标量等数据形态,特别适合用来表达或编辑数据结构和各种配置文件。在这里,我们指定了如下所示的数据源配置,这里使用了 . yml 文件,如下所示:

yaml 复制代码
spring: 
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/account
    username: root
	password: root

如果采用 .propertie 配置文件,那么上述配置信息将表示为如下的形式:

复制代码
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/account
spring.datasource.username=root 
spring.datasource.password=root

显然,类似这样的数据源通常会根据环境的不同而存在很多套配置。假设我们存在如下所示的配置文件集合:

多配置文件示意图

注意,这里有一个全局的 application.yml 配置文件以及多个局部的 profile 配置文件。那么,如何指定当前所使用的那一套配置信息呢?在 Spring Boot 中,我们可以在主 application.properties 中使用如下的配置方式来激活当前所使用的 Profile:

ini 复制代码
spring.profiles.active = test

上述配置项意味着系统当前会读取 application-test.yml 配置文件中的配置内容。同样,如果使用 .yml 文件,则可以使用如下所示的配置方法:

yaml 复制代码
spring:
  profiles:
    active: test

事实上,我们也可以同时激活几个 Profile,这完全取决于你对系统配置的需求和维度:

makefile 复制代码
spring.profiles.active: prod, myprofile1, myprofile2

当然,如果你想把所有的 Profile 配置信息只保存在一个文件中而不是分散在多个配置文件中, Spring Boot 也是支持的,需要做的事情只是对这些信息按 Profile 进行组织、分段,如下所示:

yaml 复制代码
spring: 
         profiles: test
         #test 环境相关配置信息
spring: 
         profiles: prod
         #prod 环境相关配置信息

尽管上述方法是有效的,但在 03 讲中,还是推荐你按多个配置文件的组织方法管理各个 Profile 配置信息,这样才不容易混淆和出错。

最后,如果我们不希望在全局配置文件中指定所需要激活的 Profile,而是想把这个过程延迟到运行这个服务时,那么我们可以直接在 java --jar 命令中添加"--spring.profiles.active"参数,如下所示:

bash 复制代码
java --jar customerservice-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

这种实现方案在通过脚本进行自动化打包和部署的场景下非常有用。

代码控制与Profile

在 Spring Boot 中,Profile 这一概念的应用场景还包括动态控制代码执行流程。为此,我们需要使用 @Profile 注解,先来看一个简单的示例。

java 复制代码
@Configuration
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        //创建 dev 环境下的 DataSource 
    }

    @Bean()
    @Profile("prod")
    public DataSource prodDataSource(){
        //创建 prod 环境下的 DataSource 
    }
}

可以看到,我们构建了一个 DataSourceConfig 配置类来专门管理各个环境所需的 DataSource。注意到这里使用 @Profile 注解来指定具体所需要执行的 DataSource 创建代码,通过这种方式,可以达到与使用配置文件相同的效果。

更进一步,能够在代码中控制 JavaBean 的创建过程为我们根据各种条件动态执行代码流程提供了更大的可能性。例如,在日常开发过程中,一个常见的需求是根据不同的运行环境初始化数据,常见的做法是独立执行一段代码或脚本。基于 @Profile 注解,我们就可以将这一过程包含在代码中并做到自动化,如下所示:

java 复制代码
@Profile("dev")
@Configuration
public class DevDataInitConfig {

	@Bean
	public CommandLineRunner dataInit() { 
    	return new CommandLineRunner() {
      		@Override
      		public void run(String... args) throws Exception {
        	//执行 Dev 环境的数据初始化
    		};  
}

这里用到了 Spring Boot 所提供了启动时任务接口 CommandLineRunner,实现了该接口的代码会在 Spring Boot 应用程序启动时自动进行执行,我们会在后续的课程中看到这个接口的具体使用方法。

@Profile 注解的应用范围很广,我们可以将它添加到包含 @Configuration 和 @Component 注解的类及其方法,也就是说可以延伸到继承了 @Component 注解的 @Service、@Controller、@Repository 等各种注解中。

常见配置场景和内容

在今天课程的最后,我们给出几个常见的配置示例来帮助你进一步加深对 Spring Boot 中配置体系的理解。

对于一个 Web 应用程序而言,最常见的配置可能就是指定服务暴露的端口地址,如下所示:

yaml 复制代码
server:
    port: 8080

同时,数据库访问也是 Web 应用程序的基本功能,因此,关于数据源的设置也是常见的一种配置场景,我们在 02 讲中创建第一个 Spring Boot Web 应用程序时给出了一个基本的示例。这里再以 JPA 为例,给出如下所示的一种配置方案:

yaml 复制代码
spring:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

显然,这里使用了 Hibernate 作为 JPA 规范的实现框架,并设置了 show-sql 等相关属性。然后,开发人员一般也需要设置日志级别和对象,如下所示的就是一个典型的配置示例:

ini 复制代码
logging.level.root=WARN
logging.level.com.springcss.customer=INFO

我们设置了系统的全局日志级别为 WARN,而针对自定义的 com.springcss.customer 包下的日志则将其级别调整到 INFO。

随时本课程内容的展开,这些常见的配置信息都会在我们的 SpringCSS 案例分析中得到展示。这里需要注意的是,Spring Boot 基于 application.properties 或 application.yml 全局配置文件已经自动内置了很多默认配置。即使我们不设置上述配置内容,Spring Boot 仍然可以基于这些默认配置完成系统的初始化。自动配置是 Spring Boot 中的一个核心概念,我们会在后续内容中给出详细的实现原理分析。

小结与预告

配置体系是学习 Spring Boot 应用程序的基础。在今天的课程中,我们系统梳理了 Spring Boot 中的 Profile 概念,以及如何通过配置文件和代码控制的方式来使用这一核心概念。

04 定制配置:如何创建和管理自定义的配置信息?

03 讲中,我们介绍了 Spring Boot 中的配置体系,梳理了配置文件的组织结构以及如何通过代码实现动态的配置信息加载过程。今天我们将在 03 讲的基础上,给出更多与配置体系相关的高级用法,以及如何创建和管理各种自定义的配置信息。

如何在应用程序中嵌入系统配置信息?

我们知道 Spring Boot 通过自动配置机制内置了很多默认的配置信息,而在这些配置信息中,有一部分系统配置信息也可以反过来作为配置项应用到我们的应用程序中。

例如,如果想要获取当前应用程序的名称并作为一个配置项进行管理,那么很简单,我们直接通过 ${spring.application.name} 占位符就可以做到这一点,如下所示:

复制代码
myapplication.name : ${spring.application.name}

通过 ${} 占位符同样可以引用配置文件中的其他配置项内容,如在下列配置项中,最终"system.description"配置项的值就是"The system springcss is used for health"。

复制代码
system.name=springcss
system.domain=health
system.description=The system ${name} is used for ${domain}.

再来看一种场景,假设我们使用 Maven 来构建应用程序,那么可以按如下所示的配置项来动态获取与系统构建过程相关的信息:

yaml 复制代码
info: 
  app:
    encoding: @project.build.sourceEncoding@
    java:
      source: @java.version@
      target: @java.version@

上述配置项的效果与如下所示的静态配置是一样的:

yaml 复制代码
info:
  app:
    encoding: UTF-8
    java:
        source: 1.8.0_31
        target: 1.8.0_31

根据不同的需求,在应用程序中嵌入系统配置信息是很有用的,特别是在一些面向 DevOps 的应用场景中。

如何创建和使用自定义配置信息?

在现实的开发过程中,面对纷繁复杂的应用场景,Spring Boot 所提供的内置配置信息并不一定能够完全满足开发的需求,这就需要开发人员创建并管理各种自定义的配置信息。例如,对于一个电商类应用场景,为了鼓励用户完成下单操作,我们希望每完成一个订单给就给到用户一定数量的积分。从系统扩展性上讲,这个积分应该是可以调整的,所以我们创建了一个自定义的配置项,如下所示:

复制代码
springcss.order.point = 10

这里,我们设置了每个订单对应的积分为 10,那么应用程序该如何获取这个配置项的内容呢?通常有两种方法。

使用 @Value 注解

使用 @Value 注解来注入配置项内容是一种传统的实现方法。针对前面给出的自定义配置项,我们可以构建一个 SpringCssConfig 类,如下所示:

java 复制代码
@Component
public class SpringCssConfig {
    @Value("${springcss.order.point}")
    private int point;
}

在 SpringCssConfig 类中,我们要做的就是在字段上添加 @Value 注解,并指向配置项的名称即可。

使用 @ConfigurationProperties 注解

相较 @Value 注解,更为现代的一种做法是使用 @ConfigurationProperties 注解。在使用该注解时,我们通常会设置一个"prefix"属性用来指定配置项的前缀,如下所示:

java 复制代码
@Component
@ConfigurationProperties(prefix = "springcss.order")
public class SpringCsshConfig {
	private int point;
	//省略 getter/setter
}

相比 @Value 注解只能用于指定具体某一个配置项,@ConfigurationProperties 可以用来批量提取配置内容。只要指定 prefix,我们就可以把该 prefix 下的所有配置项按照名称自动注入业务代码中。

我们考虑一种更常见也更复杂的场景:假设用户根据下单操作获取的积分并不是固定的,而是根据每个不同类型的订单会有不同的积分,那么现在的配置项的内容,如果使用 Yaml 格式的话就应该是这样:

yaml 复制代码
springcss:
    points:
      orderType[1]: 10
      orderType[2]: 20
      orderType[3]: 30

如果想把这些配置项全部加载到业务代码中,使用 @ConfigurationProperties 注解同样也很容易实现。我们可以直接在配置类 SpringCssConfig 中定义一个 Map 对象,然后通过 Key-Value 对来保存这些配置数据,如下所示:

java 复制代码
@Component
@ConfigurationProperties(prefix="springcss.points")
public class SpringCssConfig {
    private Map<String, Integer> orderType = new HashMap<>();
	//省略 getter/setter
}

可以看到这里通过创建一个 HashMap 来保存这些 Key-Value 对。类似的,我们也可以实现常见的一些数据结构的自动嵌入。

为自定义配置项添加提示功能

如果你已经使用过 Spring Boot 中的配置文件,并添加了一些内置的配置项,你就会发现,当我们输入某一个配置项的前缀时,诸如 IDEA、Eclipse 这样的,IDE 就会自动弹出该前缀下的所有配置信息供你进行选择,效果如下:

IDE 自动提示配置项的效果图

上图的效果对于管理自定义的配置信息非常有用。如何实现这种效果呢?当我们在 application.yml 配置文件中添加一个自定义配置项时,会注意到 IDE 会出现一个提示,说明这个配置项无法被 IDE 所识别,如下所示:

IDE 无法识别配置项时的示意图

遇到这种提示时,我们是可以忽略的,因为它不会影响到任何执行效果。但为了达到自动提示效果,我们就需要生成配置元数据。生成元数据的方法也很简单,直接通过 IDE 的"Create metadata for 'springcss.order.point'"按钮,就可以选择创建配置元数据文件,这个文件的名称为 additional-spring-configuration-metadata.json,文件内容如下所示:

json 复制代码
{"properties": [{
  "name": "springcss.order.point",
  "type": "java.lang.String",
  "description": "A description for 'springcss.order.point'"
}]}

现在,假如我们在 application.properties 文件中输入"springcss",IDE 就会自动提示完整的配置项内容,效果如下所示:

IDE 自动提示 springcss 前缀的效果图

另外,假设我们需要为 springcss.order.point 配置项指定一个默认值,可以通过在元数据中添加一个"defaultValue"项来实现,如下所示:

json 复制代码
{"properties": [{
  "name": "springcss.order.point",
  "type": "java.lang.String",
  "description": "'springcss.order.point' is userd for setting the point when dealing with an order.",
  "defaultValue": 10
}]}

这时候,在 IDE 中设置这个配置项时,就会提出该配置项的默认值为 10,效果如下所示:

IDE 自动提示包含默认值的 springcss 前缀效果图

如何组织和整合配置信息?

在上一课时中,我们提到了 Profile 概念,Profile 可以认为是管理配置信息中的一种有效手段。今天,我们继续介绍另一种组织和整合配置信息的方法,这种方法同样依赖于前面介绍的 @ConfigurationProperties 注解。

使用 @PropertySources 注解

在使用 @ConfigurationProperties 注解时,我们可以和 @PropertySource 注解一起进行使用,从而指定从哪个具体的配置文件中获取配置信息。例如,在下面这个示例中,我们通过 @PropertySource 注解指定了 @ConfigurationProperties 注解中所使用的配置信息是从当前类路径下的 application.properties 配置文件中进行读取。

java 复制代码
@Component
@ConfigurationProperties(prefix = "springcss.order")
@PropertySource(value = "classpath:application.properties")
public class SpringCssConfig {

既然我们可以通过 @PropertySource 注解来指定一个配置文件的引用地址,那么显然也可以引入多个配置文件,这时候用到的是 @PropertySources 注解,使用方式如下所示:

java 复制代码
@PropertySources({
        @PropertySource("classpath:application.properties "),
        @PropertySource("classpath:redis.properties"),
        @PropertySource("classpath:mq.properties")
})
public class SpringCssConfig {

这里,我们通过 @PropertySources 注解组合了多个 @PropertySource 注解中所指定的配置文件路径。SpringCssConfig 类可以同时引用所有这些配置文件中的配置项。

另一方面,我们也可以通过配置 spring.config.location 来改变配置文件的默认加载位置,从而实现对多个配置文件的同时加载。例如,如下所示的执行脚本会在启动 customerservice-0.0.1-SNAPSHOT.jar 时加载D盘下的 application.properties 文件,以及位于当前类路径下 config 目录中的所有配置文件:

bash 复制代码
java -jar customerservice-0.0.1-SNAPSHOT.jar --spring.config.location=file:///D:/application.properties, classpath:/config/

通过 spring.config.location 指定多个配置文件路径也是组织和整合配置信息的一种有效的实现方式。

理解配置文件的加载顺序

通过前面的示例,我们看到可以把配置文件保存在多个路径,而这些路径在加载配置文件时具有一定的顺序。Spring Boot 在启动时会扫描以下位置的 application.properties 或者 application.yml 文件作为全局配置文件:

复制代码
--file:./config/
--file:./
--classpath:/config/
--classpath:/

以下是按照优先级从高到低的顺序,如下所示:

Spring Boot 会全部扫描上图中的这四个位置,扫描规则是高优先级配置内容会覆盖低优先级配置内容。而如果高优先级的配置文件中存在与低优先级配置文件不冲突的属性,则会形成一种互补配置,也就是说会整合所有不冲突的属性。

如何覆写内置的配置类?

关于 Spring Boot 配置体系,最后值得介绍的就是如何覆写它所提供的配置类。在前面的课程中,我们已经反复强调 Spring Boot 内置了大量的自动配置,如果我们不想使用这些配置,就需要对它们进行覆写。覆写的方法有很多,我们可以使用配置文件、Groovy 脚本以及 Java 代码。这里,我们就以Java代码为例来简单演示覆写配置类的实现方法。

在后续的"如何使用 Spring 构建系统安全层?"专题中,我们将会引入 Spring Security 框架来实现对服务访问的权限控制。在 Spring Security 体系中,设置用户认证信息所依赖的配置类是 WebSecurityConfigurer 类。顾名思义,这是一个设置 Web 安全的配置类。Spring Security 提供了 WebSecurityConfigurerAdapter 这个适配器类来简化该配置类的使用方式,我们可以继承 WebSecurityConfigurerAdapter 类并且覆写其中的 configure() 的方法来完成自定义的用户认证配置工作。典型的 WebSecurityConfigurerAdapter 子类及其代码实现如下所示:

java 复制代码
@Configuration
public class SpringHCssWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
     builder.inMemoryAuthentication().withUser("springcss_user").password("{noop}password1").roles("USER").and()
                .withUser("springcss_admin").password("{noop}password2").roles("USER", "ADMIN");
    }
}

关于该类的具体展开超出了今天的讨论内容范围,我们在后续课程介绍到 Spring Security 时会对上述代码进行详细的展开,这里我们只需要知道,在 Spring Boot 中,提供了一些类的内置配置类,而开发人员可以通过构建诸如上述所示的 SpringCssWebSecurityConfigurer 类来对这些内置配置类进行覆写,从而实现自定义的配置信息。

小结与预告

通常在 Web 应用程序的开发过程中,或多或少都会涉及定制化配置信息的使用。在今天的课程中,我们详细介绍了如何创建和使用自定义配置信息的实现过程,同时也给出了如何组织和整合各种配置信息的方法。

05 自动配置:如何正确理解 Spring Boot 自动配置实现原理?

通过前面几个课时内容的介绍,相信你对 Spring Boot 中的配置体系已经有了全面的了解。Spring Boot 中的配置体系是一套强大而复杂的体系,其中最基础、最核心的要数自动配置(AutoConfiguration)机制了。今天我们将围绕这个话题详细展开讨论,看看 Spring Boot 如何实现自动配置。那我们就先从 @SpringBootApplication 注解开始讲起。

@SpringBootApplication 注解

@SpringBootApplication 注解位于 spring-boot-autoconfigure 工程的 org.springframework.boot.autoconfigure 包中,定义如下:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

public @interface SpringBootApplication {
    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};
}

相较一般的注解,@SpringBootApplication 注解显得有点复杂。我们可以通过 exclude 和 excludeName 属性来配置不需要实现自动装配的类或类名,也可以通过 scanBasePackages 和 scanBasePackageClasses 属性来配置需要进行扫描的包路径和类路径。

注意到 @SpringBootApplication 注解实际上是一个组合注解,它由三个注解组合而成,分别是 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。

  • @ComponentScan 注解

@ComponentScan 注解不是 Spring Boot 引入的新注解,而是属于 Spring 容器管理的内容。@ComponentScan 注解就是扫描基于 @Component 等注解所标注的类所在包下的所有需要注入的类,并把相关 Bean 定义批量加载到容器中。显然,Spring Boot 应用程序中同样需要这个功能。

  • @SpringBootConfiguration 注解

@SpringBootConfiguration 注解比较简单,事实上它是一个空注解,只是使用了 Spring 中的 @Configuration 注解。@Configuration 注解比较常见,提供了 JavaConfig 配置类实现。

  • @EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解是我们需要重点剖析的对象,下面进行重点展开。该注解的定义如下代码所示:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

这里我们关注两个新注解,@AutoConfigurationPackage 和 @Import(AutoConfigurationImportSelector.class)。

@AutoConfigurationPackage 注解

@AutoConfigurationPackage 注解定义如下:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

从命名上讲,在这个注解中我们对该注解所在包下的类进行自动配置,而在实现方式上用到了 Spring 中的 @Import 注解。在使用 Spring Boot 时,@Import 也是一个非常常见的注解,可以用来动态创建 Bean。为了便于理解后续内容,这里有必要对 @Import 注解的运行机制做一些展开,该注解定义如下:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

在 @Import 注解的属性中可以设置需要引入的类名,例如 @AutoConfigurationPackage 注解上的 @Import(AutoConfigurationPackages.Registrar.class)。根据该类的不同类型,Spring 容器针对 @Import 注解有以下四种处理方式:

  • 如果该类实现了 ImportSelector 接口,Spring 容器就会实例化该类,并且调用其 selectImports 方法;
  • 如果该类实现了 DeferredImportSelector 接口,则 Spring 容器也会实例化该类并调用其 selectImports方法。DeferredImportSelector 继承了 ImportSelector,区别在于 DeferredImportSelector 实例的 selectImports 方法调用时机晚于 ImportSelector 的实例,要等到 @Configuration 注解中相关的业务全部都处理完了才会调用;
  • 如果该类实现了 ImportBeanDefinitionRegistrar 接口,Spring 容器就会实例化该类,并且调用其 registerBeanDefinitions 方法;
  • 如果该类没有实现上述三种接口中的任何一个,Spring 容器就会直接实例化该类。

有了对 @Import 注解的基本理解,我们再来看 AutoConfigurationPackages.Registrar 类,定义如下:

java 复制代码
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
            register(registry, new PackageImport(metadata).getPackageName());
        }

        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImport(metadata));
        }
}

可以看到这个 Registrar 类实现了前面第三种情况中提到的 ImportBeanDefinitionRegistrar 接口并重写了 registerBeanDefinitions 方法,该方法中调用 AutoConfigurationPackages 自身的 register 方法:

java 复制代码
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
        if (registry.containsBeanDefinition(BEAN)) {
            BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
            ConstructorArgumentValues constructorArguments = beanDefinition
                    .getConstructorArgumentValues();
            constructorArguments.addIndexedArgumentValue(0,
                    addBasePackages(constructorArguments, packageNames));
        }
        else {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(BasePackages.class);
            beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
                    packageNames);
            beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            registry.registerBeanDefinition(BEAN, beanDefinition);
        }
}

这个方法的逻辑是先判断整个 Bean 有没有被注册,如果已经注册则获取 Bean 的定义,通过 Bean 获取构造函数的参数并添加参数值;如果没有,则创建一个新的 Bean 的定义,设置 Bean 的类型为 AutoConfigurationPackages 类型并进行 Bean 的注册。

AutoConfigurationImportSelector

然后我们再来看 @EnableAutoConfiguration 注解中的 @Import(AutoConfigurationImportSelector.class) 部分,首先我们明确 AutoConfigurationImportSelector 类实现了 @Import 注解第二种情况中的 DeferredImportSelector 接口,所以会执行如下所示的 selectImports 方法:

java 复制代码
@Override

public String[] selectImports(AnnotationMetadata annotationMetadata) {

        if (!isEnabled(annotationMetadata)) {

            return NO_IMPORTS;

        }

        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader

                .loadMetadata(this.beanClassLoader);

        AnnotationAttributes attributes = getAttributes(annotationMetadata);

 

        //获取 configurations 集合

        List<String> configurations = getCandidateConfigurations(annotationMetadata,

                attributes);

        configurations = removeDuplicates(configurations);

        Set<String> exclusions = getExclusions(annotationMetadata, attributes);

        checkExcludedClasses(configurations, exclusions);

        configurations.removeAll(exclusions);

        configurations = filter(configurations, autoConfigurationMetadata);

        fireAutoConfigurationImportEvents(configurations, exclusions);

        return StringUtils.toStringArray(configurations);

}

这段代码的核心是通过 getCandidateConfigurations 方法获取 configurations 集合并进行过滤。getCandidateConfigurations 方法如下所示:

java 复制代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
}

这段代码中可以先关注 Assert 校验,该校验是一个非空校验,会提示 "在 META-INF/spring.factories 中没有找到自动配置类" 这个异常信息。看到这里,不得不提到 JDK 中的 SPI 机制,因为无论从 SpringFactoriesLoader 这个类的命名上,还是 META-INF/spring.factories 这个文件目录,两者之间都存在很大的相通性。关于 JDK 中的 SPI 机制,我们在 05 讲的后续内容中马上就会介绍到。

从类名上看,AutoConfigurationImportSelector 类是一种选择器,负责从各种配置项中找到需要导入的具体配置类。该类的结构如下图所示:

AutoConfigurationImportSelector 类层结构图

显然,AutoConfigurationImportSelector 所依赖的最关键组件就是 SpringFactoriesLoader,下面我们对其进行具体展开。

SPI 机制和 SpringFactoriesLoader

要想理解 SpringFactoriesLoader 类,我们首先需要了解 JDK 中 SPI(Service Provider Interface,服务提供者接口)机制。

JDK 中的 SPI 机制

JDK 提供了用于服务查找的一个工具类 java.util.ServiceLoader 来实现 SPI 机制。当服务提供者提供了服务接口的一种实现之后,我们可以在 jar 包的 META-INF/services/ 目录下创建一个以服务接口命名的文件,该文件里配置着一组 Key-Value,用于指定服务接口与实现该服务接口具体实现类的映射关系。而当外部程序装配这个 jar 包时,就能通过该 jar 包 META-INF/services/ 目录中的配置文件找到具体的实现类名,并装载实例化,从而完成模块的注入。SPI 提供了一种约定,基于该约定就能很好地找到服务接口的实现类,而不需要在代码里硬编码指定。JDK 中 SPI 机制开发流程如下图所示:

JDK 中 SPI 机制开发流程图

SpringFactoriesLoader

SpringFactoriesLoader 类似这种 SPI 机制,只不过以服务接口命名的文件是放在 META-INF/spring.factories 文件夹下,对应的 Key 为 EnableAutoConfiguration。SpringFactoriesLoader 会查找所有 META-INF/spring.factories 文件夹中的配置文件,并把 Key 为 EnableAutoConfiguration 所对应的配置项通过反射实例化为配置类并加载到容器中。这一点我们可以在 SpringFactoriesLoader 的 loadSpringFactories 方法中进行印证:

java 复制代码
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        try {
            Enumeration<URL> urls = (classLoader != null ?
                    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            result = new LinkedMultiValueMap<>();

            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryClassName = ((String) entry.getKey()).trim();
                    for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                        result.add(factoryClassName, factoryName.trim());
                    }
                }
            }
            cache.put(classLoader, result);
            return result;
        }

        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
}

以下就是 spring-boot-autoconfigure 工程中所使用的 spring.factories 配置文件片段,可以看到 EnableAutoConfiguration 项中包含了各式各样的配置项,这些配置项在 Spring Boot 启动过程中都能够通过 SpringFactoriesLoader 加载到运行时环境,从而实现自动化配置:

复制代码
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
...

以上就是 Spring Boot 中基于 @SpringBootApplication 注解实现自动配置的基本过程和原理。当然,@SpringBootApplication 注解也可以基于外部配置文件加载配置信息。基于约定优于配置思想,Spring Boot 在加载外部配置文件的过程中大量使用了默认配置。

@ConditionalOn 系列条件注解

Spring Boot 默认提供了 100 多个 AutoConfiguration 类,显然我们不可能会全部引入。所以在自动装配时,系统会去类路径下寻找是否有对应的配置类。如果有对应的配置类,则按条件进行判断,决定是否需要装配。这里就引出了在阅读 Spring Boot 代码时经常会碰到的另一批注解,即 @ConditionalOn 系列条件注解。

@ConditionalOn 系列条件注解的示例

我们先通过一个简单的示例来了解 @ConditionalOn 系列条件注解的使用方式,例如以下代码就是这类注解的一种典型应用,该代码位于 Spring Cloud Config 的客户端代码工程 spring-cloud-config-client 中:

java 复制代码
@Bean    
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
    public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {
        ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
                properties);
        return locator;
}

可以看到,这里运用了两个 @ConditionalOn 注解,一个是 @ConditionalOnMissingBean,一个是 @ConditionalOnProperty。再比如在 Spring Cloud Config 的服务器端代码工程 spring-cloud-config-server 中,存在如下 ConfigServerAutoConfiguration 自动配置类:

java 复制代码
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
        ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}

这里我们运用了 @ConditionalOnBean 注解。实际上,Spring Boot 中提供了一系列的条件注解,常见的包括:

  • @ConditionalOnProperty:只有当所提供的属性属于 true 时才会实例化 Bean
  • @ConditionalOnBean:只有在当前上下文中存在某个对象时才会实例化 Bean
  • @ConditionalOnClass:只有当某个 Class 位于类路径上时才会实例化 Bean
  • @ConditionalOnExpression:只有当表达式为 true 的时候才会实例化 Bean
  • @ConditionalOnMissingBean:只有在当前上下文中不存在某个对象时才会实例化 Bean
  • @ConditionalOnMissingClass:只有当某个 Class 在类路径上不存在的时候才会实例化 Bean
  • @ConditionalOnNotWebApplication:只有当不是 Web 应用时才会实例化 Bean

当然 Spring Boot 还提供了一些不大常用的 @ConditionalOnXXX 注解,这些注解都定义在 org.springframework.boot.autoconfigure.condition 包中。

显然上述 ConfigServicePropertySourceLocator 类中只有在 "spring.cloud.config.enabled" 属性为 true(通过 matchIfMissing 配置项表示默认即为 true)以及类路径上不存在 ConfigServicePropertySourceLocator 时才会进行实例化。而 ConfigServerAutoConfiguration 只有在类路径上存在 ConfigServerConfiguration.Marker 类时才会进行实例化,这是一种常用的自动配置控制技巧。

@ConditionalOn 系列条件注解的实现原理

@ConditionalOn 系列条件注解非常多,我们无意对所有这些组件进行展开。事实上这些注解的实现原理也大致相同,我们只需要深入了解其中一个就能做到触类旁通。这里我们挑选 @ConditionalOnClass 注解进行展开,该注解定义如下:

java 复制代码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documente
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
  Class<?>[] value() default {};
  String[] name() default {};
}

可以看到, @ConditionalOnClass 注解本身带有两个属性,一个 Class 类型的 value,一个 String 类型的 name,所以我们可以采用这两种方式中的任意一种来使用该注解。同时 ConditionalOnClass 注解本身还带了一个 @Conditional(OnClassCondition.class) 注解。所以, ConditionalOnClass 注解的判断条件其实就包含在 OnClassCondition 这个类中。

OnClassCondition 是 SpringBootCondition 的子类,而 SpringBootCondition 又实现了Condition 接口。Condition 接口只有一个 matches 方法,如下所示:

java 复制代码
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

SpringBootCondition 中的 matches 方法实现如下:

java 复制代码
@Override
public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);

        try {
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            logOutcome(classOrMethodName, outcome);
            recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();
        }

        //省略其他方法
}

这里的 getClassOrMethodName 方法获取被添加了@ConditionalOnClass 注解的类或者方法的名称,而 getMatchOutcome 方法用于获取匹配的输出。我们看到 getMatchOutcome 方法实际上是一个抽象方法,需要交由 SpringBootCondition 的各个子类完成实现,这里的子类就是 OnClassCondition 类。在理解 OnClassCondition 时,我们需要明白在 Spring Boot 中,@ConditionalOnClass 或者 @ConditionalOnMissingClass 注解对应的条件类都是 OnClassCondition,所以在 OnClassCondition 的 getMatchOutcome 中会同时处理两种情况。这里我们挑选处理 @ConditionalOnClass 注解的代码,核心逻辑如下所示:

java 复制代码
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);

if (onClasses != null) {
            List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
            if (!missing.isEmpty()) {
                return ConditionOutcome
                        .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                                .didNotFind("required class", "required classes")
                                .items(Style.QUOTE, missing));
            }

            matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
                    .found("required class", "required classes").items(Style.QUOTE, getMatches(onClasses, MatchType.PRESENT, classLoader));
}

这里有两个方法值得注意,一个是 getCandidates 方法,一个是 getMatches 方法。首先通过 getCandidates 方法获取了 ConditionalOnClass 的 name 属性和 value 属性。然后通过 getMatches 方法将这些属性值进行比对,得到这些属性所指定的但在类加载器中不存在的类。如果发现类加载器中应该存在但事实上又不存在的类,则返回一个匹配失败的 Condition;反之,如果类加载器中存在对应类的话,则把匹配信息进行记录并返回一个 ConditionOutcome。

从源码解析到日常开发

在今天的内容中,我们接触到了 Spring Boot 开发过程中非常核心的话题,即自动配置。自动配置是理解 Spring Boot 应用程序构建和运行的关键要素。当我们尝试去理解一个基于 Spring Boot 开发的工具或框架时,今天的内容能帮助你快速切入该工具或框架的实现原理。同时,在日常开发过程中,诸如 SPI 机制和 @ConditionalOn 系列条件注解也都可以直接应用到我们自身的系统设计和开发中,从而提供高扩展性的架构实现方案。

小结与预告

可以说,自动配置是 Spring Boot 最核心和最基本的功能,而 @SpringBootApplication 注解又是 Spring Boot 应用程序的入口。本课时从 @SpringBootApplication 注解入手,详细分析了自动配置机制的实现过程。涉及的知识点比较多,包含 JDK 中的 SPI 机制,以及 @ConditionalOn 系列条件注解,需要你进行分析和掌握。

相关推荐
小飞Coding6 小时前
Spring Boot 中关于 Bean 加载、实例化、初始化全生命周期的扩展点
spring boot
小飞Coding6 小时前
彻底搞懂 Spring 容器导入配置类:@EnableXXX 与 spring.factories 核心原理
spring boot
悟空码字1 天前
Spring Boot 整合 MongoDB 最佳实践:CRUD、分页、事务、索引全覆盖
java·spring boot·后端
皮皮林5513 天前
拒绝写重复代码,试试这套开源的 SpringBoot 组件,效率翻倍~
java·spring boot
用户908324602735 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840826 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解6 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解6 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记6 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者7 天前
Kafka 基础介绍
spring boot·kafka·消息队列