SpringBoot2 升级 Starter 影响

背景

升级 SpringBoot2,对于 starter 是否有不兼容的地方

升级指南

Spring Boot 2.0 Migration Guide · spring-projects/spring-boot Wiki

官方给出的迁移指南,这里选取一些常见的、可能存在影响的改动点陈列

依赖版本

SpringBoot2 的依赖表 Appendix F. Dependency versions

其中 Spring 框架的版本升级到了 5.x,所以对于 Java 6 和 7 不再支持了

Maven 插件相关的命名和 Surefire 使用方式

以下库的最低支持版本已经更改

  • Elasticsearch 5.6
  • Gradle 4
  • Hibernate 5.2
  • Jetty 9.4
  • Spring Framework 5
  • Spring Security 5
  • Tomcat 8.5

特性

动态代理

默认使用 CGLIB 实现,如果需要使用 JDK 基于接口的动态代理实现

设置配置 spring.aop.proxy-target-classfalse

事件

新增了事件

ApplicationStartedEvent 在上下文刷新后,但在调用任何应用程序和命令行运行器之前

ApplicationReadyEvent 在任何应用程序和命令行运行器被调用后

ApplicationRunner 和 CommandLineRunner

当需要执行特殊逻辑在 Application 启动(started)后,可以实现 ApplicationRunnerCommandLineRunner 接口,提供的 run 方法会在 SpringApplication.run(…) 完成之前被调用

非常适合在应用程序启动后但在开始接收流量之前运行的任务

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {

@Override
public void run(String... args) {
// Do something...
}

}

外部配置

绑定收紧

有关宽松绑定的规则变得更加严格,假设有一个 acme.my-project.my-name 属性

  1. 前缀必须是烤串命名;acme.myProjectacme.my_project 是无效的
  2. 属性名可以使用烤串、驼峰或者蛇形命名
  3. 环境属性必须使用常规的大写下划线格式;例如 ACME_MYPROJECT_MYNAME

概括起来是 prefix 严格,属性宽松

@ConfigurationProperties 校验

如果要打开校验,需要在 @ConfigurationProperties 对象上添加注解 @Validated

Web 应用

Jackson

修改了 Jackson 配置默认值,将 JSR-310 日期写成 ISO-8601 字符串

设置 pring.jackson.serialization.write-dates-as-timestampstrue 可以使用旧配置

spring-boot-starter-json 这个新的 starter 整合了读写 JSON 的必要部分,提供了 Jackson 使用的相关模块依赖,如果之前手动依赖这些模块,现在可以改为依赖这个新的 starter

Spring MVC 路径匹配

禁用了后缀匹配

GET /projects/spring-boot.json 不会匹配到 @GetMapping("/projects/spring-boot")

测试

Mockito 1.x

Mockito 1.x 不再支持 @MockBean@SpyBean

如果没有使用 spring-boot-starter-test 管理依赖,需要升级到 Mockito 2.x

EnvironmentTestUtils

EnvironmentTestUtils 弃用了,使用 TestPropertyValues 提供更强大的能力

1
2
3
TestPropertyValues.of("acme.first=1", "acme.second=2")
.and("acme.third=3")
.applyTo(this.environment);

自定义 Auto-configuration

@ConditionalOnBean 语意变更

@ConditionalOnBean 不再表达候选 bean 的含义,而是 的含义,即多个条件满足才生效

如果需要保持存在任何目标 bean 的条件,可以考虑使用如以下示例所示的 AnyNestedCondition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ThisOrThatCondition extends AnyNestedCondition {

ThisOrThatCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnBean(This.class)
static class ThisCondition {

}

@ConditionalOnBean(That.class)
static class ThatCondition {

}

}

文档

从升级指南来看,涉及 starter 调整的只有 @ConditionalOnBean 语意变化一点,所以还是回归开发文档,看下相关章节

Core Features

理解自动装配 bean

实现自动配置的类使用 @AutoConfiguration (SpringBoot2 才有)进行注解,这个注解本身又被 @Configuration 进行元注解,使得自动配置成为标准的 @Configuration 类;额外的 @Conditional 注解用于约束自动配置何时应用

可以参考 spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports at v2.7.18 · spring-projects/spring-boot

定位自动装配候选

SpringBoot 会检查已发布的 jar 包中是否存在一个名为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件

该文件应当列出你的配置类,每行一个类名,如下例所示

1
2
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

