Home > Software engineering >  How to initialize Jackson on Spring Boot start to have fast 1st request?
How to initialize Jackson on Spring Boot start to have fast 1st request?

Time:10-30

Problem

I have a simple Spring Boot app with a basic RestController (full code available here). It consumes JSON and uses Jackson to convert request from JSON and response to 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);
    }

After start-up, the 1st request is about 10-times slower than any sub-sequent request. I debugged and profiled the app and it seems that on first request a Jackson JSON parser is getting initialized somewhere in AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters and AbstractJackson2HttpMessageConverter.

In sub-sequent requests, it seems to get re-used.

Question

How do I initialize Jackson JSON parsing during start-up so that also 1st request is fast?

I know how to trigger a method after Spring started. In PreloadComponent I added as an example how to do a REST request against the controller.

@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;
    }
}

This only works in this toy example. In reality, I have many REST endpoints with complex DTOs and I would need to add a "warm-up" endpoint next to each "real" endpoint as I can't call my real endpoints.

What I already tried?

I added a second endpoint with a different DTO and called it in my PreloadComponent. This doesn't solve the problem. I assume that an Jackson / whatever instance is created for each type.

I autowired ObjectMapper into my PreloadComponent and parsed JSON to my DTO. Again, this doesn't solve the issue.

Full source available at: https://github.com/steinsag/warm-me-up

CodePudding user response:

I believe, that a lot of classes will be lazy-loaded. If first call performance is important, then I think warming up by calling each endpoint is the way to go.

Why do you say, that you cannot call the endpoints? If you have a database and you don't want to change the data, wrap everything in a transaction and roll it back after the warm up calls.

I haven't seen any other method to solve this, which doesn't necessarily mean, that it doesn't exist ;)

CodePudding user response:

It turns out that Jackson validation is the problem. I added the JVM option

-verbose:class

to see when classes get loaded. I noticed that on 1st request, there are many Jackson validation classes getting loaded.

To confirm my assumption, I re-worked my example and added another independent warm-up controller with a distinct DTO.

This DTO uses all Java validation annotations also present like in the real DTO, e.g. @NotNull, @Min, etc. In addition, it also has a custom enum to also have validation of sub-types.

During start-up, I now do a REST request to this warm-up endpoint, which doesn't need to contain any business logic.

After start-up, my 1st request is now only 2-3 times slower than any sub-sequent requests. This is is acceptable. Before, the 1st request was 20-40 times slower.

I also evaluated if really a REST request is needed or if it is sufficient to just do JSON parsing or validation of a DTO (see PreloadComponent). This reduces runtime of 1st request a bit, but it is still 5-15 times slower than with proper warm-up. So I guess a REST request is needed to also load other classes in Spring Dispatcher, etc.

I updated my example at: https://github.com/steinsag/warm-me-up

  • Related