version: 5.2.0-RELEASE

https://github.com/spring-projects/spring-framework/blob/d1aee0e8691c41753621332ff69b17be3f7c8ba2/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java#L132

Spring Jackson2Decoder fails to determine correct target type from default interface method with a generic type

Spring Jackson2Decoder fails to determine correct target type from default interface method with a generic type

Comment From: sdeleuze

Please provide a repro project for the issue you observe.

Comment From: zhou-hao

https://github.com/zhou-hao/spring-webflux-issue-23791/

https://github.com/zhou-hao/spring-webflux-issue-23791/blob/07b798e53f6f3754bc5902f0ba990d62d84fa38e/src/test/java/com/example/demo/DemoApplicationTests.java

Comment From: rstoyanchev

Thanks for the sample project. I can confirm there is an issue with resolving generic types from a default method via ResolvableType. I was able to further reduce it to the following test:

import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;

import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import static org.assertj.core.api.Assertions.assertThat;

public class Issue23791Tests {

    @Test
    void name() throws NoSuchMethodException {

        Method method = TestController.class.getMethod("test", Mono.class);
        MethodParameter bodyParam = new MethodParameter(method, 0);

        ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParam);
        ResolvableType elementType = bodyType.getGeneric();

        assertThat(elementType.getSource()).isEqualTo(TestEntity.class);
    }

    public interface GenericController<E> {

        @PostMapping("/test")
        default Mono<String> test(@RequestBody Mono<E> test) {
            return Mono.empty();
        }
    }

    @RestController
    public class TestController implements GenericController<TestEntity> {
    }

    public class TestEntity {
    }
}

The above prints:

Expecting:
 <E>
to be equal to:
 <org.springframework.web.reactive.function.client.Issue23791Tests.TestEntity>
but was not.

If the default method is changed to be a regular interface method, the test passes.

Comment From: philwebb

I did a bit of digging that problem is caused by the fact that ResolvableType doesn't know that the method is actually declared in the TestController.

If you put a breakpoint at ResolvableType.forMethodParameter (around line 1326) you'll see that owner is resolved as GenericController. If you change TestController to actually override the test method then you'll see owner is TestController.

Unfortunately it doesn't appear that Method can provide the information that we need. The only way to workaround the problem is to create the parameter as follows:

    MethodParameter param = new MethodParameter(method, 0, TestController.class);

The sets MethodParameter.containingClass which means we can resolve the result.

Comment From: philwebb

@rstoyanchev I think the only way to fix this is to move up the stack to the point where the ResolvableType is created. I don't think there's anything that can be done in ResolvableType itself.

Comment From: rstoyanchev

Thanks for taking a look @philwebb. My mistake actually, the example above does not accurately reflect what actually happens in Spring MVC where a MethodParameter is obtained from a HandlerMethod which has a MethodParameter sub-class that overrides getContainingClass().

So in the above example, the MethodParameter should be initialized as shown below, after which around line 1326 in ResolvableType.forMethodParameter the owner is TestController:

TestController bean = new TestController();
HandlerMethod handlerMethod = new HandlerMethod(bean, bean.getClass().getMethod("test", Mono.class));
MethodParameter bodyParam = handlerMethod.getMethodParameters()[0];

I have also added a disabled test in the framework that mimics closely the scenario. For debugging, this is where we handle the MethodParameter and resolve its generic, and this is where we obtain the context class but fail.

Would you mind taking another look? It seems that all the information should be available and as far as I know the code is using ResolvableType in the expected ways.

Comment From: rstoyanchev

Oops closed in error. So far the only commit for this issue is https://github.com/spring-projects/spring-framework/commit/7456fb9c65f5b21c9aa825a01df1c02360d176f5.

Comment From: philwebb

I've not yet been able to pinpoint the exact cause. If I take the failing type and change ConcreteController23791 so that it overrides the test method I can debug into Jackson2CodecSupport.getObjectReader and see the following:

  • elementType is Person
  • param is null
  • contextClass is null

The call to getJavaType gets passes a type of Class with the value Person.

If I run the original I see the following:

  • elementType is Person
  • param is null
  • contextClass is null

The call to getJavaType gets passes a type of TypeVariableImpl with the value E.

In the second case the call to GenericTypeResolver.resolveType(type, contextClass) fails because there is no context class.

So it seems like the getParameter(type) call is failing in both cases, we just get away with it when we're not using default interface methods.

Comment From: philwebb

It looks like the type passed to getParameter will never be able to get back to the MethodParameter with reactive types. The elementType is ultimately coming from AbstractMessageReaderArgumentResolver.readBody:

ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);

At this point, because adaper is not null we're calling bodyType.getGeneric(). This means that the MethodParameter is no longer the source. It's the generic on the method parameter. Calling bodyType.getSource() will give you the MethodParameter but calling elementType.getSource() will give you the E variable type.

Comment From: philwebb

I'm not sure what the fix is since I don't really understand all of that code. Perhaps if getJavaType fails you can fallback to elementType.resolve() which will give you a Person type.

Comment From: rstoyanchev

Thanks @philwebb.

So I found a way to pass the containing class through the "hints" map which resolves this issue. However I still wonder if there is room for improvement in ResolvableType.

The elementType is ultimately coming from AbstractMessageReaderArgumentResolver.readBody: ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);

At this point, because adaper is not null we're calling bodyType.getGeneric(). This means that the MethodParameter is no longer the source. It's the generic on the method parameter.

Right. I found out that if elementType is created like this, it also works:

ResolvableType elementType = ResolvableType.forType(
        GenericTypeResolver.resolveType(
                bodyType.getGeneric().getType(), bodyParam.getContainingClass()));

Considering that bodyType has the MethodParameter source (i.e. bodyParam), and therefore has access to bodyParam.getContainingClass(), then the magic that is in GenericTypeResolver should be possible to do from within ResolvableType.getGeneric() as well.