4

我有一个使用带有 REST API 的 WebFlux 的反应式 Spring 应用程序。每当用户调用我的 API 时,我都需要调用公开 WSDL 的 SOAP 服务,执行一些操作并返回结果。

如何将此调用与响应式 WebFlux 框架结合到 SOAP 服务?

在我看来,我可以通过 2 种不同的方式做到这一点:

  1. 使用 WebFlux 的 WebClient 构造和发送 SOAP 消息。
  2. 在 Mono / Flux 中使用 WebServiceGatewaySupport 包装同步调用。

第一种方法有我的偏好,但我不知道该怎么做。

此处已提出类似问题: Reactive Spring WebClient - Making a SOAP call,参考这篇博文 ( https://blog.godatadriven.com/jaxws-reactive-client )。但我无法让这个例子起作用。

在 Gradle 插件中使用wsdl2java我可以使用异步方法创建客户端接口,但我不明白如何使用它。使用时,WebServiceGatewaySupport我根本不使用生成的接口或其方法。相反,我调用通用marshalSendAndReceive方法

public class MySoapClient extends WebServiceGatewaySupport {

    public QueryResponse execute() {
        Query query = new ObjectFactory().createQuery();
        // Further create and set the domain object here from the wsdl2java generated classes       
        return (QueryResponse) getWebServiceTemplate().marshalSendAndReceive(query);
    }
}

谁能分享一个从 WebFlux 控制器到进行 SOAP 调用并异步返回的完整示例?我觉得我错过了一些至关重要的东西。

4

3 回答 3

4

我有同样的目标,但没有 WSDL 文件。作为输入,我有定义我应该发送的请求方案的端点和 XSD 文件。这是我的一段代码。

首先让我们定义我们的 SOPA WebClient bean(以避免每次我们想拨打电话时都创建它)

@Bean(name = "soapWebClient")
public WebClient soapWebClient(WebClient.Builder webClientBuilder) {
        String endpoint = environment.getRequiredProperty(ENDPOINT);
        log.info("Initializing SOAP Web Client ({}) bean...", endpoint);

        return webClientBuilder.baseUrl(endpoint)
                               .defaultHeader(CONTENT_TYPE, "application/soap+xml")
                               //if you have any time limitation put them here
                               .clientConnector(getWebClientConnector(SOAP_WEBCLIENT_CONNECT_TIMEOUT_SECONDS, SOAP_WEBCLIENT_IO_TIMEOUT_SECONDS))
                               //if you have any request/response size limitation put them here as well
                               .exchangeStrategies(ExchangeStrategies.builder()
                                                                     .codecs(configurer -> configurer.defaultCodecs()
                                                                                                     .maxInMemorySize(MAX_DATA_BUFFER_SIZE))
                                                                     .build())
                               .build();
}

public static ReactorClientHttpConnector getWebClientConnector(int connectTimeoutSeconds, int ioTimeoutSeconds) {
        TcpClient tcpClient = TcpClient.create()
                                       .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutSeconds * 1000)
                                       .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(ioTimeoutSeconds))
                                                                  .addHandlerLast(new WriteTimeoutHandler(ioTimeoutSeconds)));
        return new ReactorClientHttpConnector(HttpClient.from(tcpClient));
}

现在您可以使用客户端进行 SOAP 调用,如下所示:

@Slf4j
@Component
public class SOAPClient {

    private final WebClient soapWebClient;

    public SOAPClient(@Qualifier("soapWebClient") WebClient soapWebClient) {
        this.soapWebClient = soapWebClient;
    }

    public Mono<Tuple2<HttpStatus, String>> send(String soapXML) {
        return Mono.just("Request:\n" + soapXML)
                   .doOnNext(log::info)
                   .flatMap(xml -> soapWebClient.post()
                                                .bodyValue(soapXML)
                                                .exchange()
                                                .doOnNext(res -> log.info("response status code: [{}]", res.statusCode()))
                                                .flatMap(res -> res.bodyToMono(String.class)
                                                                   .doOnNext(body -> log.info("Response body:\n{}", body))
                                                                   .map(b -> Tuples.of(res.statusCode(), b))
                                                                   .defaultIfEmpty(Tuples.of(res.statusCode(), "There is no data in the response"))))
                   .onErrorResume(ConnectException.class, e -> Mono.just(Tuples.of(SERVICE_UNAVAILABLE, "Failed to connect to server"))
                                                                   .doOnEach(logNext(t2 -> log.warn(t2.toString()))))
                   .onErrorResume(TimeoutException.class, e -> Mono.just(Tuples.of(GATEWAY_TIMEOUT, "There is no response from the server"))
                                                                   .doOnEach(logNext(t2 -> log.warn(t2.toString()))));
    }

}

