如何基于SOAP消息体字段配置Spring Cloud Gateway的RouteLocator路由?
好问题!Spring Cloud Gateway确实没有内置针对SOAP消息体的路由断言工厂,不过我们可以通过自定义RoutePredicateFactory来实现这个需求。下面是一步步的实现方案:
实现基于SOAP消息体字段的Spring Cloud Gateway路由
1. 自定义SOAP Body路由断言工厂
首先,我们需要创建一个自定义的RoutePredicateFactory,用来解析SOAP请求体并提取指定字段的值,当和预期值匹配时触发路由。
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.util.Map; import java.util.function.Predicate; @Component public class SoapBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<SoapBodyRoutePredicateFactory.Config> { public SoapBodyRoutePredicateFactory() { super(Config.class); } @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { // 缓存请求体,避免流式请求体被读取后无法重复使用 return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> { return serverHttpRequest.getBody() .collectList() .map(dataBuffers -> { // 将DataBuffer集合转换为字节数组 byte[] bytes = new byte[dataBuffers.stream().mapToInt(dataBuffer -> dataBuffer.readableByteCount()).sum()]; int offset = 0; for (var buffer : dataBuffers) { int readable = buffer.readableByteCount(); buffer.read(bytes, offset, readable); offset += readable; buffer.release(); } try { // 启用命名空间支持的XML解析器 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new ByteArrayInputStream(bytes)); // 配置XPath的命名空间上下文,匹配SOAP消息中的命名空间 XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(new NamespaceContext() { @Override public String getNamespaceURI(String prefix) { if ("ser".equals(prefix)) { return "http://service.viettel.com/"; } else if ("soapenv".equals(prefix)) { return "http://schemas.xmlsoap.org/soap/envelope/"; } return null; } @Override public String getPrefix(String namespaceURI) { return null; } @Override public java.util.Iterator<?> getPrefixes(String namespaceURI) { return null; } }); // 提取目标节点的文本值并和预期值对比 String nodeValue = xpath.evaluate(config.getNodeXpath(), doc); return config.getExpectedValue().equals(nodeValue); } catch (Exception e) { // 解析失败或节点不存在时,不匹配当前路由 return false; } }); }); }; } // 配置类,用于接收路由配置中的参数 public static class Config { private String nodeXpath; private String expectedValue; public String getNodeXpath() { return nodeXpath; } public void setNodeXpath(String nodeXpath) { this.nodeXpath = nodeXpath; } public String getExpectedValue() { return expectedValue; } public void setExpectedValue(String expectedValue) { this.expectedValue = expectedValue; } } }
2. 配置路由规则
接下来在application.yml中配置路由,使用我们自定义的断言工厂:
spring: cloud: gateway: routes: # 匹配msg=John的SOAP请求,路由到指定服务 - id: soap-john-route uri: http://john-specific-service:8080 predicates: - name: SoapBody args: nodeXpath: "/soapenv:Envelope/soapenv:Body/ser:sayHello/ser:msg/text()" expectedValue: "John" # 默认SOAP请求路由 - id: default-soap-route uri: http://default-soap-service:8080 predicates: - Path=/soap/**
3. 关键注意事项
- 命名空间处理:SOAP消息大多包含命名空间,必须在XPath的命名空间上下文中正确配置,否则无法定位到目标节点。你需要根据自己的SOAP消息命名空间调整代码中的配置。
- 请求体缓存:Spring Cloud Gateway的请求体是流式的,必须用
ServerWebExchangeUtils.cacheRequestBody缓存后才能重复读取,避免后续流程无法获取请求体。 - 性能优化:DOM解析XML会带来一定性能开销,如果你的SOAP请求量极大,可以考虑使用更轻量的StAX解析方式替代。
- 请求大小限制:如果SOAP消息体过大,需要调整Gateway的内存缓存上限,比如配置
spring.cloud.gateway.httpclient.max-in-memory-size=10MB。
4. 测试验证
用curl发送示例SOAP请求测试路由效果:
curl -X POST \ http://your-gateway-host:8080/soap \ -H 'Content-Type: text/xml' \ -d '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://service.viettel.com/"> <soapenv:Header/> <soapenv:Body> <ser:sayHello> <msg>John</msg> </ser:sayHello> </soapenv:Body> </soapenv:Envelope>'
当msg字段为John时,请求会路由到指定服务;其他值则会走默认路由。
内容的提问来源于stack exchange,提问作者Cuong T.Tran




