Spring Boot:创建你自己的自动配置

https://docs.spring.io/spring-boot/docs/3.2.0/reference/htmlsingle/#features.developing-auto-configuration

如果你在开发共享库的公司工作,或者你正在开发开源或商业库,则可能希望开发自己的自动配置。自动配置类可以打包在外部jar中,并且仍然可以被Spring Boot识别。

自动配置可以与“启动器”相关联,该启动器提供自动配置代码以及通常与之一起使用的库。

理解自动配置的Bean

实现自动配置的类都使用 @AutoConfiguration 注解。这个注解本身又使用了 @Configuration 元注解,使得自动配置成为标准的 @Configuration 类。额外的 @Conditional 注解用于约束何时应该应用自动配置。通常,自动配置类使用 @ConditionalOnClass@ConditionalOnMissingBean 注解。这确保了只有当找到相关类并且没有声明自己的 @Configuration 时,才会应用自动配置。

定位自动配置候选项

Spring Boot 会检查你发布的 JAR 包中是否存在名为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件。该文件应列出你的配置类,每行一个类名,如下所示:

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

提示:可以使用 # 字符在 imports 文件中添加注释。

注意:自动配置只能通过在 imports 文件中命名来加载。请确保它们定义在特定的包空间中,并且它们永远不是组件扫描的目标。此外,自动配置类不应该启用组件扫描以查找其它组件。而应该使用特定的 @Import 注解。

如果你的配置需要以特定的顺序应用,你可以在 @AutoConfiguration 注解上使用 beforebeforeNameafterafterName 属性,或者使用专门的 @AutoConfigureBefore@AutoConfigureAfter 注解。例如,如果你提供的是 Web 特定的配置,你的类可能需要在 WebMvcAutoConfiguration 之后应用。

如果想要对某些自动配置进行排序,而这些自动配置之间不应该有任何直接的知识关联,还可以使用 @AutoConfigureOrder 注解。这个注解与常规的 @Order 注解具有相同的语义,但为自动配置类提供了一个专用的排序顺序。

与标准的 @Configuration 类一样,自动配置类的应用顺序只会影响它们定义的 bean 的顺序。这些 bean 随后创建的顺序不受影响,并且由每个 bean 的依赖关系和任何 @DependsOn 关系决定。

条件注解

你几乎总是希望在自动配置类中包含一个或多个 @Conditional 注解。@ConditionalOnMissingBean 注解是一个常见的示例,它允许开发人员在不满意你的默认值时覆盖自动配置。

Spring Boot 包含许多 @Conditional 注解,你可以通过注解 @Configuration 类或单个 @Bean 方法来在自己的代码中重复使用这些注解。这些注解包括:

  • Class Conditions
  • Bean Conditions
  • Property Conditions
  • Resource Conditions
  • Web Application Conditions
  • SpEL Expression Conditions

类条件(Class Conditions)

@ConditionalOnClass@ConditionalOnMissingClass 注解允许根据特定类的存在或不存在来包含 @Configuration 类。由于注解元数据是使用 ASM 解析的,因此你可以使用 value 属性引用实际的类,即使该类实际上并未出现在运行的应用程序的类路径中。如果你更喜欢使用字符串值来指定类名,也可以使用 name 属性。

对于 @Bean 方法,这种机制并不适用,因为通常返回类型是条件的目标:在方法上的条件应用之前,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();
        }

    }

}

提示:如果你将 @ConditionalOnClass@ConditionalOnMissingClass 作为元注解的一部分来组合你自己的组合注解,则必须使用 name,因为在这种情况下不处理对类的引用。

bean条件(Bean Conditions)

@ConditionalOnBean@ConditionalOnMissingBean 注解允许根据特定 bean 的存在或不存在来包含 bean。你可以使用 value 属性通过类型指定 bean,或者使用 name 属性按名称指定 bean。search 属性允许你限制在搜索 bean 时应考虑的 ApplicationContext 层次结构。

当放置在 @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();
    }

}

在前面的示例中,如果 ApplicationContext 中尚未包含类型为 SomeService 的 bean,则会创建 someService bean。

提示:需要非常小心 bean 定义的添加顺序,因为这些条件是基于迄今为止已处理的内容进行评估的。因此,建议在自动配置类上仅使用 @ConditionalOnBean@ConditionalOnMissingBean 注解(因为这些注解保证在添加任何用户定义的 bean 定义之后加载)。

注意@ConditionalOnBean@ConditionalOnMissingBean 不会阻止创建@Configuration 类。在类级别使用这些条件与将每个包含的 @Bean 方法与注解进行标记之间的唯一区别是,如果条件不匹配,前者将阻止将 @Configuration 类注册为 bean。

