Spring Boot 中的手动 Bean 定义

工程 | Dave Syer | 2019年1月21日 | ...

假设您想使用 Spring Boot,但不想使用 @EnableAutoConfiguration。您应该怎么做?在之前的一篇文章中,我展示了 Spring 本身是快速且轻量级的,但其中一条改进启动时间的简短建议是考虑手动导入 Spring Boot 自动配置,而不是自动全部导入。这并非适用于所有应用程序的正确方法,但它可能有所帮助,并且了解可用的选项肯定不会有害。在本文中,我们将探讨各种手动配置方法并评估其影响。

完整自动配置:Hello World WebFlux

作为基线,让我们看一下具有单个 HTTP 端点的 Spring Boot 应用程序。

@SpringBootApplication
@RestController
public class DemoApplication {

  @GetMapping("/")
  public Mono<String> home() {
    return Mono.just("Hello World");
  }

  public void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

如果您使用之前文章中建议的所有调整运行此应用程序,它应该在大约一秒钟内启动,或者根据您的硬件可能稍长一些。它在这段时间内做了很多事情 - 设置日志系统,读取并绑定到配置文件,启动 Netty 并在端口 8080 上监听,为应用程序中的 @GetMapping 提供路由,并提供默认错误处理。如果 Spring Boot Actuator 在类路径上,您还将获得一个 /health 和一个 /info 端点(并且由于此原因,启动时间会稍长一些)。

@SpringBootApplication 注解(如果您不知道的话)使用 @EnableAutoConfiguration 进行元注释,这就是免费提供所有这些有用功能的原因。这就是 Spring Boot 受欢迎的原因,因此我们不想丢失任何功能,但我们可以仔细查看实际发生的情况,并可能手动执行其中一些操作,看看我们是否能学到一些东西。

注意

如果您想尝试此代码,可以轻松地从Spring Initializr获取一个空的 WebFlux 应用程序。只需选中“Reactive Web”复选框并下载项目即可。

自动配置的手动导入

虽然 @EnableAutoConfiguration 功能使向应用程序添加功能变得容易,但它也减少了对启用哪些功能的控制。大多数人都乐于做出这种权衡 - 易用性超过了控制权的损失。潜在的性能损失 - 应用程序的启动速度可能会稍慢一些,因为 Spring Boot 必须做一些工作才能找到所有这些功能并安装它们。事实上,在查找正确的功能方面并没有付出很多努力:没有类路径扫描,并且条件评估非常快,经过仔细优化。此应用程序启动时间的很大一部分(约 80%)被 JVM 加载类所占用,因此实际上,使其启动速度更快唯一的办法就是要求它做更少的事情,通过安装更少的特性。

可以使用 @EnableAutoConfiguration 注解中的 exclude 属性始终禁用自动配置。一些单独的自动配置也有自己的布尔配置标志,可以在外部设置,例如对于 JMX,我们可以使用 spring.jmx.enabled=false(例如作为系统属性或在属性文件中)。我们可以走这条路,手动关闭我们不想使用的所有内容,但这会变得有点笨拙,并且如果类路径发生变化,也不会阻止额外内容被打开。

相反,让我们看看我们可以使用现有的自动配置类做些什么,但只应用我们知道想要使用的类,对应于我们喜欢的功能。我们可以称之为“点菜”方法,而不是完整自动配置提供的“随意吃”。自动配置类只是普通的 @Configuration,因此原则上我们可以将它们 @Import 到不使用 @EnableAutoConfiguration 的应用程序中。

警告

在阅读本文的其余部分之前,请勿执行此操作。这不是使用 Spring Boot 自动配置的正确方法。它可能会破坏某些东西,但一如既往,您的里程可能会有所不同。

例如,以下是上面的应用程序,其中包含我们想要的所有功能(不包括执行器)

@SpringBootConfiguration
@Import({
    WebFluxAutoConfiguration.class,
    ReactiveWebServerFactoryAutoConfiguration.class,
    ErrorWebFluxAutoConfiguration.class,
    HttpHandlerAutoConfiguration.class,
    ConfigurationPropertiesAutoConfiguration.class,
    PropertyPlaceholderAutoConfiguration.class
})
@RestController
public class DemoApplication {

  @GetMapping("/")
  public Mono<String> home() {
    return Mono.just("Hello World");
  }

  public void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

此版本的应用程序仍然将具有我们上面描述的所有功能,但启动速度会更快(可能快 30% 左右)。那么为了获得更快的启动速度,我们放弃了什么?以下是快速概述

  • Spring Boot 自动配置的完整功能集包括其他可能在实际应用程序中实际需要的功能,而不是特定的微小示例。换句话说,30% 的提速并非适用于所有应用程序,并且您的里程可能会有所不同。

