从Spring Boot 1.5系到2.1系的迁移指南
首先 ,

这是关于将一个Web服务从使用Spring Boot 1.5.4.RELEASE(其是2019年5月时最新版本)升级到2.1.4.RELEASE的经验总结。升级工作在去年的5月左右已经完成,我们当时将经验总结整理在公司内部的esa平台上,现在在此基础上进行了编辑以供公开使用。
这个图表是根据某公司的财务报告而制作的梗,旨在通过视觉上传达版本升级的影响力。
对于我们的产品来说,根据声明顺序引发的问题,以及由于Mockito的行为更改而需要修复的测试,给我们留下了深刻的印象。
我们在收集指标方面使用了Prometheus,但从Spring Boot 2版本开始,切换到了成为标准的Micrometer,这需要我们在理解行为的同时进行修复,所付出的代价稍大,但一旦熟悉后,它非常方便和易于理解。
此外,由于在一些子系统中我们大量使用了驼峰命名方式编写application.yml文件,因此在将其修改为短横线分隔方式时,也需要进行相应的修正,包括使用@ConfigurationProperties等方式直接指定YAML键来读取值的部分。
实施周围
Lombok注解规范的变更
根据Lombok v1.16.22的变更日志,
FEATURE: Private no-args constructor for @Data and @Value to enable deserialization frameworks (like Jackson) to operate out-of-the-box.
Use lombok.noArgsConstructor.extraPrivate = false to disable this behavior.
简而言之,给予注解@Data或@Value会使得无参数(默认)构造函数默认声明为私有,这是默认行为。
在lombok.properties中写入lombok.noArgsConstructor.extraPrivate = false可以解除这种行为,但是通过在@Data之前声明@NoArgsConstructor,可以先创建一个public的默认构造函数,这样影响范围就较小,或者如果不想编写额外的属性文件的话,(目前)也可以尝试更改注解的顺序作为解决方法(出处)。
只要与此更改的目的(即 Jackson 在反序列化时需要默认构造函数)相符,那么就不必明确地创建默认构造函数,这取决于用途和当前系统中的使用情况。
Netty4ClientHttpRequestFactory已经过时。
由于SpringFramework 5系中已经废弃,因此我决定使用Apache HttpClient(参考)。
看起来还可以使用其他的库如OkHttp3。
度量衡通过Micrometer进行了改变。
我们使用io.prometheus的simpleclient-spring-boot在弊产品中输出基于Prometheus格式的指标。但是,我们需要切换到Spring Boot 2系列的标准指标库Micrometer。
虽然有点难以举例,但基本上,我们需要设置Micrometer(选择使用哪个指标产品),然后相应地修改Gauge和Counter等,以适应各种用途。
// io.prometheus でのGaugeの作り方の例
Gauge gauge = Gauge.build()
.name("health_dsl_context")
.help("Health dsl context")
.register();
// io.micrometer でのGaugeの作り方の例
@Autowired /* 何らかの方法でBeanをInjectしてください */
private PrometheusMeterRegistry prometheusMeterRegistry;
AtomicDouble newGauge = prometheusMeterRegistry.gauge("health_dsl_context", new AtomicDouble(0.0));
@ConfigurationProperties 的前缀必须使用小写的短横线命名法,否则会抛出异常。
我认为您可能会使用@ConfigurationProperties从application.yml读取属性,但自Spring Boot 2.0以后,如果以除了驼峰命名法以外的格式编写时,会抛出InvalidConfigurationPropertyNameException异常。
根据参考资料所述,
The prefix value for the annotation must be in kebab case (lowercase and separated by -, such as acme.my-project.person).
根据所述,关于YAML文件方面(如上所述),使用驼峰命名法等方式进行编写没有问题。然而,由于在代码中读取时存在以上限制,混合使用不同的命名规范会降低配置值的可读性。因此,我们决定统一使用小写短横线命名法进行描述。
允许覆盖 Bean 定义需要明确指定。
如果您使用的是默认设置,Bean的重写将被禁用。
如果您正在使用该功能,请在 application.yml 中添加 spring.main.allow-bean-definition-overriding=true。
如果只是在测试中使用,可能可以在环境配置文件中进行切换。
MySQL的Connector/J和jOOQ的套件类变更。
伴随着 Connector/J 的升级,com.mysql.jdbc.Driver 将会改为 com.mysql.cj.jdbc.Driver(包括驱动程序指定)。
另外,我们将如下更改jOOQ代码生成器的设置。
org.jooq.util.JavaGenerator -> org.jooq.codegen.JavaGenerator
org.jooq.util.mysql.MySQLDatabase -> org.jooq.meta.mysql.MySQLDatabase
在使用Jackson进行序列化时,指定时间戳的格式。
我在 application.yml 文件中指定了 spring.jackson.datetime=”yyyy-MM-dd’T’HH:mm:ss.SSSX”。
如果不写这个,时区标识符会变成 +0000 的形式。
SpringBootApplicationBuilder的启动模式
由于增加了响应式模式,切换到了通过枚举进行指定的方式,可以重新写成.web(WebApplicationType.(SERVLET|NONE|REACTIVE))这样的形式。
public static void main(String... args) {
new SpringApplicationBuilder()
.web(WebApplicationType.NONE)
.sources(ExampleConfiguration.class)
.main(ExampleApp.class)
.build()
.run(args);
}
如果通过RequestInterceptor接收HandlerMethod的话
如果没有使用instanceof进行类型检查,就会出错(实际上,之前运行得还挺好的)。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
/* handlerを扱う処理 */
}
}
内容类型:multipart/form-data;边界=不写完全匹配的测试用例
这只是一个小问题,由于在boundary=之前加上了encoding=UTF-8;,导致我们之前设定的完全匹配条件的测试没有通过。
虽然测试案例的设置也是一个问题,但是写一些能够比较灵活地接受的案例,如使用containsString进行比较,可能会更好。
当在带有@RestController注解的控制器中将返回值设为String类型时,Content-type变为了text/plain。
由于之前通常是以application/json的格式返回,所以这个修正导致客户端无法解析响应的问题出现了。尽管以字符串形式表示的JSON在结构上也是可以接受的,所以在以前的情况下没有问题,但这却是一个意想不到的陷阱。
如果将返回的Content-type设置为application/json作为一种规避策,可以通过将其作为Jackson的TextNode返回来解决问题。 (来自StackOverflow的回答提供了参考)。
@GetMapping("/v1/test")
public TextNode returnStringOnlyJson() {
return new TextNode("responseText");
}
RestTemplateBuilder的超时设置已经改为基于Duration的指定。
过去是用long类型指定毫秒的形式,现在我将其改写如下。
private RestTemplateBuilder restTemplateBuilder(int connectTimeout, int readTimeout) {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofMillis(connectTimeout))
.setReadTimeout(Duration.ofMillis(readTimeout));
}
考試周邊
Mockito中的anyObject()和anyListOf()已被弃用。
请使用 any()、anyList() 或 any(HogeHoge.class)。
使用Mockito的anyInteger()等方法时,直接使用变为了不允许为空。
我们可以使用 nullable(HogeHoge.class)。