Overview

We update our spring boot version to 3.0.0 and build native image with below mentioned grallvm version.

openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

While call the API which use Jackson-databind annotation then it will return with an error.

Related Issues

  • 29426

  • 29386

Comment From: mhalbritter

If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.

Comment From: dilipdhankecha2530

@mhalbritter Yeah, sure I will do it. and share it over here.

Comment From: dilipdhankecha2530

@mhalbritter

Please, you can take a look into test.zip.

I got an error as I mentioned below.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Builder class `com.demo.wrapper.model.Test$Builder` does not have build method (name: 'build')
 at [Source: (String)"{"id":"default"}"; line: 1, column: 1]
    at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1909) ~[wrapper:2.14.1]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder.buildBuilderBased(BeanDeserializerBuilder.java:440) ~[na:na]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBuilderBasedDeserializer(BeanDeserializerFactory.java:362) ~[na:na]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBuilderBasedDeserializer(BeanDeserializerFactory.java:170) ~[na:na]
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:343) ~[na:na]
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264) ~[na:na]
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244) ~[na:na]
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142) ~[na:na]
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:648) ~[wrapper:2.14.1]
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4861) ~[wrapper:2.14.1]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4731) ~[wrapper:2.14.1]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3677) ~[wrapper:2.14.1]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3645) ~[wrapper:2.14.1]
    at com.demo.wrapper.web.TestResource.test(TestResource.java:23) ~[wrapper:na]
    at java.base@17.0.5/java.lang.reflect.Method.invoke(Method.java:568) ~[wrapper:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[wrapper:6.0.2]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003) ~[wrapper:6.0.2]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:906) ~[wrapper:6.0.2]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731) ~[wrapper:6.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:880) ~[wrapper:6.0.2]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814) ~[wrapper:6.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[wrapper:10.1.1]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[wrapper:6.0.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[wrapper:6.0.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109) ~[wrapper:6.0.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[wrapper:6.0.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[na:na]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[na:na]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[wrapper:10.1.1]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[na:na]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[wrapper:10.1.1]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[na:na]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[na:na]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400) ~[na:na]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[wrapper:10.1.1]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861) ~[na:na]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739) ~[na:na]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[wrapper:10.1.1]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[na:na]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[na:na]
    at java.base@17.0.5/java.lang.Thread.run(Thread.java:833) ~[wrapper:na]
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775) ~[wrapper:na]
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203) ~[na:na]

You can use below curl to reproduce the issue.

curl --location --request POST 'localhost:8080/api/test' \
--header 'Content-Type: text/plain' \
--data-raw '{"id":"default"}'

Comment From: mhalbritter

Thanks for the sample. It looks like that the Jackson builder feature is not supported yet.

@JsonDeserialize(builder = Test.Builder.class)

You can either remove the builder or register the ctor and methods from your builder to be reflection-enabled:

    class TestresourceRuntimeHints implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.reflection().registerType(Test.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
        }
    }

and then use

@ImportRuntimeHints(TestresourceRuntimeHints.class)

on any bean.

Comment From: sbrannen

If we address this issue, I think we should reopen #29386 and address it as well.

Comment From: bclozel

As part of this issue, we should consider whether we want to introduce specific AOT rules for Jackson. Since there are many features and variants in Jackson, I'm not sure we should invest in that area.

Comment From: bclozel

We could expand the scope of our @RegisterReflectionForBinding and use it to process method parameters and return types of controller methods.

This means supporting various features like:

  • @JsonNaming strategies
  • @JsonDeserialize, @JsonSerialize
  • @JsonGetter, @JsonSetter @JsonValue
  • @JsonCreator
  • Detecting @JsonSubTypes types if only the interface type is detected through AOT processing

We could also refine the binding algorithm to include/ignore @JsonIgnoreProperties, @JsonIgnore and @JsonIgnoreType, @JsonAutoDetect annotated elements.

All of that should also support custom annotations with @JacksonAnnotationsInside. Note that all of those features can be turned off by a Jackson configuration flag through the MapperFeature, which is not accessible during the AOT phase.

With that in mind, I'm not in favor of implementing Jackson-specific behavior, but keep using our general "reflection for binding" that matches a lot of cases already. I think this should be handled in a GraalVM Feature or better, through a build-time mechanism provided by the Jackson project itself: keeping up with all the changes in Jackson is an important maintenance burden in this space.

Comment From: mhalbritter

keeping up with all the changes in Jackson is an important maintenance burden in this space.

I agree on not implementing Jackson-specific behavior.

Comment From: sdeleuze

On one side I understand our reluctance related to the maintenance burden especially given the various annotations we would have to support, on another side: - We need to be consistent. Why would we support JPA @Converter and @Convert annotation like we do in PersistenceManagedTypesBeanRegistrationAotProcessor and not Jackson similar use case? - Spring is already in charge of providing hints for classes serialized by Jackson via the knowledge it has of related classes via for example Spring MVC/WebFlux programming model or RegisterReflectionForBinding, conceptually this is similar. - The end user will have to go threw the pain of multiple native build failure which is not ideal - Jackson is probably the most popular dependency we support

Since BindingReflectionHintsRegistrar already has some logic related to the handing of @JacksonAnnotation, my proposal would be simply to refine that logic to add invoke hints on declared constructors for class attribute of annotations annotated with @JacksonAnnotation. Looks like a small change, rather maintainable which translate as a big improvement for native users. Any thoughts?

Comment From: sbrannen

Since BindingReflectionHintsRegistrar already has some logic related to the handing of @JacksonAnnotation, my proposal would be simply to refine that logic to add invoke hints on declared constructors for class attribute of annotations annotated with @JacksonAnnotation. Looks like a small change, rather maintainable which translate as a big improvement for native users. Any thoughts?

I agree that not doing anything additional for Jackson use cases will put a large burden on our users. If the Jackson team some day introduces a GraalVM Feature (or similar), that would of course be great, but who knows if that day will ever come.

So, if we can make a big improvement with such a small change like you've proposed Sebastien, I am in favor of that.

Comment From: sdeleuze

After discussing with @bclozel and @jhoeller, let's give it a try. If the implementation is more involved than expected, we may reconsider implementing this.