spring-cloud / spring-cloud-openfeign

Support for using OpenFeign in Spring Cloud apps
Apache License 2.0
1.2k stars 779 forks source link

Deserialization exception in native image with DTO with @ConstructorProperties #966

Closed dpozinen closed 7 months ago

dpozinen commented 8 months ago

Problem:

With the below setup:

@FeignClient(name = "play-client", configuration = PlayClientConfiguration.class)
public interface PlayClient {

    @GetMapping("/play-value")
    PlayResponseValue playValue(@RequestParam String item);

    @lombok.Value
    class PlayResponseValue {
        String result;
    }
}

I'm getting the following exception:

Caused by: org.springframework.web.client.RestClientException: Error while extracting response for type [class com.playtika.services.samples.PlayClient$PlayResponseValue] and content type [application/json]
        at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:118)
        at org.springframework.cloud.openfeign.support.SpringDecoder.decode(SpringDecoder.java:75)
        at org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:61)
        at feign.optionals.OptionalDecoder.decode(OptionalDecoder.java:36)
        at feign.InvocationContext.proceed(InvocationContext.java:36)
        ... 37 common frames omitted
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.playtika.services.samples.PlayClient$PlayResponseValue` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354)
        at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:103)
        ... 41 common frames omitted
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.playtika.services.samples.PlayClient$PlayResponseValue` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
        at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1739)

But everything works perfectly when using a java application (non native).

Investigation results:

At first I thought this was a hint issue, but I checked and they were indeed automatically generated.

{
    "name": "com.playtika.services.samples.PlayClient$PlayResponseValue",
    "allDeclaredConstructors": true,
    "allDeclaredFields": true,
    "methods": [ { "name": "getResult", "parameterTypes": [ ] } ]
  }

I tried adding the constructor as a method hint manually, but that didn't help.

I also attempted to solve this by adding: lombok.anyConstructor.addConstructorProperties=true, which ensured that this was my class file:

public static final class PlayResponseValue {
        private final String result;

        @ConstructorProperties({"result"})
        @Generated
        public PlayResponseValue(final String result) {
            this.result = result;
        }

        @Generated
        public String getResult() {
            return this.result;
        }
...
}

but that didn't help.

Workarounds:

Everything works in either case:

These workarounds are generally acceptable, although would require a lot of effort to implement across multiple services.

Since the constructor is present, the hint for it as well, I believe that native should be able to detect everything just as java would.

Environment

Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.9+11.1 (build 17.0.9+11-LTS-jvmci-23.0-b21, mixed mode, sharing) Spring Boot 3.1.6
OpenFeign 4.0.4

(duplicate of https://github.com/spring-projects/spring-boot/issues/39197)

OlgaMaciaszek commented 8 months ago

Hello @dpozinen, thanks for reporting the issue. We do not offer dedicated support for Lombok, so the users would need to figure out the proper setup for themselves; if you do figure it out and would like to submit a PR, we would review it, but we are not planning to work on it ourselves.

dpozinen commented 8 months ago

hi @OlgaMaciaszek I have verified that this is not a Lombok issue. If I reimplement the class to what Lombok produces, specifically

    public static class PlayResponseValue {
        private final String result;

        @ConstructorProperties("result")
        public PlayResponseValue(String result) {
            this.result = result;
        }

        public String getResult() {
            return this.result;
        }
    }

The issue still persists exactly as described.

I have also linked the decompiled class file previously, which shows no Lombok specific code, except the @Generated markers, which should be of no consequence.

Please have a look. I believe the issue here is that @ConstructorProperties is ignored during deserialization

OlgaMaciaszek commented 8 months ago

@dpozinen I was not able to reproduce it. See a working sample (to reproduce, run a Eureka server docker run -p 8761:8761 springcloud/demo-eureka-server:latest, run server as a regular Boot app, build and run client with AOT, run HTTP call http GET :7211/play). If your issue persists, please provide a minimal, complete, verifiable example that reproduces the issue as a link to a GitHub repo with a small executable app.

dpozinen commented 7 months ago

Hi again @OlgaMaciaszek I finally got my hands on this again. Your working sample works, because it looks like it does not build a native image, it lacks an execution to build it in the maven plugin. Indeed process-aot runs, but the resulting executable is not a AOT compiled native image.

Please have a look at this fork

Use mvn -Pnative -DskipTests package in the client module and then run the binary ./target/feign-eureka-hello-client. Use the same steps you specified to reproduce. The resulting log:

 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: feign.codec.DecodeException: Error while extracting response for type [class demo.clients.HelloClient$PlayResponseValue] and content type [application/json]] with root cause

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `demo.clients.HelloClient$PlayResponseValue` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
OlgaMaciaszek commented 7 months ago

Hello @dpozinen, by "run client with AOT" I meant creating an AOT-processed jar and running it. I've done it locally and haven't faced the issue you've described. I've pushed the AOT adjusted code here now. Sorry if that was confusing. Will take a look at your fork.

OlgaMaciaszek commented 7 months ago

Thanks @dpozinen - was able to reproduce it with your sample. However, this does not seem to be an OpenFeign-specific issue. If you comment-out any Spring Cloud-related code and just use RestTemplate, you'll get the same exception. You may want to create an issue upstream (Spring Framework), but @ConstructorProperties may not be within the scope they support.