PAYATHON 2026

Spring Integration outbound-gateway 添加 POST 参数异常

支付小周

结论

需要同时修正三个问题:

  1. 请求必须使用 Content-Type: application/json。同时,为 HTTP outbound gateway 配置 MappingJackson2HttpMessageConverter,由它将 Map 序列化为 JSON。
  2. Transformer 必须接在 outbound gateway 的响应通道之后,不能与 outbound gateway 同时监听 PaymentAuthRequestChannel。否则 Transformer 收到的是原始 HashMap,其中没有 statusCode。
  3. 如果 Gateway 方法需要返回 HttpStatus,可以设置 extract-response-body="false",让 outbound gateway 返回 ResponseEntity<?>,再通过 payload.statusCode 提取 HttpStatus。

数据流应当是:

requestGateway
    -> PaymentAuthRequestChannel
    -> HTTP outbound-gateway
    -> PaymentAuthHttpReplyChannel
    -> transformer: payload.statusCode
    -> Gateway 的 replyChannel

推荐配置

如果没有实际的路由需求,可以先移除 recipient-list-router,让 Gateway 直接将请求发送到 HTTP outbound gateway。

<!-- Request channel -->
<int:channel id="PaymentAuthRequestChannel"/>

<!-- HTTP response enters this channel -->
<int:channel id="PaymentAuthHttpReplyChannel"/>

<!-- Messaging Gateway -->
<int:gateway id="requestGateway"
             service-interface="org.springframework.integration.samples.http.MultipartRequestGateway"
             default-request-channel="PaymentAuthRequestChannel">
    <int:default-header name="Api-Key" value="XYZ"/>
    <int:default-header name="Accept" value="application/json;v=3"/>
    <int:default-header name="Content-Type" value="application/json"/>
</int:gateway>

<!-- Jackson JSON converter -->
<bean id="jsonMessageConverter"
      class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>

<!-- HTTP outbound gateway -->
<int-http:outbound-gateway
        id="authOutboundGateway"
        request-channel="PaymentAuthRequestChannel"
        reply-channel="PaymentAuthHttpReplyChannel"
        request-factory="sslFactory"
        header-mapper="headerMapper"
        url="https://XX.XX.XX.XXX:11210/int-tsys-cc-payment-auth-web/integration/tsys/credit-cards/payments/xyz"
        http-method="POST"
        message-converters="jsonMessageConverter"
        expected-response-type="java.lang.String"
        extract-request-payload="true"
        extract-response-body="false"/>

<!-- ResponseEntity<?> -> HttpStatus -->
<int:transformer input-channel="PaymentAuthHttpReplyChannel"
                 expression="payload.statusCode"/>

<!-- HTTPS request factory -->
<bean id="sslFactory"
      class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
    <constructor-arg ref="httpClient"/>
</bean>

<bean id="httpClient"
      class="org.springframework.integration.samples.http.HttpClientFactory"/>

<!-- HTTP header mapper -->
<bean id="headerMapper"
      class="org.springframework.integration.http.support.DefaultHttpHeaderMapper">
    <property name="inboundHeaderNames" value="*"/>
    <property name="outboundHeaderNames"
              value="Api-Key, Accept, Content-Type"/>
    <property name="userDefinedHeaderPrefix" value=""/>
</bean>

这里没有为 Transformer 显式配置 output-channel。Spring Integration 会继续使用原消息中的 replyChannel,把转换后的 HttpStatus 返回给调用 Messaging Gateway 的线程。

接口可以保持不变:

import java.util.Map;
import org.springframework.http.HttpStatus;

public interface MultipartRequestGateway {

    HttpStatus postMultipartRequest(Map<String, Object> request);
}

调用代码中的字段名必须与 SOAP-UI 验证成功的 JSON 一致:

Map<String, Object> requestMap = new HashMap<String, Object>();
requestMap.put("cardNumber", "4444333322221111");
requestMap.put("currencyCode", "USD");
requestMap.put("paymentAmount", "10.00");

HttpStatus status =
        requestGateway.postMultipartRequest(requestMap);

此前示例使用了 cardNo 和 amount,但服务端期望的是 cardNumber 和 paymentAmount。即使 JSON 已经成功序列化,字段名不一致仍可能导致服务端校验失败。

最终请求体类似于:

{
  "cardNumber": "4444333322221111",
  "currencyCode": "USD",
  "paymentAmount": "10.00"
}

为什么会出现这些异常

String cannot be cast to Map

application/x-www-form-urlencoded 对应的转换器通常要求使用类似 MultiValueMap<String, ?> 的数据结构,而不是任意字符串或已经转换好的 JSON 字符串。

目标接口实际接收 JSON,因此不应按表单请求处理,也不需要沿用 multipart 示例中的请求结构。

415 Unsupported Media Type

日志表明请求被写成了:

application/x-www-form-urlencoded

SOAP-UI 中成功的请求使用的是:

Content-Type: application/json

