spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.62k stars 38.13k forks source link

Add support for Jackson optional serializers in `BindingReflectionHintsRegistrar` #30795

Closed vince-0202 closed 1 year ago

vince-0202 commented 1 year ago

Hi,I got a problem when my spring-boot program running with native image. It can't serializer java.sql.Date caus class com.fasterxml.jackson.databind.ser.std.SqlDateSerializer not found.It only happend in native image.if my program don't running in native image,it works well.

org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Failed to find class `com.fasterxml.jackson.databind.ser.std.SqlDateSerializer` for handling values of type `java.sql.Date`, problem: (java.lang.ClassNotFoundException) com.fasterxml.jackson.databind.ser.std.SqlDateSerializer
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:492) ~[v-tools:6.0.10]
        at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:103) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:297) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:194) ~[na:na]
        at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) ~[na:na]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:136) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[v-tools:6.0.10]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[v-tools:6.0.10]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[v-tools:6.0]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[v-tools:6.0.10]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[v-tools:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[na:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[na:na]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[v-tools:10.1.10]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[na:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[na:na]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[v-tools:6.0.10]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[v-tools:6.0.10]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[na:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[na:na]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[v-tools:6.0.10]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[v-tools:6.0.10]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[na:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[na:na]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[v-tools:6.0.10]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[v-tools:6.0.10]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[na:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[na:na]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166) ~[na:na]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[na:na]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[v-tools:10.1.10]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[na:na]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[v-tools:10.1.10]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[na:na]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) ~[na:na]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[na:na]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[v-tools:10.1.10]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) ~[na:na]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[na:na]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[v-tools:10.1.10]
        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) ~[v-tools:na]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775) ~[v-tools:na]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203) ~[na:na]
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Failed to find class `com.fasterxml.jackson.databind.ser.std.SqlDateSerializer` for handling values of type `java.sql.Date`, problem: (java.lang.ClassNotFoundException) com.fasterxml.jackson.databind.ser.std.SqlDateSerializer (through reference chain: com.github.vince.common.base.Result["data"]->java.util.ArrayList[0]->com.github.vince.domain.schedule.entity.TaskList["tasks"]->java.util.ArrayList[0]->com.github.vince.domain.schedule.entity.Task["taskDate"])
        at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:323) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:780) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18) ~[na:na]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79) ~[na:na]
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18) ~[na:na]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[na:na]
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:479) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:318) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1572) ~[na:na]
        at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1061) ~[v-tools:2.15.2]
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:483) ~[v-tools:6.0.10]
        ... 50 common frames omitted
Caused by: java.lang.IllegalStateException: Failed to find class `com.fasterxml.jackson.databind.ser.std.SqlDateSerializer` for handling values of type `java.sql.Date`, problem: (java.lang.ClassNotFoundException) com.fasterxml.jackson.databind.ser.std.SqlDateSerializer
        at com.fasterxml.jackson.databind.ext.OptionalHandlerFactory.instantiate(OptionalHandlerFactory.java:242) ~[na:na]
        at com.fasterxml.jackson.databind.ext.OptionalHandlerFactory.findSerializer(OptionalHandlerFactory.java:153) ~[na:na]
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.findOptionalStdSerializer(BasicSerializerFactory.java:496) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.findSerializerByPrimaryType(BasicSerializerFactory.java:432) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory._createSerializer2(BeanSerializerFactory.java:235) ~[na:na]
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory.createSerializer(BeanSerializerFactory.java:174) ~[na:na]
        at com.fasterxml.jackson.databind.SerializerProvider._createUntypedSerializer(SerializerProvider.java:1503) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.SerializerProvider._createAndCacheUntypedSerializer(SerializerProvider.java:1451) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.SerializerProvider.findPrimaryPropertySerializer(SerializerProvider.java:717) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap.findAndAddPrimarySerializer(PropertySerializerMap.java:64) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter._findAndAddDynamic(BeanPropertyWriter.java:901) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:710) ~[v-tools:2.15.2]
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[v-tools:2.15.2]
        ... 68 common frames omitted

Here is the response from my program which running in native image and the struct of the task field.Other types of fields are fine.

{
    "code": 0,
    "data": [
        {
            "status": "NOT_START",
            "tasks": [
                {
                    "id": 1,
                    "parentId": null,
                    "name": "test",
                    "details": null,
                    "taskStatus": 0,
                    "progress": 0
                }
            ]
        }
    ]
}{
    "code": 100,
    "data": "Could not write JSON: Failed to find class `com.fasterxml.jackson.databind.ser.std.SqlDateSerializer` for handling values of type `java.sql.Date`, problem: (java.lang.ClassNotFoundException) com.fasterxml.jackson.databind.ser.std.SqlDateSerializer",
    "msg": "error"
}
image

What I use

And the follow is my gradle.build, did I write something wrong?

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.1'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'org.hibernate.orm' version '6.1.6.Final'
    id 'org.graalvm.buildtools.native' version '0.9.23'
}

group = 'com.github.vince'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

compileJava {
    options.compilerArgs << "-Amapstruct.unmappedTargetPolicy=IGNORE"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.h2database:h2'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

hibernate {
    enhancement {
        lazyInitialization true
        dirtyTracking true
        associationManagement true
    }
}
wilkinsona commented 1 year ago

Jackson loads a small number of its less commonly used serializers using reflection. Without some additional metadata, these serializers cannot be loaded in a native image. You could fix your problem by providing some reflection hints that allow SqlDateSerializer to be loaded and instantiated through reflection.

I'll transfer this issue to the Framework team as they may want to consider automatically adding the necessary reflection hints when org.springframework.aot.hint.annotation.RegisterReflectionForBindingProcessor detects that a java.sql.Date will be serialized and that Jackson's in use.

vince-0202 commented 1 year ago

Jackson loads a small number of its less commonly used serializers using reflection. Without some additional metadata, these serializers cannot be loaded in a native image. You could fix your problem by providing some reflection hints that allow SqlDateSerializer to be loaded and instantiated through reflection.

@wilkinsona THX!!! My problem was solved when I added the following configuration

@ImportRuntimeHints(SqlDateSerializerRuntimeHintsConfig.SqlDateSerializerRuntimeHints.class)
public class SqlDateSerializerRuntimeHintsConfig {

    static class SqlDateSerializerRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            for (Constructor<?> constructor : SqlDateSerializer.class.getConstructors()) {
                hints.reflection().registerConstructor(constructor,ExecutableMode.INVOKE);
            }
        }
    }
}

I'll transfer this issue to the Framework team as they may want to consider automatically adding the necessary reflection hints when org.springframework.aot.hint.annotation.RegisterReflectionForBindingProcessor detects that a java.sql.Date will be serialized and that Jackson's in use.

That's great!!!

sdeleuze commented 1 year ago

It could be interesting to explore if OptionalHandlerFactory can be used in BindingReflectionHintsRegistrar#registerJacksonHints (with proper classpath check) to support the various serializers/deserializers listed there.

sdeleuze commented 1 year ago

After a deeper look, since those hints are not very dynamic and can take advantage of conditionalOnType, I think it will be better to support those on https://github.com/oracle/graalvm-reachability-metadata side. I will create a related PR that I will link in a comment.

sdeleuze commented 1 year ago

PR created at https://github.com/oracle/graalvm-reachability-metadata/pull/362.