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

How to initialize Jackson on Spring Boot start to have fast 1st request?

问题

我有一个带有基本 RestController (full code available here) 的简单 Spring 引导应用程序。它使用 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);
    }

启动后,第一个请求比任何后续请求慢大约 10 倍。我调试并分析了该应用程序,似乎在第一次请求时,Jackson JSON 解析器正在 AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters and AbstractJackson2HttpMessageConverter.

中的某处初始化

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

问题

如何在启动期间初始化 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;
    }
}

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

我已经尝试过什么?

我使用不同的 DTO 添加了第二个端点,并在我的 PreloadComponent 中调用了它。这不能解决问题。我假设为每种类型创建了一个 Jackson / whatever 实例。

我将 ObjectMapper 自动连接到我的 PreloadComponent 并将 JSON 解析为我的 DTO。同样,这并不能解决问题。

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

我相信,很多 类 将被延迟加载。如果首次调用性能很重要,那么我认为通过调用每个端点进行热身是可行的方法。

为什么说不能调用端点?如果您有数据库并且不想更改数据,请将所有内容包装在事务中并在预热调用后回滚。

我还没有看到任何其他方法来解决这个问题,这并不一定意味着它不存在 ;)

事实证明,Jackson 验证是问题所在。我添加了 JVM 选项

-verbose:class

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

为了证实我的假设,我重新编写了示例并添加了另一个独立的 warm-up controller with a distinct DTO

此 DTO 使用所有 Java 验证注解,如 real DTO 中一样,例如@NotNull, @Min, 等等。此外,它还有一个自定义枚举,也有子类型的验证。

在启动期间,我现在向这个预热端点发出 REST 请求,它不需要包含任何业务逻辑。

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

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

我在以下位置更新了示例:https://github.com/steinsag/warm-me-up