如果自动装配类需要特定顺序应用,可以使用 @AutoConfiguration 注解的beforebeforeNameafter and afterName 属性,或者专有的 @AutoConfigureBefore@AutoConfigureAfter 注解;举个例子,如果希望提供 Web 特殊的装配,那么这个类可能需要在 WebMvcAutoConfiguration 之后被应用(applied)

如果想要对某些不相互直接了解的自动配置进行排序,也可以使用@AutoConfigureOrder;该注解与常规的 @Order 注解具有相同的语义,但为自动配置类提供了专用的排序

和标准的 @Configuration 类一样,顺序只影响 beans 之间的应用,不影响其创建的顺序;创建的顺序由每个 bean 的依赖关系和任何 @DependsOn 关系决定

条件注解

对自动装配生效的规则控制

  • Class Conditions:ASM 解析运行中的类
    • @ConditionalOnClass:存在类
    • @ConditionalOnMissingClass:不存在类
  • Bean Conditions:扫描 beans,可以使用 search 参数指定目标层级
    • @ConditionalOnBean:存在 bean
    • @ConditionalOnMissingBean:不存在 bean
  • Property Conditions
    • @ConditionalOnProperty:检查属性;更多规则使用 havingValuematchIfMissing 参数
  • Resource Conditions
    • @ConditionalOnResource:资源可以使用常见的 Spring 约定来指定,如以下示例所示 file:/home/user/test.dat
  • Web Application Conditions
    • @ConditionalOnWebApplication:Web 应用
    • @ConditionalOnNotWebApplication:非 Web 应用
  • SpEL Expression Conditions
    • @ConditionalOnExpression:使用 SpEL 检查配置

测试自动装配

自动装配会受到很多因素影响,比如配置文件、条件环境(依赖库)等等因素;所以通常来说,每个测试应该创建一个完整定义的 ApplicationContext,它代表了这些定制的组合,ApplicationContextRunner 提供了一种良好的实现方式

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

每个测试都可以使用运行器来表示特定的用例。例如,下面的示例调用用户配置(UserConfiguration)并检查自动配置是否正确回退

调用 run 提供了一个回调上下文,可以与 Assert 一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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

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

使用 FilteredClassLoader 过滤类路径来测试类相关条件

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

自定义自动装配

命名

不要以 spring-boot 开头,因为未来可能被官方收录

应该以业务名称开头,例如 acme-spring-boot-starter

配置键

不要使用官方的命名空间避免冲突,应该使用业务命名例如 acme

确保通过为每个属性添加字段 JavaDoc 来记录配置键

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}

一些建议

  • 不要以 The 或 A 开头来写描述
  • 对于 boolean 类型描述为 “是否” 或者 “启用”
  • 对于集合类型,以 “逗号分隔列表” 为描述开头
  • 使用 java.time.Duration 而不是 long 类型,并在默认单位与毫秒不同时描述该默认单位,例如 “如果未指定持续时间后缀,则将使用秒”
  • 除非在运行时必须确定默认值,否则不要在描述中提供默认值

自动装配 Module

SpringBoot 使用一个注解处理器在元数据文件(META-INF/spring-autoconfigure-metadata.properties)中收集自动配置上的条件,如果该文件存在,它将用于快速过滤不匹配的自动配置,这将提高启动时间

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

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>

如果直接在项目中定义了自动装配,确保配置了 spring-boot-maven-plugin 防止repackage 目标将依赖项添加到 fat jar 中(避免把条件中的 class 打入 jar)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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

  • SpringBoot starter 是一个空 jar(不包含业务逻辑),用于提供一组默认依赖
  • 应该包含所有必要依赖,但避免包含可选依赖;不能预设使用方的依赖情况
  • 必须直接或间接依赖 spring-boot-starter,以确保 SpringBoot 核心功能可用
  • 通常与自动配置结合使用,提供开箱即用的功能

源码

spring-boot 2.7.x

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getCandidateConfigurations 的 Doc 直接就标明了

返回应被考虑的自动配置类名称。默认情况下,此方法将使用 ImportCandidates 和 getSpringFactoriesLoaderFactoryClass() 加载候选类。为了向后兼容,它还会考虑 SpringFactoriesLoader 和 getSpringFactoriesLoaderFactoryClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// SpringFactoriesLoader @since 3.2
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));

// ImportCandidates @since 2.7.0
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);

// assert
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

参考

Spring Boot 2.0 Migration Guide · spring-projects/spring-boot Wiki

Core Features

spring-projects/spring-boot at 2.7.x