服务端返回 415,说明它明确拒绝了当前请求的媒体类型。将 Map 改为 MultiValueMap 只能让表单转换器正常工作,无法满足服务端对 JSON 的要求。

no suitable HttpMessageConverter

设置 Content-Type: application/json 只是声明请求格式,并不会自动提供 JSON 序列化能力。

要将 HashMap 写成 JSON,需要使用:

org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

运行时还必须存在与当前 Spring 版本兼容的 Jackson 依赖。较老的 Spring 版本可能使用 Jackson 1 或 Jackson 2,具体取决于项目版本。从异常中的类名判断,当前配置需要的是 MappingJackson2HttpMessageConverter。

ResponseEntity<?> 无法转换成 String

设置:

extract-response-body="false"

之后,HTTP outbound gateway 的响应 payload 是:

ResponseEntity<?>

它不能自动转换为 String 或 HttpStatus。可以让接口直接返回 ResponseEntity<?>,也可以在响应通道上添加 Transformer,从中提取状态码。

Transformer 在 HashMap 上找不到 statusCode

原配置将 Transformer 的输入通道设为:

input-channel="PaymentAuthRequestChannel"

这个通道传递的是请求,payload 就是调用方传入的 HashMap:

{
    cardNumber=...,
    currencyCode=...,
    paymentAmount=...
}

此时执行 payload.statusCode,相当于尝试调用:

requestMap.getStatusCode()

因此必然失败。

此外,在同一个 DirectChannel 上同时挂载 outbound gateway 和 Transformer,并不表示系统会先调用 HTTP,再执行 Transformer。它们只是同一个请求通道上的两个消费者,不能用这种方式定义处理顺序。

Transformer 应当监听 outbound gateway 的 reply-channel:

<int-http:outbound-gateway
        request-channel="PaymentAuthRequestChannel"
        reply-channel="PaymentAuthHttpReplyChannel"
        .../>

<int:transformer
        input-channel="PaymentAuthHttpReplyChannel"
        expression="payload.statusCode"/>

另一种方案:直接返回 ResponseEntity<?>

如果调用方除了状态码,还需要读取响应头或响应体,可以删除 Transformer,让 Gateway 方法直接返回 ResponseEntity<?>:

import java.util.Map;
import org.springframework.http.ResponseEntity;

public interface MultipartRequestGateway {

    ResponseEntity<?> postMultipartRequest(Map<String, Object> request);
}

继续保留:

extract-response-body="false"

但 outbound gateway 不再将响应固定发送给 Transformer:

<int-http:outbound-gateway
        request-channel="PaymentAuthRequestChannel"
        request-factory="sslFactory"
        header-mapper="headerMapper"
        url="https://XX.XX.XX.XXX:11210/int-tsys-cc-payment-auth-web/integration/tsys/credit-cards/payments/xyz"
        http-method="POST"
        message-converters="jsonMessageConverter"
        expected-response-type="java.lang.String"
        extract-request-payload="true"
        extract-response-body="false"/>

调用方自行读取状态码:

ResponseEntity<?> response =
        requestGateway.postMultipartRequest(requestMap);

HttpStatus status = response.getStatusCode();

如果只关心 201 Created,返回 HttpStatus 并使用 Transformer 更简单。如果还要读取 Location、其他响应头或响应体,直接返回 ResponseEntity<?> 更合适。

也可以从消息头取得状态码

当 extract-response-body="true" 时,响应 payload 通常是响应体,HTTP 状态码则位于 Spring Integration 的响应消息头中。此时可以改用:

<int:transformer input-channel="PaymentAuthHttpReplyChannel"
                 expression="headers['http_statusCode']"/>

这种方式不受响应体是否为空的影响。不过,不同版本的 Spring Integration 对 HTTP 头常量和响应提取行为可能有所不同。当前配置已经观察到了 ResponseEntity<?>,因此使用下面的组合更直接:

extract-response-body="false"
expression="payload.statusCode"

注意事项

  • expected-response-type 表示响应体类型,不是 HTTP 状态码类型,因此不应配置为:

    expected-response-type="org.springframework.http.HttpStatus"
  • 即使 201 Created 没有响应体,仍可通过 ResponseEntity.getStatusCode() 或 http_statusCode 消息头取得状态码。

  • 4xx 和 5xx 默认通常会被 RestTemplate 的错误处理器转换为异常,而不是作为普通的 HttpStatus 返回。如果业务需要将错误状态也作为普通返回值处理,应为底层 RestTemplate 配置自定义 ResponseErrorHandler。

  • Accept 建议与已经验证成功的请求完全一致:

    Accept: application/json;v=3

    不要无意间加入空格,以免服务端将其解析为不同的媒体类型参数。

  • 示例中的“信任所有证书”和 ALLOW_ALL_HOSTNAME_VERIFIER 会绕过证书链与主机名校验,只适合受控的测试环境。生产环境应使用可信证书库,并保留正常的主机名验证。

备注:内容仅供参考。