如何在Spring Boot上初始化Jackson以开始快速的第一次请求



问题

我有一个简单的Spring Boot应用程序,它有一个基本的RestController(完整的代码在这里可用)。它使用JSON并使用Jackson将请求从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倍。我调试并分析了该应用程序,第一次请求时,似乎在AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters和AbstractJackson2HttpMessageConverter中的某个位置初始化了Jackson JSON解析器。

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

问题

如何在启动期间初始化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验证类正在加载。

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

此DTO使用所有Java验证注释,这些注释也与真实DTO中的注释类似,例如@NotNull@Min等。此外,它还有一个自定义枚举,也可以验证子类型。

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

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

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

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

我相信,很多类都会延迟加载。如果第一次调用性能很重要,那么我认为通过调用每个端点来进行预热是可行的。

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

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

最新更新