领先一步
VMware 提供培训和认证,助您加速进步。
了解更多假设你想使用 Spring Boot,但不想使用 @EnableAutoConfiguration
。确切地说,你应该怎么做?在一篇早期文章中,我展示了 Spring 本质上是快速且轻量级的,但其中一条有助于改善启动时间的简短建议是考虑手动导入 Spring Boot 自动配置,而不是自动全部导入。这不适用于所有应用程序,但可能有所帮助,而且了解这些选项肯定不会有坏处。在本文中,我们将探讨进行手动配置的各种方法并评估其影响。
作为基准,我们来看一个拥有单一 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
提供路由,并且还提供默认错误处理。如果 classpath 中包含 Spring Boot Actuator,您还会获得 /health 和 /info 端点(因此启动时间会稍长一些)。
如果您不知道,@SpringBootApplication
注解是使用 @EnableAutoConfiguration
进行元注解的,这正是免费提供了所有这些有用功能的原因。这就是 Spring Boot 受欢迎的原因,所以我们不想失去任何功能,但我们可以更仔细地看看实际发生了什么,并尝试手动完成其中一些工作,看看是否能学到什么。
注意
如果您想尝试这段代码,可以很容易地从 Spring Initializr 获取一个空的 WebFlux 应用程序。只需勾选 "Reactive Web" 复选框并下载项目即可。
尽管 @EnableAutoConfiguration
特性使得向应用程序添加功能变得容易,但它也剥夺了一些对哪些功能被启用的控制权。大多数人乐于做出这种权衡——易用性胜过控制权的丧失。潜在地会带来性能损失——应用程序可能会启动得稍慢,因为 Spring Boot 需要做一些工作来查找所有这些功能并安装它们。事实上,寻找正确功能所需的工作量并不大:没有 classpath 扫描,并且经过仔细优化后,条件评估非常快。对于此类应用程序,启动时间的大部分(约 80%)都花在 JVM 加载类上,因此实际上使其启动更快唯一的方法是让它做更少的工作,即安装更少的功能。
始终可以使用 @EnableAutoConfiguration
注解中的 exclude
属性禁用自动配置。一些单独的自动配置也有自己的布尔配置标志,可以在外部设置,例如,对于 JMX,我们可以使用 spring.jmx.enabled=false
(例如作为系统属性或在属性文件中)。我们可以沿着这条路走下去,手动关闭所有我们不想使用的东西,但这会变得有点笨拙,并且如果 classpath 改变,也不能阻止额外的东西被打开。
相反,让我们看看如何使用现有的自动配置类,但只应用我们知道想要使用的、与我们喜欢的功能相对应的那些。我们可以称之为“按菜单点菜”(a la carte)方法,而不是完全自动配置带来的“自助餐”(all you can eat)。自动配置类只是普通的 @Configuration
,所以原则上我们可以将它们 @Import
到没有 @EnableAutoConfiguration
的应用程序中。
警告
在阅读完本文其余部分之前,请勿这样做。这不是使用 Spring Boot 自动配置的正确方式。它可能会导致某些功能失效,但与往常一样,实际效果可能因情况而异。
例如,这是上面的应用程序,包含了我们想要的所有功能(不包括 Actuator)
@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 自动配置实际上从未被设计成以这种方式使用,这样做可能会在您的应用程序中引入微妙的错误。唯一的缓解方法是进行彻底测试,确保它按照您期望的方式工作,并对升级保持谨慎。
如果 classpath 中包含 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 未来很可能会做更多工作来禁用未暴露或未使用的 Actuator。例如,请参阅关于惰性 Actuator 和条件端点的问题(这已经在 Spring Boot 2.1.2 中实现)。
手动配置的应用程序有 51 个 bean,而完全自动配置的应用程序有 107 个 bean(不包括 Actuator)。所以它启动得更快一点也就不奇怪了。在我们继续介绍实现示例应用程序的不同方式之前,让我们先看看为了让它启动更快而省略了什么。如果您列出这两个应用程序中的 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 为此提供了一个注解:@ImportAutoConfiguration
,它来自 spring-boot-autoconfigure
,但用于 Spring Boot Test 附带的 测试切片 功能。因此,您可以将上面示例中的 @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 会为簿记(排序和定序)做一些额外的工作,但实际上并不多。在合适的硬件上,使用合适的 JVM 标志,它可能仍然能在不到 700ms 内启动。
在早期的文章中我提到,函数式 Bean 定义是使用 Spring 启动应用程序最有效的方式。目前仍然如此,并且通过将所有 Spring Boot 自动配置重写为 ApplicationContextInitializers
,我们可以从这个应用程序中再榨取出 10% 左右的性能。您可以手动完成,也可以使用一些已经为您准备好的初始化器,只要您不介意尝试一些实验性功能。目前有两个活跃的项目正在探索基于函数式 Bean 定义的新工具和新编程模型的想法:Spring Fu 和 Spring Init。两者都提供了至少一套最小的函数式 Bean 定义,用于替换或包装 Spring Boot 自动配置。Spring Fu 基于 API (DSL),不使用反射或注解。Spring Init 包含函数式 Bean 定义,并且还有一个针对“按菜单点菜”配置的基于注解的编程模型原型。两者在其他地方有更详细的介绍。
这里需要注意的主要一点是,函数式 Bean 定义更快,但如果这是您主要关注的问题,请记住它只带来大约 10% 的效果。一旦您将上面精简掉的所有功能重新放回应用程序中,您就会回到加载所有必要类的状态,启动时间也大致相同。换句话说,运行时处理 @Configuration
的成本并非完全可以忽略,但也不是很高(在这些微小应用程序中约为 10% 或大约 100 毫秒)。
以下是一张图表,总结了来自另一个应用程序 Spring PetClinic 的一些基准测试结果
图 1. Petclinic 启动时间(秒)
它不是一个“真实”的应用程序,但比简单示例更重,并且在运行时使用了更多功能(例如 Hibernate),因此它更具现实意义。有两个版本,“demo”和“actr”,其中后者与前者相同,但包含了 Actuator。对于这两个示例,启动时间最快的是黄点,代表函数式 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 和简单示例并非适用于更大、更复杂应用程序的指导。