这里要提到的重要一点是,您soapXML显然应该采用 SOAP 协议定义的格式。更具体地说,消息至少应该以soap:Envelope标签开头和结尾,并包含里面的所有其他数据。此外,请注意您将要使用的 SOAP 协议版本,因为它定义了允许在信封中使用哪些标签,哪些标签不能使用。我的是1.1这里是它的规范 https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494

干杯

于 2020-03-16T16:28:56.970 回答
1

在经历了很多痛苦和麻烦之后,我找到了解决这个问题的好方法。由于提供了一个wsdl文件,你应该访问这个站点::https ://www.wsdl-analyzer.com 你可以输入一个wsdl文件并查看soap服务的所有操作。找到要调用的所需操作后,单击它,它将在 xml 中显示示例请求。某种方式,您必须生成此 xml 才能发出请求。有很多方法可以做到这一点,有些方法比其他方法更复杂。我发现手动序列化效果很好,而且确实比使用库更容易。

假设您有这样的操作请求:

   <s11:Envelope> 
      <s11:body> 
        <s11:operation> 
            <ns:username>username</ns:username>
            <ns:password>password</ns:password> 
        </sll:operation> 
     </s11:body> 
  <s11:Envelope> 

那么你会生成

public String gePayload(String username, String password) { 
   StringBuilder payload = new Stringbuilder(); 
   payload.append("<s11:Envelope><s11:body><s11:operation>");
   payload.append("<ns:username>");
   payload.append(username); 
   payload.append("</ns:username>"); 
   payload.append("<ns:password>");
   payload.append(password); 
   payload.append("</ns:password>");
   payload.append("</s11:operation></s11:body></s11:Envelope>");    
   return payload.toString() 
}   

然后网络调用

   public String callSoap(string payload) {
       Webclient webclient = Webclient.builder() 
                             // make sure the path is absolute 
                             .baseUrl(yourEndPoint) 
                             .build() 


       return WebClient.post()
                .contentType(MediaType.TEXT_XML)
                .bodyValue(payload)
                .retrieve()
                .bodyToMono(String.class)
                .block(); 

    } 

指定内容类型是 xml 并且该类返回一个字符串是很重要的。web Flux 不能轻易地将 xml 转换为用户定义的类。所以你必须执行手动解析。您可以指定 jaxb2xmlEncoders 和 jaxb2xmlDecoders 来结束编码/解码特定的类,但我发现这很复杂。有效负载必须与 wsdl 分析器生成的请求格式相匹配,并且让编码器/解码器与该格式相匹配可以是它自己的任务。如果需要,您可以进一步研究这些编码器,但这种方法会起作用。

于 2021-07-23T17:21:28.787 回答
0

我一周都面临同样的问题,但仍然找不到最佳解决方案。如果您想测试 WebClient,您只需使用 SOAP Envelope 请求发布一个字符串。像这样的东西:

    String _request = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\n" +
              "<soap:Body>\n" +
               "<request>\n" +
                  "<Example>blabla</Example>\n" +
               "</request>\n" +
              "</soap:Body>\n" +
            "</soap:Envelope>";

    WebClient webClient = WebClient.builder().baseUrl("http://example-service").build();

    Mono<String> stringMono = webClient.post()
            .uri("/example-port")
            .body(BodyInserters.fromObject(_request))
            .retrieve()
            .bodyToMono(String.class);

    stringMono.subscribe(System.out::println);

问题是您需要弄清楚如何将整个 SOAP 信封(请求和响应)序列化为字符串。这只是一个例子 - 不是解决方案。

于 2020-02-21T07:30:41.227 回答