提示:在声明 @Bean 方法时,请尽可能在方法的返回类型中提供类型信息。例如,如果 bean 的具体类实现了某个接口,那么 bean 方法的返回类型应该是具体类,而不是接口。在使用 bean 条件时,提供 @Bean 方法中尽可能多的类型信息尤为重要,因为它们的评估只能依赖于方法签名中可用的类型信息。

属性条件(Property Conditions)

@ConditionalOnProperty 注解允许基于 Spring Environment 属性包含配置。使用 prefixname 属性来指定应检查的属性。默认情况下,任何存在的属性且不等于 false 的属性都将匹配。还可以使用 havingValuematchIfMissing 属性来创建更复杂的检查。

资源条件(Resource Conditions)

@ConditionalOnResource 注解允许仅在存在特定资源时包含配置。可以使用通常的 Spring 约定来指定资源,如下所示:file:/home/user/test.dat

web应用条件(Web Application Conditions)

@ConditionalOnWebApplication@ConditionalOnNotWebApplication 注解允许根据应用程序是否为 Web 应用程序来包含配置。基于 servlet 的 Web 应用程序是任何使用 Spring WebApplicationContext、定义session 范围或具有 ConfigurableWebEnvironment 的应用程序。响应式 Web 应用程序是任何使用 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的应用程序。

@ConditionalOnWarDeployment@ConditionalOnNotWarDeployment 注解允许根据应用程序是否是部署到 servlet 容器的传统 WAR 应用程序来包含配置。对于使用嵌入式 Web 服务器的应用程序,此条件将不匹配。

SpEL表达式条件(SpEL Expression Conditions)

@ConditionalOnExpression 注解允许基于 SpEL 表达式的结果来包含配置。

注意:在表达式中引用 bean 会导致在上下文刷新处理的非常早期对该 bean 进行初始化。因此,该 bean 将无法进行后处理(例如配置属性绑定),并且其状态可能不完整。

测试你的自动配置(Testing your Auto-configuration)

自动配置可能会受到许多因素的影响:用户配置(@Bean 定义和Environment 自定义)、条件评估(特定库的存在)以及其它因素。具体来说,每个测试都应该创建一个代表这些自定义组合的明确定义的 ApplicationContextApplicationContextRunner 提供了一个很好的实现这一点的方法。

注意:在本地映像中运行测试时,ApplicationContextRunner 无法工作。

ApplicationContextRunner 通常被定义为测试类的字段,以收集基础、常见的配置。以下示例确保始终调用 MyServiceAutoConfiguration

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));

提示:如果必须定义多个自动配置,则无需对它们的声明进行排序,因为它们将以与运行应用程序时完全相同的顺序被调用。

每个测试都可以使用runner 来表示特定的用例。例如,下面的示例调用用户配置(UserConfiguration)并检查自动配置是否正确地进行了回退。调用 run 提供了可以使用 AssertJ 的回调上下文。

@Test
void defaultServiceBacksOff() {
    this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class));
    });
}

@Configuration(proxyBeanMethods = false)
static class UserConfiguration {

    @Bean
    MyService myCustomService() {
        return new MyService("mine");
    }

}

还可以轻松地自定义Environment,如下所示:

@Test
void serviceNameCanBeConfigured() {
    this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
    });
}

runner 还可以用于显示 ConditionEvaluationReport。报告可以 INFODEBUG 级别打印。以下示例展示了如何在自动配置测试中使用 ConditionEvaluationReportLoggingListener 打印报告。

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class MyConditionEvaluationReportingTests {

    @Test
    void autoConfigTest() {
        new ApplicationContextRunner()
            .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
            .run((context) -> {
                // Test something...
            });
    }

}

模拟 Web 上下文

如果您需要测试仅在 servlet 或响应式 Web 应用程序上下文中运行的自动配置,请分别使用 WebApplicationContextRunnerReactiveWebApplicationContextRunner

覆盖类路径(Overriding the Classpath)

还可以测试在运行时缺少特定类或/和包时会发生什么。Spring Boot 提供了一个 FilteredClassLoader,可以方便地在运行器中使用。在下面的示例中,我们断言如果 MyService 不存在,则会自动正确禁用自动配置:

@Test
void serviceIsIgnoredIfLibraryIsNotPresent() {
    this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
        .run((context) -> assertThat(context).doesNotHaveBean("myService"));
}

创建自己的 Starter

典型的 Spring Boot Starter 包含用于自动配置和自定义给定技术(我们称之为“acme”)基础设施的代码。为了使其易于扩展,可以在专用命名空间中公开多个配置键以供环境使用。最后,提供了一个单独的“starter”依赖项,以帮助用户尽可能轻松地入门。

