前言
上篇介绍了 Spring Boot 的自动装配机制,个人认为理解自动装配主要有两个作用,一个是应付面试,另一个是只有理解它才能更好的使用它,通过 SPI 机制用户可以轻松自定义自己的自动装配。自动装配常与 spring-boot-starter 结合到一起,当为公司开发内部使用的通用框架,或者做开源项目时,经常会自定义 spring-boot-starter。
再谈自动装配 SPI 机制
在底层,自动装配是通过标准的 @Configuration
类实现的,那么就需要一种机制发现这个 @Configuration
类,这种机制就是 SPI,即 Service Provider Interface
。
Spring 的 SPI 机制其实也不是它的首创,例如 Java JDBC 就通过 SPI 机制查找 /META-INF/services/java.sql.Driver
文件中的驱动实现,Servlet 规范中容器在启动时回调类路径下的 SpringServletContainerInitializer
接口方法。
SPI 机制在 Spring Framework 中使用极少,直到 Spring Boot 才将其发扬光大。当使用注解 @EnableAutoConfiguration
激活自动装配后,/META-INF/spring.factories
文件中的配置类随即被装载。例如想要定义自己的配置类,可以在该文件中写入下述内容。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zzuhkp.autonconfigure.MyCustomAutoConfiguration
自动装配模块划分
其实理解自动装配的 SPI 机制后就能自定义自动装配了,为什么还需要 spring-boot-starter
呢?
这是因为自动装配模块可能会可选的依赖一些第三方的 jar,当这些 jar 包的某个类存在于类路径中时才会条件化的注册某些 bean,因此 spring-boot-starter
的作用更多的是引入这些可选的依赖 jar,然后启用自动装配模块中某些特性。
具体来说,一个完整的 spring-boot-starter 由以下模块组成:
- 包含自动配置代码的
autoconfigure
模块。 - 依赖
autoconfigure
模块并提供附加依赖的starter
模块。
当然了划分两个模块也并非强制,如果业务逻辑比较简单,将 autoconfigure
和 starter
这两个模块合并到一个 starter
模块也可以。
自动装配命名规范
在正式开发一个 spring-boot-starter
之前我们还需要了解一些命名规范,这些命名规范或为约定俗成,或为官方强制要求。
1. 类名规范
这里的类名规范主要指的是自动配置类,Spring 官方也没有对自动配置类指定命名规范,不过通过观察 Spring 官方的自动配置可以可以发现一些规律。
可以看到,所有的自动配置类都遵循 *AutoConfiguration
模式,这种命名方式无论在 Spring Cloud 还是第三方整合,都得到了体现,建议遵循这种命名方式。
2. 包名规范
同样,Spring 官方也没有对自动配置类的包名做强制要求,不过通过观察上面的配置类,同样可以看出,配置类的包名遵循 {root-package}.{module-package}.autoconfigure
的模式,建议大家遵循。
3. 模块名规范
自动装配的模块通常分为 autoconfigure
和 starter
。
对于 autoconfigure
模块的命名方式 Spring 官方并未强制,通常来说为 {module-name}-autoconfigure
。但是对于 starter
模块来说,Spring 官方 starter
的命名方式为 spring-boot-starter-{module-name}
,官方要求用户自定义的命名方式不要和官方保持一致,而是使用 {module-name}-spring-boot-starter
的形式,依此来和官方 starter
做区分。
4. 命名空间
有时候我们定义的 starter
可能会用到一些 Environment 中的属性,这些属性通常会有一个公共的前缀,这个前缀被 Spring 官方称为命名空间。Spring 官方强烈要求用户不应该使用 Spring Boot 内置的命名空间,如 server
、management
、spring
,Spring 升级时很有可能修改这些内置的命名空间。
创建自己的 spring-boot-starter
有了上面的理论基础之后,我们就可以尝试实现自己的 spring-boot-starter
,这里假定我们想要开发一个格式化对象为字符串的功能模块,由于功能比较简单,我们将其合并到一个 starter
模块中。
注意,本文所使用的的 Spring Boot 版本均为
2.2.7.RELEASE
。
首先创建 spring-boot-starter-format
模块,pom 文件内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.zzuhkp</groupId>
<artifactId>spring-boot-starter-format</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
注意,我们自定义的 starter
使用 optional
指定了依赖的 spring-boot-starter
为可选的,以避免依赖传递。
循序面向接口编程,定义我们的格式化接口如下。
public interface Formater {
String format(Object obj);
}
然后再为这个接口创建一个默认的实现类。
public class DefaultFormater implements Formater {
@Override
public String format(Object obj) {
return String.valueOf(obj);
}
}
我们希望能够将 Formater
注册为 bean,以便用户可以直接注入使用,自定义配置类如下。
@Configuration
public class FormatAutoConfiguration {
@Bean
public Formater defaultFormater() {
return new DefaultFormater();
}
}
为了能够让 Spring Boot 发现这个配置类,我们将其添加到 /META-INF/spring.factories
文件中。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zzuhkp.format.autoconfigure.FormatAutoConfiguration
至此,一个最简单的 starter
就创建完成了,创建一个测试项目,然后引入我们自定义的 starter
,测试代码如下。
@SpringBootApplication
public class FormatTestApplication implements CommandLineRunner {
@Autowired
private Formater formater;
public static void main(String[] args) {
SpringApplication.run(FormatTestApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("name", "zzuhkp");
System.out.println(formater.format(map));
}
}
项目启动后可以看到控制台打印出 {name=zzuhkp}
,表明我们自定义的 starter
已生效。
Spring Boot 中的 @Conditional
不过通过 SPI 机制发现配置类,只能为自动装配提供最基础的功能,Spring Boot 自动装配之所以比较灵活还要依托于 Spring Framework 的条件化装配。
Spring Boot 封装了一些常见的 @Conditional
供自动装配使用,并将其命名为 @ConditionalOn*
,下面结合这些 @Conditional
对上述的示例进行改造。
类条件注解
Spring Boot 提供了 @ConditionalOnClass
与 ConditionalOnMissingClass
两个注解允许根据类是否在类路径存在,来决定是否注册 bean。由于 Spring Framework 使用 ASM 直接读取 class 而无需将类加载到 JVM,因此即便给定类不存在也不会抛出异常。
这两个注解的属性如下表所示。
注解 | 属性类型 | 属性方法 | 属性说明 |
---|---|---|---|
@ConditionalOnClass | Class<?>[] | value | 必须存在的类 |
@ConditionalOnClass | String[] | name | 必须存在的类名 |
@ConditionalOnMissingClass | String[] | value | 必须不存在的类名 |
我个人比较喜欢使用阿里的 fastjson 将对象转换为字符串,假定我们希望类路径下存在 fastjson 的 JSON
类时使用 fastjson 来格式化对象,我们可以先引入 fastjson 的依赖。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
<optional>true</optional>
</dependency>
然后修改我们的配置类。
@Configuration
public class FormatAutoConfiguration {
@Bean
@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
public Formater defaultFormater() {
return new DefaultFormater();
}
@Bean
@ConditionalOnClass(JSON.class)
public Formater fastjsonFormater() {
return JSON::toJSONString;
}
}
这里利用 @ConditionalOnClass
与 @ConditionalOnMissingClass
的互斥性,当 JSON
存在时使用 fastjson
格式化对象,不存在时使用默认的 Formater
格式化对象。
将测试项目引入 fastjson,然后再次运行,控制台打印如下。
{"name":"zzuhkp"}
引入 fastjson
之后成功将 Formater
切换为使用 fastjson
格式化对象。
bean 条件注解
bean 条件注解允许用户控制当哪些 bean 存在或不存在时才注册用户自定义的 bean,对应的两个注解是 @ConditionalOnBean
和 @ConditionalOnMissingBean
,这两个注解比类条件注解稍复杂一些,多数属性相同,其属性如下。
注解 | 属性类型 | 属性方法 | 属性说明 |
---|---|---|---|
@ConditionalOnBean、@ConditionalOnMissingBean | Class<?>[] | value | bean 类型,parameterizedContainer 不为空时同时表示泛型类型 |
@ConditionalOnBean、@ConditionalOnMissingBean | String[] | name | bean 类型,parameterizedContainer 不为空时同时表示泛型类型 |
@ConditionalOnBean、@ConditionalOnMissingBean | Class<? extends Annotation>[] | annotation | bean 类型上的注解 |
@ConditionalOnBean、@ConditionalOnMissingBean | String[] | name | bean 名称 |
@ConditionalOnBean、@ConditionalOnMissingBean | SearchStrategy | search | bean 搜索策略 |
@ConditionalOnBean、@ConditionalOnMissingBean | Class<?>[] | parameterizedContainer | 带泛型的 bean 类型 |
@ConditionalOnMissingBean | Class<?>[] | ignored | 忽略的 bean 类型 |
@ConditionalOnMissingBean | String[] | ignoredType | 忽略的 bean 类型 |
对于我们默认的 Formater
,我们希望不满足用户需求时用户可以自定义,那么我们就可以使用 bean 条件注解,当用户未定义时使用默认的配置,修改配置类如下。
@Configuration
public class FormatAutoConfiguration {
@Bean
@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
@ConditionalOnMissingBean(Formater.class)
public Formater defaultFormater() {
return new DefaultFormater();
}
@Bean
@ConditionalOnClass(JSON.class)
@ConditionalOnMissingBean(Formater.class)
public Formater fastjsonFormater() {
return JSON::toJSONString;
}
}
这下我们的配置类更复杂了,在测试项目中注册 Formater
。
@Bean
public Formater formater() {
return obj -> "自定义 Formater:" + obj;
}
然后再次运行,控制台打印如下。
自定义 Formater:{name=zzuhkp}
说明 @ConditionalOnMissingBean
条件注解已生效。
属性条件注解
属性条件注解允许当 Environment 中的某些属性存在并且为指定值时才注册 bean,属性条件注解在 starter
中使用也比较多,对应的注解是 @ConditionalOnProperty
,其属性如下。
属性类型 | 属性方法 | 属性说明 |
---|---|---|
String | prefix | 属性前缀 |
String[] | value | 属性名称 |
String[] | name | 属性名称 |
String | havingValue | 属性值 |
boolean | matchIfMissing | 属性不存在时是否匹配 |
属性条件注解常用于控制是否开启 starter
中的某种特性,假定我们希望环境变量中存在 format.eanble
并且值为 true
中才启用时,我们可以修改配置类如下。
@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true")
public class FormatAutoConfiguration {
}
去掉测试项目中自定义的 Formater
bean,重新启动测试项目,由于没有配置对应属性,可以看到控制台报出如下错误。
Description:
Field formater in com.zzuhkp.format.FormatTestApplication required a bean of type 'com.zzuhkp.format.formater.Formater' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
The following candidates were found but could not be injected:
- Bean method 'defaultFormater' in 'FormatAutoConfiguration' not loaded because @ConditionalOnProperty (format.enable=true) did not find property 'enable'
- Bean method 'fastjsonFormater' in 'FormatAutoConfiguration' not loaded because @ConditionalOnProperty (format.enable=true) did not find property 'enable'
Action:
Consider revisiting the entries above or defining a bean of type 'com.zzuhkp.format.formater.Formater' in your configuration.
错误提示我们有两个候选 Formater
,但是由于没有配置 format.enable=true
属性,导致注入失败,在 application.properties
中配置 format.enable=true
再次运行项目可以看到项目正常运行。
如果我们想默认开启 Formater
怎么办呢?可以设置 matchIfMissing
为 true,代码如下。
@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
public class FormatAutoConfiguration {
}
去掉 application.properties
配置再次运行则可以看到项目正常运行,如果想要关闭 Formater
特性,直接设置 format.enable=false
即可。
资源条件注解
除了上面类条件注解、bean 条件注解、属性条件注解,还有一些不太常用的条件注解。
首先是资源条件注解 @ConditionalOnResource
,这个注解只有一个 String[]
类型的 resources
属性,表示资源的位置。关于 Spring 的资源管理,想要了解更多细节可以参考我之前写的 《Spring 资源管理 (Resource)》。
假定我们希望 application.properties
属性文件存在时才生效 Formater
,可以在配置类上添加如下注解。
@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
public class FormatAutoConfiguration {
}
Web 应用条件注解
Web 应用条件注解用于判断运行环境是否为 Web,对应的注解为 @ConditionalOnWebApplication
和 @ConditionalOnNotWebApplication
。假定希望 Formater
仅运行在 Servlet 环境,可以修改配置类如下。
@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class FormatAutoConfiguration {
}
Spring 表达式条件注解
属性条件注解诞生前多用于表达式条件注解判断属性值,对应的注解为 @ConditionalOnExpression
,表达式的值为 true 时开启特性。使用表达式注解替换属性条件表达式如下。
@Configuration
@ConditionalOnProperty(prefix = "format", name = "enable", havingValue = "true", matchIfMissing = true)
@ConditionalOnResource(resources = "application.properties")
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnExpression("${format.enable:true}")
public class FormatAutoConfiguration {
}
自动装配元数据
为了加快判断条件装配,Spring Boot 读取到配置类之后会先读取 META-INF/spring-autoconfigure-metadata.properties
文件判断条件是否匹配,这个文件内部包含一些配置类的条件。为了生成这个这个配置类的元数据文件,可以在自定义 autoconfiger
或 starter
中加入如下的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
将这个依赖加入到我们的 starter,编译后可以看到生成如下的文件内容。
#Sun Apr 24 22:06:03 CST 2022
com.zzuhkp.format.autoconfigure.FormatAutoConfiguration.ConditionalOnWebApplication=SERVLET
com.zzuhkp.format.autoconfigure.FormatAutoConfiguration=
测试 spring-boot-starter
为了测试 spring-boot-starter
,Spring Boot 官方提供了一个类 ApplicationContextRunner
,在单元测试类中创建这个类的示例,然后调用里面的方法即可。示例如下。
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(FormatAutoConfiguration.class));
@Test
void defaultServiceBacksOff() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Formater.class);
assertThat(context).getBean("defaultFormater").isSameAs(context.getBean(Formater.class));
});
}
更多内容可参考官网 Testing your Auto-configuration。
总结
Spring Boot 自动装配特性基于注解编程模型、条件装配、Spring SPI,这些功能均基于 Spring 应用上下文,在传统的 Spring Framework 项目中,应用上下文是由 Servlet 容器创建的,而 Spring Boot 时代则在应用上下文的生命周期中创建 Servlet 容器,Spring Boot 底层到底做了什么工作呢?后面将在 SpringApplication
的生命周期中进行介绍。
目前我的 《重学 Spring》专栏已经更新了近 60 篇,内容涵盖 IOC、AOP、Web、Spring Boot 各核心特性及其实现原理。如果你也对 Spring 感兴趣,不妨点个关注,目前持续更新中…