Return 来自 Spring 集成的 @Gateway 方法的 ResponseEntity

Return ResponseEntity from Spring Integration's @Gateway method

我有 IntegrationFlow 我调用 HTTP 端点的地方:

@Bean
public IntegrationFlow getInformationFlow(RestTemplate restTemplate) {
    return IntegrationFlows.from(GET_RESPONSE_ENTITY)
            .handle(Http
                    .outboundGateway(url + "/application/{code}", restTemplate)
                    .httpMethod(GET)
                    .uriVariable("code", "payload")
                    .expectedResponseType(new ParameterizedTypeReference<ResponseEntity<Information>>() {
                    })
            ).get();
}

由于 Gateway

,调用 getInformation 时会执行此流程
@MessagingGateway
public interface MyGateway {

    @Gateway(requestChannel = GET_RESPONSE_ENTITY)
    ResponseEntity<Information> getInformation(String code);

}

上面的代码抛出以下异常:

Caused by: org.springframework.http.converter.HttpMessageConversionException: 
Type definition error: [simple type, class org.springframework.http.ResponseEntity]; 
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Cannot construct instance of `org.springframework.http.ResponseEntity` 
(no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

Jackson 尝试反序列化为 ResponseEntity 而不是 Information class

我的目标是首先检查状态代码,然后有选择地检查 Information 字段

是否可以从 @Gateway 注释的方法中 return ResponseEntity?

对您问题的简短回答:这取决于 MyGateway 合同的实施。处理 return 确实不是网关(或任何接口 API)的责任。他们只定义契约。妥善执行这样的合约已经是你的目标了。

我的意思是 Spring 与其 EIP 组件的集成并不比常规 Java 程序设计和架构更进一步。这个合约是 IntegrationFlow 作为一个实现只是一个特例。因此,问题不在于合同,而在于实现细节,在您的情况下是 HTTP 调用。

所以,最好问这样的问题:

How to return a ResponseEntity<Information> from the Http.outboundGateway() with a Jackson message converter?

这就是为什么我在 Gitter 上向您询问这个 SO 线程以更好地了解正在发生的事情。您最初的问题具有误导性,实际上与 @MessagingGateway 无关。我什至确定堆栈跟踪中的一些线索表明问题发生在 RestTemplate 调用中,而不是在 @MessagingGateway.

现在正在尝试帮助您解决您的显式问题。

AbstractHttpRequestExecutingMessageHandler不return一个ResponseEntity当有一个body:

protected Object getReply(ResponseEntity<?> httpResponse) {
    HttpHeaders httpHeaders = httpResponse.getHeaders();
    Map<String, Object> headers = this.headerMapper.toHeaders(httpHeaders);
    if (this.transferCookies) {
        doConvertSetCookie(headers);
    }

    AbstractIntegrationMessageBuilder<?> replyBuilder;
    MessageBuilderFactory messageBuilderFactory = getMessageBuilderFactory();
    if (httpResponse.hasBody()) {
        Object responseBody = httpResponse.getBody();
        replyBuilder = (responseBody instanceof Message<?>)
                ? messageBuilderFactory.fromMessage((Message<?>) responseBody)
                : messageBuilderFactory.withPayload(responseBody); // NOSONAR - hasBody()
    }
    else {
        replyBuilder = messageBuilderFactory.withPayload(httpResponse);
    }
    replyBuilder.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
            httpResponse.getStatusCode());
    return replyBuilder.copyHeaders(headers);
}

仅当没有 body 时。在那种情况下,这意味着没有任何内容可以映射到 payload 以用于回复消息,因此我们使用整个 ResponseEntity.

如您在此代码片段中所见,StatusCode 映射到 org.springframework.integration.http.HttpHeaders.STATUS_CODE 回复消息 header。

关于这个问题的文档中也有一些解释:https://docs.spring.io/spring-integration/docs/current/reference/html/http.html#using-cookies

从这里开始,这意味着您的 expectedResponseType 只能作为 Information 类型。 RestTemplate 及其 HttpMessageConverts 确实不知道如何处理要映射的 ResponseEntity 类型。

