通过Spring Boot中的手动Bean定义提高启动性能

19-01-22 banq
         

使用Spring Boot时你不想使用@EnableAutoConfiguration。你应该怎么做?Spring本质上是快速且轻量级的,但是如何让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加载类时间,因此实际上使其启动更快的唯一方法是通过安装更少的功能来让JVM加载更少。

我们可以在注释@EnableAutoConfiguration中使用exclude属性禁用自动配置。一些单独的自动配置也有自己的布尔配置标志,可以在外部设置,例如我们可以使用的JMX spring.jmx.enabled=false(例如, 作为系统属性或在属性文件中)。我们可以走这条路并手动关闭我们不想使用的所有东西,但是这有点笨拙,并且如果类路径改变也不会阻碍其他组件功能被发现。

现在我们只是使用我们想要使用的那些功能,我们可以将其称为“点菜”方式,而不是“完全自动配置autoconfiguration”中的“所有的你必须吃进去”。自动配置也其实自动寻找@Configuration标注的类,我们可以使用@Import替代@EnableAutoConfiguration,例如,以下是上面的应用程序,具有我们想要的所有功能(不包括执行器):

@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自动配置实际上从未被设计为以这种方式使用,使用这种方式可能会在您的应用程序中引入细微的错误。对此的唯一缓解方式就是是详尽的测试,让它以您期望的方式工作,并且对升级持谨慎态度。

增加Actuators

如果我们也可以在类路径上添加Actuator:

@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 Test附带spring-boot-autoconfigure的 测试切片功能。因此,您可以替换上面示例中的注释@Import, @ImportAutoConfiguration效果是推迟自动配置的处理,直到所有用户配置加载(例如,通过@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为排序做了一些额外的工作,但实际上并不是很多。在合适的硬件上,它可能仍然会在不到700毫秒的时间内启动,并带有正确的JVM标志。

函数Bean定义

在前面的文章中,我提到函数bean定义将是使用Spring启动应用程序的最有效方法。我们可以通过重新编写所有Spring Boot自动配置为ApplicationContextInitializers来将这个应用程序额外挤出10%左右。

您可以手动执行此操作,或者您可以使用已为您准备的一些初始化程序,只要您不介意尝试某些实验性功能即可。目前有2个项目正在积极探索基于函数bean定义的新工具和新编程模型的概念:Spring Fu和 Spring Init。两者都提供至少一组函数bean定义来替换或包装Spring Boot自动配置。

Spring Fu是基于API(DSL)的,不使用反射或注释;Spring Init具有函数bean定义,并且还具有用于“单点”配置的基于注释的编程模型的原型。其他地方都有更详细的介绍。

这里要注意的要点是函数bean定义更快,但如果这是你主要考虑的问题,请记住它只有10%的效果。只要将所有功能放回到我们上面剥离的应用程序中,您就可以重新加载所有必需的类,并重新回到大致相同的启动时间。换句话说,@Configuration本身运行时处理的成本 并不是完全可以忽略不计,但它也不是很高(在这些小应用程序中可能只有10%左右,或者可能是100毫秒)。

总结

Spring Boot自动配置非常方便,但可以称之为“吃进所有你可以吃的东西”。目前(从2.1.x开始)它可能提供比某些应用程序使用或要求更多的功能。在“菜单单点”方法中,您可以使用Spring Boot作为准备和预测试配置的便捷集合,并选择您使用的部件。如果你这样做,那么@ImportAutoConfiguration是工具包的一个重要部分,但是当我们进一步研究这个主题时,你应该如何最好地使用它。

Spring Boot的未来版本以及可能的其他新项目(如Spring Fu或Spring Init)将使得在运行时使用的配置选择变得更加容易,无论是自动还是通过显式选择。注意,@Configuration本身在运行时处理并不是免费的,但它也不是特别昂贵(特别是使用Spring Boot 2.1.x)。您使用的功能数量越少,加载的类越少,启动速度越快。最后,我们不希望 @EnableAutoConfiguration失去其价值或受欢迎程度。

         

1