7.10 Creating Your Own Auto-configuration (Spring Boot 3.1.5 Reference 翻译)

305 阅读8分钟

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 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来决定是否包含。使用参数prefixname来指定应该被检查的property。默认情况下,存在且不等于false的会被匹配。还可以使用进阶检查参数havingValuematchIfMissing

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,请提及它们。如果可选的依赖非常多,为了避免引入不需要的依赖,那么提供适当的默认依赖可能很困难。换句话说,你不应该包含任何的可选依赖。