因为它可能 return 只是有效载荷中的一个 Information 如果它出现在响应中,或者可能 return 整个 ResponseEntity 空 body,看来您必须在 returning ResponseEntity<Information> 作为对 @MessagingGateway 呼叫的回复之前添加一些路由和转换逻辑。或者您可以修改该网关合同并通过路由器或过滤器在集成流程中真正实施状态代码检查 - 您的 @MessagingGateway 消费者将摆脱状态代码检查和 headers 转换等 HTTP 内容.

虽然在 AbstractHttpRequestExecutingMessageHandler 上有一些选项总是 return 整个 ResponseEntity 作为有效载荷可能没有什么坏处。欢迎提出 GH 问题,我们会考虑尽快实施!

TL;DR: 这是 Spring Integration >=5.5.0 中的一项新功能(感谢 OP)。您需要:

  1. 获取合适的Spring集成版本
  2. 更改您的 MessagingGateway,使其 return 成为 ResponseEntity
  3. 将您的集成流程配置为提取正文并期望ResponseEntity
  4. 新建一个支持ResponseEntity
  5. HttpMessageConverter

OP 创建的 GitHub issue/feature 请求导致在 Spring 集成中实现了一个实际功能。现在可以接收 ResponseEntity 个对象。 Artem Bilan 解释的内容在功能实施之前是正确的,如果标志 extractResponseBody 设置为 true(默认),则仍然正确。如果设置为false,那么很容易得到ResponseEntity(现在)。

你需要做什么

首先获取 Spring 的新版本 (>=5.5.0) 集成到您的 POM.xmlbuild.gradle

dependencies {

    ...
    implementation "org.springframework.integration:spring-integration-http:5.5.2"
    ...
}

在我的例子中,我之前有一个 REST 调用导致 String。现在我想要一个ResponseEntity<String>。所以我之前定义的 MessagingGateway...

@MessagingGateway
public interface Client {

    /**
     * Makes call to (another) Server
     */
    @Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
    String makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}

... 变为(仅 return 值更改):

@MessagingGateway
public interface Client {

    /**
     * Makes call to (another) server
     */
    @Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
    ResponseEntity<String> makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}

现在,如果我们就这样保留它,那么它仍然无法正常工作。我们需要告诉 Spring,不应提取 ResponseBody。我们在集成流程中这样做:


    @Bean
    MessageChannel replyChannel() {
        return MessageChannels.direct("replyChannel").get();
    }
    
    @Bean
    MessageChannel clientChannel() {
        return MessageChannels.direct().get();
    }


    IntegrationFlow clientFlow() {
        final SpelExpressionParser parser = new SpelExpressionParser();
        return IntegrationFlows.from(clientChannel())
                .handle(Http.outboundGateway(parser.parseExpression("headers.url"), restTemplate)
                     .charset("UTF-8") 
                     .extractResponseBody(false) // <-- this is new w/ default being true!
                     .expectedResponseType(ResponseEntity.class)) // <--  this was String.class
                .channel(clientReplyChannel())
                .get();
    }

如果我们把它留在这里,它仍然无法工作。该程序将无法找到合适的 HttpMessageConverter。你需要自己写一个。这完全取决于您 ResponseEntity 的内容。值得庆幸的是,您很可能不需要自己编写整个 HttpMessageConverter。您可以简单地扩展“适当的”一个并告诉它接受 ResponseEntity。我的新 MessageConverter 看起来像这样。

@Component
public class ResponseEntityHttpMessageConverter extends StringHttpMessageConverter {
     
    public ResponseEntityHttpMessageConverter() {
    }

    @Override
    public boolean supports(final Class<?> clazz) {
        return ResponseEntity.class == clazz;
    }

}

Spring 开机应该可以自己捡起来。我相信网上有很多关于此的 material 以防你遇到困难。

提示:只是不要错误地实施HttpMessageConverter<ResponseEntity>。 Spring 将为 MessageConverter 本身解包 ResponseEntity。所以 转换器应该只用于内容 .