如何在 Spring Boot 启动时初始化 Jackson 以获得快速的第一个请求?

Seb*_*ebi 3 spring jackson spring-boot

问题

我有一个简单的 Spring Boot 应用程序,带有基本的 RestController(完整代码可在此处获取)。它使用 JSON 并使用 Jackson 将请求从 JSON 转换并将响应转换为 JSON。

@RestController("/")
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public class SomeController {

    @Autowired
    private SomeService someService;

    @PostMapping
    public ResponseEntity<SomeResponseDto> post(@RequestBody @Valid SomeRequestDto someRequestDto) {
        final SomeResponseDto responseDto = new SomeResponseDto();
        responseDto.setMessage(someRequestDto.getInputMessage());
        responseDto.setUuid(someService.getUuid());

        return ResponseEntity.ok(responseDto);
    }
Run Code Online (Sandbox Code Playgroud)

启动后,第一个请求比任何后续请求慢大约 10 倍。我调试并分析了该应用程序,似乎在第一次请求时,Jackson JSON 解析器正在AbstractMessageConverterMethodArgumentResolver.readWithMessageConvertersAbstractJackson2HttpMessageConverter中的某处进行初始化。

在后续请求中,它似乎会被重新使用。

问题

如何在启动期间初始化 Jackson JSON 解析,以便第一个请求也很快?

我知道如何在Spring启动后触发一个方法。在PreloadComponent中,我添加了如何针对控制器执行 REST 请求的示例。

@Component
public class PreloadComponent implements ApplicationListener<ApplicationReadyEvent> {

    private final Logger logger = LoggerFactory.getLogger(PreloadComponent.class);

    @Autowired
    private Environment environment;

    @Autowired
    private WebClient.Builder webClientBuilder;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // uncomment following line to directly send a REST request on app start-up
//        sendRestRequest();
    }

    private void sendRestRequest() {
        final String serverPort = environment.getProperty("local.server.port");
        final String baseUrl = "http://localhost:" + serverPort;
        final String warmUpEndpoint = baseUrl + "/warmup";

        logger.info("Sending REST request to force initialization of Jackson...");

        final SomeResponseDto response = webClientBuilder.build().post()
                .uri(warmUpEndpoint)
                .header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
                .body(Mono.just(createSampleMessage()), SomeRequestDto.class)
                .retrieve()
                .bodyToMono(SomeResponseDto.class)
                .timeout(Duration.ofSeconds(5))
                .block();

        logger.info("...done, response received: " + response.toString());
    }

    private SomeRequestDto createSampleMessage() {
        final SomeRequestDto someRequestDto = new SomeRequestDto();
        someRequestDto.setInputMessage("our input message");

        return someRequestDto;
    }
}
Run Code Online (Sandbox Code Playgroud)

这只适用于这个玩具示例。实际上,我有许多具有复杂 DTO 的 REST 端点,并且我需要在每个“真实”端点旁边添加一个“预热”端点,因为我无法调用我的真实端点。

我已经尝试过什么?

我添加了具有不同 DTO 的第二个端点,并在我的PreloadComponent. 这并不能解决问题。我假设为每种类型创建了一个杰克逊/任何实例。

我自动连接ObjectMapper到我的PreloadComponentJSON 并将其解析到我的 DTO。再说一遍,这并不能解决问题。

完整源代码位于: https: //github.com/steinsag/warm-me-up

Seb*_*ebi 5

事实证明杰克逊验证是问题所在。我添加了 JVM 选项

-verbose:class
Run Code Online (Sandbox Code Playgroud)

查看类何时加载。我注意到在第一个请求时,有许多 Jackson 验证类被加载。

为了证实我的假设,我重新设计了我的示例,并添加了另一个具有不同 DTO 的独立预热控制器

该 DTO 使用与真实 DTO中一样的所有 Java 验证注释,例如@NotNull@Min等。此外,它还有一个自定义枚举来对子类型进行验证。

在启动过程中,我现在向此预热端点发出 REST 请求,该端点不需要包含任何业务逻辑。

启动后,我的第一个请求现在仅比任何后续请求慢 2-3 倍。这是可以接受的。之前,第一个请求的速度慢了 20-40 倍。

我还评估了是否确实需要 REST 请求,或者仅进行 JSON 解析或 DTO 验证是否就足够了(请参阅PreloadComponent)。这会稍微减少第一个请求的运行时间,但仍然比适当预热慢 5-15 倍。所以我猜想还需要一个 REST 请求来加载 Spring Dispatcher 等中的其他类。

我更新了我的示例: https: //github.com/steinsag/warm-me-up