具体来说,自定义 Starter 可以包含以下内容:

  • 包含“acme”的自动配置代码的自动配置模块。
  • Starter 模块,它提供了对自动配置模块以及“acme”和通常很有用的任何其它依赖项的依赖。简而言之,添加 Starter 应该提供开始使用该库所需的一切。

将两个模块分开并不是必需的。如果“acme”具有多种风味、选项或可选功能,则最好将其与自动配置分开,因为你可以清楚地表达某些功能是可选的这一事实。此外,你有能力创建一个 Starter,该 Starter 对这些可选依赖项提供了意见。同时,其他人可以仅依赖自动配置模块并创建具有不同意见的自己的 Starter。

如果自动配置相对简单并且没有可选功能,那么将两个模块合并到 Starter 中当然是一个选择。

命名(Naming)

应该确保为你的 Starter 提供适当的命名空间。即使你使用不同的 Maven groupId,也不要以 spring-boot 开头来命名你的模块。

作为一般规则,应该根据 Starter 命名组合模块。例如,假设你正在为“acme”创建一个 Starter,并将自动配置模块命名为 acme-spring-boot,将 Starter 命名为 acme-spring-boot-starter。如果你只有一个模块将两者组合在一起,请将其命名为 acme-spring-boot-starter

配置键(Configuration keys)

如果你的 Starter 提供了配置键,请为它们使用唯一的命名空间。特别是,不要将你的键包含在 Spring Boot 使用的命名空间中(例如 servermanagementspring 等)。作为一般规则,请使用你拥有的命名空间(例如 acme)为所有键添加前缀。

请确保通过为每个属性添加字段 javadoc 来记录配置键,如下所示:

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;
    }

}

注意:你应该只使用纯文本为 @ConfigurationProperties 字段 Javadoc,因为在将其添加到 JSON 之前不会对其进行处理。

以下是Spring在内部遵循的一些规则,以确保描述的一致性:

  • 不要以“The”或“A”开始描述。
  • 对于布尔类型,请以“Whether”或“Enable”开始描述。
  • 对于基于集合的类型,请以“逗号分隔的列表”开始描述。
  • 使用 java.time.Duration 而不是 long,如果默认单位不同于毫秒,请描述默认单位,例如“如果未指定持续时间后缀,将使用秒”。
  • 除非必须在运行时确定默认值,否则不要在描述中提供默认值。

请确保触发元数据生成,以便为你的键提供 IDE 辅助。你可能需要查看生成的元数据(META-INF/spring-configuration-metadata.json)以确保你的键得到了正确的记录。在兼容的 IDE 中使用你自己的 Starter 也是一个验证元数据质量的好主意。

“autoconfigure”模块

autoconfigure模块包含启动库所需的一切。它还可能包含配置键定义(如 @ConfigurationProperties)以及任何可用于进一步自定义组件初始化方式的回调接口。

提示:你应该将库的依赖项标记为可选,以便更轻松地将autoconfigure模块包含在你的项目中。如果这样做,将不提供库,默认情况下,Spring Boot 将退出。

Spring Boot 使用注解处理器来在元数据文件(META-INF/spring-autoconfigure-metadata.properties)中收集自动配置的条件。如果该文件存在,将用于提前过滤不匹配的自动配置,从而提高启动时间。

当使用 Maven 构建时,建议在包含自动配置的模块中添加以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

如果你在应用程序中直接定义了自动配置,请确保配置 spring-boot-maven-plugin,以防止 repackage 目标将依赖项添加到最终的 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>

对于 Gradle,应该将依赖项声明在 annotationProcessor 配置中,如下所示:

dependencies {
    annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}

Starter 模块

Starter 实际上是一个空 JAR 文件。它的唯一目的是提供与库一起工作所需的必要依赖项。你可以将其视为开始工作所需的必要条件的视图。

不要对添加你的 Starter 的项目做出假设。如果你要自动配置的库通常需要其它 Starter,也请提及它们。如果可选依赖项的数量很多,则提供适当的默认依赖项集可能很困难,因为你应该避免包含对库的典型使用来说不必要的依赖项。换句话说,你不应该包含可选依赖项。

注意:无论如何,你的 Starter 必须直接或间接引用核心的 Spring Boot Starter(spring-boot-starter)(如果你的 Starter 依赖于另一个 Starter,则无需添加它)。如果仅使用你的自定义 Starter 创建项目,则由于核心 Starter 的存在,Spring Boot 的核心功能将得到保留。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值