  • 手动配置很脆弱,而且难以猜测。如果您编写了另一个执行稍微不同操作的应用程序,则需要不同的配置导入。您可以通过将其提取到一个便捷类或注释中并重复使用它来缓解此问题。

  • @Import 在配置类的排序方面与 @EnableAutoConfiguration 的行为方式不同。如果某些类具有依赖于早期类的条件行为,则 @Import 中的顺序很重要。为了缓解这种情况,您只需小心即可。

  • 在典型的现实世界应用程序中还有另一个排序问题。为了模拟 @EnableAutoConfiguration 的行为,您需要首先处理用户配置,以便它们可以覆盖 Spring Boot 中的条件配置。如果您使用 @ComponentScan,则无法控制扫描的顺序,或与 @Imports 相比,这些类处理的顺序。您可以通过使用不同的注释来缓解此问题(请参见下文)。

  • Spring Boot 自动配置实际上从未设计为以这种方式使用,这样做可能会在您的应用程序中引入细微的错误。对此的唯一缓解措施是详尽的测试以确保它按预期工作,并在升级时保持谨慎。

添加执行器

如果执行器位于类路径上,我们也可以添加它们。

@SpringBootConfiguration
@Import({
    WebFluxAutoConfiguration.class,
    ReactiveWebServerFactoryAutoConfiguration.class,
    ErrorWebFluxAutoConfiguration.class,
    HttpHandlerAutoConfiguration.class,
    EndpointAutoConfiguration.class,
    HealthIndicatorAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
    InfoEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
    ReactiveManagementContextAutoConfiguration.class,
    ManagementContextAutoConfiguration.class,
    ConfigurationPropertiesAutoConfiguration.class,
    PropertyPlaceholderAutoConfiguration.class
})
@RestController
public class DemoApplication {

  @GetMapping("/")
  public Mono<String> home() {
    return Mono.just("Hello World");
  }

  public void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

与完整的 @EndpointAutoConfiguration 应用程序相比,此应用程序的启动速度更快(甚至可能快 50%),因为我们只包含与两个默认端点相关的配置。Spring Boot 默认情况下激活所有端点,但不会将其公开到 HTTP。如果我们只关心 /health 和 /info,那就会浪费,但当然,它也留下很多非常有用的功能。

注意

Spring Boot 可能会在未来做更多的事情来禁用尚未公开或未使用过的执行器。例如,请参阅延迟执行器条件端点上的问题(已在 Spring Boot 2.1.2 中提供)。

有什么区别?

手动配置的应用程序有 51 个 Bean,而完全加载的自动配置应用程序有 107 个 Bean(不包括执行器)。所以它启动速度更快也就不足为奇了。在我们转向实现示例应用程序的不同方法之前,让我们看一下为了使其启动速度更快而省略的内容。如果列出两个应用程序中的 Bean 定义,您会发现所有差异都来自我们省略的自动配置,并且这些自动配置不会被 Spring Boot 有条件地排除。以下是列表(假设您正在使用spring-boot-start-webflux并且没有手动排除)

AutoConfigurationPackages
CodecsAutoConfiguration
JacksonAutoConfiguration
JmxAutoConfiguration
ProjectInfoAutoConfiguration
ReactorCoreAutoConfiguration
TaskExecutionAutoConfiguration
TaskSchedulingAutoConfiguration
ValidationAutoConfiguration
HttpMessageConvertersAutoConfiguration
RestTemplateAutoConfiguration
WebClientAutoConfiguration

因此,我们不需要 12 个自动配置(无论如何),这些配置导致自动配置应用程序中增加了 56 个 Bean。它们都提供了有用的功能,因此我们可能有一天希望再次包含它们,但现在让我们假设我们乐于放弃它们的功能。

注意

spring-boot-autoconfigure 有 122 个自动配置(在spring-boot-actuator-autoconfigure中还有更多),上面完全加载的自动配置示例应用程序仅使用了其中的 18 个。哪些自动配置将被使用,这一计算非常早地就开始了,并且在加载任何类之前,大多数自动配置都会被 Spring Boot 丢弃。它非常快(几毫秒)。

Spring Boot 自动配置导入

与用户配置(必须首先应用)和自动配置之间的差异相关的排序问题可以通过使用不同的注解来部分解决。Spring Boot 为此提供了一个注解:@ImportAutoConfiguration,它来自spring-boot-autoconfigure,但用于随 Spring Boot 测试一起提供的测试切片功能。因此,您可以用@ImportAutoConfiguration替换上面示例中的@Import注解,其效果是推迟处理自动配置,直到所有用户配置(例如,通过@ComponentScan@Import获取)之后。

如果我们准备将自动配置列表整理成自定义注解,我们甚至可以更进一步。与其仅仅将它们复制到显式的@ImportAutoConfiguration中,我们可以编写一个像这样的自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface EnableWebFluxAutoConfiguration {
}

此注解的主要功能是它使用@ImportAutoConfiguration进行元注解。有了它,我们就可以将新的注解添加到我们的应用程序中

@SpringBootConfiguration
@EnableWebFluxAutoConfiguration
@RestController
public class DemoApplication {

