前言
上篇介绍了 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,因此即便给定类不存在也不会抛出异常。
这两个注解的属性如下表所示。
我个人比较喜欢使用阿里的 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,这两个注解比类条件注解稍复杂一些,多数属性相同,其属性如下。
对于我们默认的 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,其属性如下。
属性条件注解常用于控制是否开启 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)); }); }
总结
Spring Boot 自动装配特性基于注解编程模型、条件装配、Spring SPI,这些功能均基于 Spring 应用上下文,在传统的 Spring Framework 项目中,应用上下文是由 Servlet 容器创建的,而 Spring Boot 时代则在应用上下文的生命周期中创建 Servlet 容器,Spring Boot 底层到底做了什么工作呢?后面将在 SpringApplication 的生命周期中进行介绍。