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

首先 ,

図1.png

这是关于将一个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)。

bannerAds