  @GetMapping("/")
  public Mono<String> home() {
    return Mono.just("Hello World");
  }

  public void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

}

并在/META-INF/spring.factories中列出实际的配置类

com.example.config.EnableWebFluxAutoConfiguration=\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

这样做的优点是应用程序代码不再需要手动枚举配置,并且排序现在由 Spring Boot 处理(属性文件条目在使用前会进行排序)。缺点是它仅对需要这些功能的应用程序有用,并且必须在任何想要执行一些不同操作的应用程序中进行替换或增强。不过它仍然很快 - Spring Boot 为簿记(排序和排序)做了一些额外的工作,但实际上不多。在合适的硬件和合适的 JVM 标志下,它可能仍然会在不到 700 毫秒的时间内启动。

函数式 Bean 定义

在前面的文章中,我提到函数式 Bean 定义将是使用 Spring 启动应用程序最有效的方法。情况仍然如此,并且我们可以通过将所有 Spring Boot 自动配置重写为ApplicationContextInitializers来从该应用程序中挤出大约 10% 的额外性能。您可以手动执行此操作,或者可以使用一些已经为您准备好的初始化程序,只要您不介意尝试一些实验性功能即可。目前有两个项目正在积极探索基于函数式 Bean 定义的新工具和新编程模型的想法:Spring FuSpring Init。两者都至少提供了一组最小的函数式 Bean 定义,用于替换或包装 Spring Boot 自动配置。Spring Fu 基于 API(DSL),不使用反射或注解。Spring Init 具有函数式 Bean 定义,并且还具有“点菜式”配置的基于注解的编程模型的原型。两者在其他地方都有更详细的介绍。

这里需要注意的主要一点是函数式 Bean 定义更快,但如果这是您的主要关注点,请记住它只是一个 10% 的效果。一旦您将上面剥离的所有功能放回应用程序中,您就会回到加载所有必要的类并回到大致相同的启动时间。换句话说,运行时@Configuration处理的成本并非完全可以忽略不计,但也不是很高(在这些小型应用程序中大约为 10%,或者可能是 100 毫秒)。

总结和未来方向

这是一个图表,总结了来自不同应用程序(Spring PetClinic)的一些基准测试结果

pubchart?oid=1003506885&format=image

图 1. Petclinic 启动时间(秒)

它不是一个“真正的”应用程序,但它比简单的示例更重,并且在运行时使用了更多功能(例如 Hibernate),因此它在某种程度上更现实。有两个版本,“demo”和“actr”,后者与前者相同,但增加了执行器。对于这两个示例,最快的启动时间是黄点,即函数式 Bean 定义,但仅落后于此 10%(在此应用程序中大约为 200 毫秒)的是“点菜式”选项(绿色和红色)。绿色使用类似于上面@EnableWebFluxAutoConfiguration的自定义注解。红色是另一种“点菜式”选项,其中可以通过不同的自定义注解一起导入自动配置组,当前命名为@SpringInitApplication,并且正在 Spring Init 中进行原型设计。蓝色是完全加载的自动配置(开箱即用的 Spring Boot)。

Spring Boot 自动配置非常方便,但可以被描述为“随意享用”。目前(截至 2.1.x),它提供的功能可能超出了某些应用程序的使用或需求。在“点菜式”方法中,您可以将 Spring Boot 用作方便的准备好的预测试配置集合,并选择使用哪些部分。如果您这样做,那么@ImportAutoConfiguration是工具包的重要组成部分,但当我们进一步研究此主题时,您应该如何最好地使用它可能会发生变化。未来版本的 Spring Boot,以及可能像 Spring Fu 或 Spring Init 这样的其他新项目,将使在运行时缩小所用配置的选择变得更容易,无论是自动还是通过显式选择。归根结底,运行时@Configuration处理并非免费,但它也不是特别昂贵(尤其是在 Spring Boot 2.1.x 中)。您使用的功能越少,加载的类就越少,从而导致启动速度更快。归根结底,我们不希望@EnableAutoConfiguration失去其价值或受欢迎程度,并请记住您的里程可能会有所不同:本文中的 PetClinic 和简单示例并非您在使用更大、更复杂的应用程序时可以预期的指南。

获取 Spring 电子报

与 Spring 电子报保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部