Spring Integration outbound-gateway 添加 POST 参数异常
结论
需要同时修正三个问题:
- 请求必须使用
Content-Type: application/json。同时,为 HTTP outbound gateway 配置MappingJackson2HttpMessageConverter,由它将Map序列化为 JSON。 - Transformer 必须接在 outbound gateway 的响应通道之后,不能与 outbound gateway 同时监听
PaymentAuthRequestChannel。否则 Transformer 收到的是原始HashMap,其中没有statusCode。 - 如果 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会绕过证书链与主机名校验,只适合受控的测试环境。生产环境应使用可信证书库,并保留正常的主机名验证。
备注:内容仅供参考。
