7.10 Creating Your Own Auto-configuration
如果你需要开发一些共享或开源的库,你可能想要开发你自己的auto-configuration。在外部jar包中定义的Auto-configuration类同样也会被SpringBoot所处理。
自动配置常与Starter相关联,下面会介绍开发自己Starter应当知道的东西,并进一步给出具体的步骤。
7.10.1 理解Auto-configured Beans
实现auto-configuration的类会被@AutoConfiguration注解。该注解被@Configuration元注解,这意味着这种类其实也是标准的@Configuration类。其他的@Conditional注解被用来在自动配置过程中进行限制。常用@ConditionalOnClass和@ConditionalOnMissingBean进行自动配置,因为它们可以根据存在指定bean和不存在指定bean的情况进行动态配置。
可以通过阅读spring-boot-autoconfigure 源代码来查看SpringBoot提供的@AutoConfiguration类( META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)。
7.10.2 定位Auto-configuration 候选项
SpringBoot会扫描你的jar中的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。在该文件中定义的有Auto-configuration类列表。
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
Tip
在imports文件中你可以使用#添加comment。
Note
自动配置只能通过在导入文件中命名来加载。确保它们定义在特定的包空间中,并且不会被SpringBoot component scanning。此外,自动配置类不应启用组件扫描以查找其他组件。而应该使用特定的@Import注解代替。
如果你的configuration需要按照特定的顺序,你可以使用@AutoConfiguration注解的before,beforeName,after,afterNamme参数或使用专用的@AutoConfigureBefore和 @AutoConfigureAfter注解。例如,如果你想要定义一个web-specific configuration 类,那么这个类应当在WebMvcAutoConfiguration之后生效。
如果你定义的auto-configuration类不应知道或本来就不知道其他auto-configuration类,但还想定义顺序,那么那你可以使用@AutoConfigureOrder注解,该注解与常规的@Order有相同的语义,但是是auto-configuration专用的。
与标准@Configuration类一样,auto-configuration类定义的顺序只影响它们bean定义的次序,被创建的次序还是按照每个bean的依赖和@DependsOn关系。
7.10.3 Condition Annotations
在实际开发中,你几乎总会要在你的auto-configuration类上添加一个或多个@Conditional注解。例如常见注解@ConditionalOnMissingBean,它允许开发者去覆盖auto-configuration如果它们对默认的不满意。
SpringBoot包含了很多@Conditional注解,你可以在你自己的@Configuration类或者@Bean方法上使用。
- Class Conditions
- Bean Conditions
- Property Conditions
- Resource Conditions
- Web Application Conditions
- SpEL Expression Conditions
Class Condition
注解@ConditionalOnClass和@ConditionalOnMissingClass让@Configuration类可以根据指定类是否存在来决定是否被包含。由于注解的元数据是使用ASM解析的,所以你可以使用参数value指向真实的类,尽管这个类可能并不存在应用的classpath下。你还可以使用参数name如果你想用String来指定class name。
但是这个机制不适用@Bean方法,因为通常方法的返回类型是condition的目标。在方法的condition生效之前,JVM会加载这个类并处理方法引用,但当类不存在时会失败。
为了处理这种场景,可以使用一个单独的@Configuration类
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@AutoConfiguration
// Some conditions ...
public class MyAutoConfiguration {
// Auto-configured beans ...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SomeService.class)
public static class SomeServiceConfiguration {
@Bean
@ConditionalOnMissingBean
public SomeService someService() {
return new SomeService();
}
}
}
Tip
如果你使用@ConditionalOnClass或者@ConditionalOnMissingClass作为你自己注解的元注解,请使用name,因为在这种场景指向类不会被处理。
Bean Condition
注解@ConditionalOnBean和@ConditionalOnMissingBean允许根据特定bean是否存在来包含一个bean。可以使用参数value来通过指定bean的类型或者通过名称来指定bean。参数search用来限制在ApplicationContext的那一层级寻找bean。
当注解在@Bean方法上时,目标类型为方法的返回值类型
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SomeService someService() {
return new SomeService();
}
}
在上面示例中someServicebean会被创建如果ApplicationContext没有SomeService类类型的bean。
Tip
你需要对这些bean definitions的需要格外小心,因为这些conditions会根据当前bean定义与否进行处理。基于该原因,我们建议只在auto-configuration类上使用@ConditionalOnBean and @ConditionalOnMissingBean,因为这样可以保证它们在user-defined bean definitions添加后才加载。
Note
@ConditionalOnBean和@ConditionalOnMissingBean不会阻止@Configuration类被创建,注解在类和注解在@Bean方法上的唯一区别是注解在类上会阻止@Configuration类成为一个bean。
Tip
当声明一个@Bean方法时,方法的返回值应当提供尽可能多的类型信息,比如,如果你的类实现了一个接口,那么@Bean方法的返回值应当是这个类而不是接口。提供尽可能多的类型信息是非常重要的,因为当使用bean conditions时只能依赖方法签名上的类型信息。
Property Conditions
注解@ConditionalOnProperty能让configuration根据Spring Environment property来决定是否包含。使用参数prefix和name来指定应该被检查的property。默认情况下,存在且不等于false的会被匹配。还可以使用进阶检查参数havingValue和matchIfMissing。
Resource Conditions
注解@ConditionalOnResource能让configuration根据指定的resource是否存在来决定是否包含。resource可以使用传统的Spring方式来进行指定,例如,file:/home/user/test.dat。
Web Application Conditions
注解@ConditionalOnWebApplication和@ConditionalOnNotWebApplication能让configuration根据应用是否是一个web应用来决定是否包含。当应用使用Spring的WebApplicationContext,定义了session域或者使用了ConfigurableWebEnvironment,那么它就是一个基于servlet的web应用。当应用使用了ReactiveWebApplicationContext或者使用了ConfigurableReactiveWebEnvironment那么它就是一个reactive web 应用。
注解@ConditionalOnWarDeployment和@ConditionalOnWarDeployment让configuration根据应用是否是一个部署到servlet容器的war包应用来决定是否包含。
SpEL Expression Conditions
注解@ConditionalOnExpression让configuration根据SpEL表达式的结果来决定是否包含。
7.10.4 Testing your Auto-configuration
pass
7.10.5 Creating Your Own Starter
典型的SpringBoot Starter包含auto-configure的代码和对技术基础设施自定义的代码,让我们称之为“acme”。为了让其是容易扩展的,可以暴露出一些configuration key。最后,提供一个“starter”依赖来帮助用户可以轻松开始。
具体地,一个自定义starter应该包含下列东西:
autoconfigure模块,该模块包含auto-configuration的代码,即“acme”starter模块,该模块为autoconfigure模块即acme提供依赖,并添加其他有用的依赖。简而言之,添加starter应该提供使用该库的一切所需。
将两个模块分开是没必要的。如果"acme"有多种风格,选项或可选的feature,那么最好将auto-configuration分离,这样可以清楚的表明有些feature是可选的。同时,其他人也可以只依赖autoconfigure模块并使用不同的选项来开发他们自己的starter。
Naming
你要确保你的starter拥有一个合适的命名。你的模块名称不要用spring-boot开头,即使你使用了不同的groupId。
假设你为"acme"开发了一个starter,那么auto-configure模块命名为acme-spring-boot,stater模块命名为acme-spring-boot-starter。如果你将这两模块合并到一块,那么应命名为acme-spring-boot-starter。
Configuration keys
如果你的starter提供configuration keys,请确保使用了独一无二的namespace。尤其不要使用SpringBoot使用的namespace(例如,server, management,spring)。按照经验,你应该为你的key添加一个你自己的前缀(例如 acme)。
确保你的configuration key被注释标注,如下所示
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("acme")
public class AcmeProperties {
/**
* Whether to check the location of acme resources.
*/
private boolean checkLocation = true;
/**
* Timeout for establishing a connection to the acme server.
*/
private Duration loginTimeout = Duration.ofSeconds(3);
public boolean isCheckLocation() {
return this.checkLocation;
}
public void setCheckLocation(boolean checkLocation) {
this.checkLocation = checkLocation;
}
public Duration getLoginTimeout() {
return this.loginTimeout;
}
public void setLoginTimeout(Duration loginTimeout) {
this.loginTimeout = loginTimeout;
}
}
下面是一些规则来确保注释是一致的
- 不要以"The"或"A"开头
- 对于
boolean类型,以"Whether"或"Enable"开头 - 对于集合类型,以 "Comma-separated list"开头
- 使用
java.time.Duration而不是long,如果默认单位不是milliseconds那么清描述它,例如"If a duration suffix is not specified, seconds will be used"。 - 不要在注解中提供默认值除非它必须在运行时被确定
记得 trigger meta-data generation以便IDE可以为你的keys提供辅助。生成的元数据为META-INF/spring-configuration-metadata.json,
The "autoconfigure" Module
autoconfiguremodule包含了所需的一切东西,可能包含configuration key的定义(例如 @ConfigurationProperties)或者用于后续自定义的interface。
SpringBoot使用annotation processor来收集auto-configuration上的conditions,并存储到一个元数据文件(META-INF/spring-autoconfigure-metadata.properties)。该文件存在的意义是,用于快速过滤掉不匹配的auto-configurations来提高启动时间。
当使用Maven进行构建时,推荐添加下面依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
如果auto-configuration是直接定义在你的应用中,确保对spring-boot-maven-plugin进行配置防止repackage命令把该依赖打进fat jar。
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Starter Module
这个starter实际是一个空的jar包。它唯一的作用就是提供该库所需的依赖。
不要对添加你的starter的项目有任何假设。如果你的stater需要其他starters,请提及它们。如果可选的依赖非常多,为了避免引入不需要的依赖,那么提供适当的默认依赖可能很困难。换句话说,你不应该包含任何的可选依赖。