杰克逊编码器的假装客户体型序列化失败

发布于 2025-01-22 16:00:44 字数 2773 浏览 0 评论 0原文

我正在努力实施春季服务和客户端,并希望为客户端使用OpenFeign。客户将与不想在春季产生依赖性的旧应用程序部署,因此我直接使用OpenFeign而不是通过弹簧云。

我遇到了杰克逊编码器和身体类型的问题。似乎杰克逊编码器无法序列化接口实现到客户端方法的接口类型。例如,如果我的客户端方法是createfoo(foo interface)其中foo是一个接口,则使用create> createfoo(((fooimpl)fooimpl))其中<代码> fooimpl 实现foo接口,然后我得到一个编码器异常。

我已经创建了一个MCCE Gradle项目,该项目演示了问题在这里

客户定义是这样的:


public interface FooClient {


    @RequestLine("POST /submit")
    @Headers("Content-Type: application/json")
    Response createFoo(Foo foo);

    @RequestLine("POST /submit")
    @Headers("Content-Type: application/json")
    Response createFooImpl(FooImpl foo);

    interface Foo { int id(); }

    record FooImpl(int id) implements Foo { }
}

出现问题的失败测试是:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class FooClientTest {

    @LocalServerPort int port;

    @Test
    public void clientTest() {
        final FooClient lClient = Feign.builder()
            .encoder(new JacksonEncoder(List.of(
                // Possibly this would be necessary with the original encoder implementation.
//                new FooModule()
            )))
            .target(FooClient.class, String.format("http://localhost:%s", port));

        Response response = lClient.createFooImpl(new FooImpl(10));
        assertThat(response.status()).isEqualTo(404);

        response = lClient.createFoo(new FooImpl(10));
        assertThat(response.status()).isEqualTo(404); // <<===== This fails with the exception below.
    }

    public static class FooModule extends SimpleModule {
        {
            addAbstractTypeMapping(Foo.class, FooImpl.class);
        }
    }

}

例外是:

feign.codec.EncodeException: No serializer found for class 
codes.asm.feign.mcce.client.FooClient$FooImpl 
and no properties discovered to create 
BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

此问题是在此提交。它似乎以某种方式删除了杰克逊通过明确调用接口编码来映射接口的编码器的能力。

JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);

基于某些实验,我认为原始代码可以正常工作,并有可能通过模块对编码器进行某些配置。

如测试中所示,我可以通过在接口实现的情况下键入客户端方法来解决问题,但是在我的上下文中,由于多种原因,这是不希望的。

I'm working on implementing a Spring service and client and would like to use OpenFeign for the client. The client will be deployed with legacy applications that do not want to incur a dependency on Spring, so I'm using OpenFeign directly instead of via Spring Cloud.

I've run into an issue with the Jackson encoder and the Body type. It seems that the Jackson encoder cannot serialize an interface implementation to an interface type for the client method. e.g. if my client method is createFoo(Foo interface) where Foo is an interface calling the method with createFoo((FooImpl)fooImpl) where FooImpl implements the Foo interface then I get an encoder exception.

I've created an MCCE Gradle project demonstrating the issue here

The client definition is this:


public interface FooClient {


    @RequestLine("POST /submit")
    @Headers("Content-Type: application/json")
    Response createFoo(Foo foo);

    @RequestLine("POST /submit")
    @Headers("Content-Type: application/json")
    Response createFooImpl(FooImpl foo);

    interface Foo { int id(); }

    record FooImpl(int id) implements Foo { }
}

And the failing test demonstrating the issue is this:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class FooClientTest {

    @LocalServerPort int port;

    @Test
    public void clientTest() {
        final FooClient lClient = Feign.builder()
            .encoder(new JacksonEncoder(List.of(
                // Possibly this would be necessary with the original encoder implementation.
//                new FooModule()
            )))
            .target(FooClient.class, String.format("http://localhost:%s", port));

        Response response = lClient.createFooImpl(new FooImpl(10));
        assertThat(response.status()).isEqualTo(404);

        response = lClient.createFoo(new FooImpl(10));
        assertThat(response.status()).isEqualTo(404); // <<===== This fails with the exception below.
    }

    public static class FooModule extends SimpleModule {
        {
            addAbstractTypeMapping(Foo.class, FooImpl.class);
        }
    }

}

The exception is:

feign.codec.EncodeException: No serializer found for class 
codes.asm.feign.mcce.client.FooClient$FooImpl 
and no properties discovered to create 
BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

This issue was introduced in this commit. It seems to have somehow removed Jackson's ability to map the encoder for the interface to the implementation by explicitly calling for the interface encoder.

JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);

Based on some experiments I think the original code would work fine, potentially with some configuration of the encoder via a Module.

As demonstrated in the test, I can work around the issue by typing the client method with the interface implementation, but this is undesirable for a number of reasons in my context.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

橪书 2025-01-29 16:00:44

我已经找到了解决方法,但这很丑陋。创建一个模块,然后添加以下序列化器。我希望这会非常脆弱,很可能只放弃界面,并以DTO的具体记录定义进行。

 addSerializer(new JsonSerializer<Foo>() {
            @Override
            public Class<Foo> handledType() {
                return Foo.class;
            }

            /**
             * This is an ugly hack to work around this: https://github.com/OpenFeign/feign/issues/1608
             * Alternative would be to just make Foo a concrete record
             * instead of an interface.  That may be better.
             */
            @Override
            public void serialize(Foo value, JsonGenerator gen,
                SerializerProvider serializers) throws IOException {
                gen.writeStartObject();
                final Method[] methods = Foo.class.getMethods();
                for (Method method : methods) {
                    try {
                        final Object result = method.invoke(value);
                        gen.writePOJOField(method.getName(), result);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new IllegalArgumentException(String.format("Class %s has method %s which is not an accessible no argument getter", value.getClass(), method.getName()));
                    }
                }
                gen.writeEndObject();
            }
        });

I've figured out a workaround, but it's quite ugly. Create a Module and add the following serializer. I expect this to be extremely brittle and will likely just abandon the interface and go with a concrete record definition as a DTO.

 addSerializer(new JsonSerializer<Foo>() {
            @Override
            public Class<Foo> handledType() {
                return Foo.class;
            }

            /**
             * This is an ugly hack to work around this: https://github.com/OpenFeign/feign/issues/1608
             * Alternative would be to just make Foo a concrete record
             * instead of an interface.  That may be better.
             */
            @Override
            public void serialize(Foo value, JsonGenerator gen,
                SerializerProvider serializers) throws IOException {
                gen.writeStartObject();
                final Method[] methods = Foo.class.getMethods();
                for (Method method : methods) {
                    try {
                        final Object result = method.invoke(value);
                        gen.writePOJOField(method.getName(), result);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new IllegalArgumentException(String.format("Class %s has method %s which is not an accessible no argument getter", value.getClass(), method.getName()));
                    }
                }
                gen.writeEndObject();
            }
        });
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文