Spring WebFlux,如何调试我的 WebClient POST 交换?

Posted

技术标签:

【中文标题】Spring WebFlux,如何调试我的 WebClient POST 交换?【英文标题】:Spring WebFlux, how can I debug my WebClient POST exchange? 【发布时间】:2018-02-09 16:52:15 【问题描述】:

我无法理解在构建 WebClient 请求时我做错了什么。我想了解实际的 HTTP 请求是什么样的。 (例如,将原始请求转储到控制台)

POST /rest/json/send HTTP/1.1
Host: emailapi.dynect.net
Cache-Control: no-cache
Postman-Token: 93e70432-2566-7627-6e08-e2bcf8d1ffcd
Content-Type: application/x-www-form-urlencoded

apikey=ABC123XYZ&from=example%40example.com&to=customer1%40domain.com&to=customer2%40domain.com&to=customer3%40domain.com&subject=New+Sale+Coming+Friday&bodytext=You+will+love+this+sale.

我正在使用 Spring5 的反应式工具来构建 API。我有一个实用程序类,它将使用 Dyn 的电子邮件 API 发送电子邮件。我想使用新的 WebClient 类来完成这个(org.springframework.web.reactive.function.client.WebClient

以下命令取自:https://help.dyn.com/email-rest-methods-api/sending-api/#postsend

curl --request POST "https://emailapi.dynect.net/rest/json/send" --data "apikey=ABC123XYZ&from=example@example.com&to=customer1@domain.com&to=customer2@domain.com&to=customer3@domain.com&subject=New Sale Coming Friday&bodytext=You will love this sale."

当我使用实际值在 curl 中进行调用时,电子邮件会正确发送,所以我觉得我的请求生成错误。

我的发送命令

public Mono<String> send( DynEmailOptions options )

    WebClient webClient = WebClient.create();
    HttpHeaders headers = new HttpHeaders();
    // this line causes unsupported content type exception :(
    // headers.setContentType( MediaType.APPLICATION_FORM_URLENCODED );
    Mono<String> result = webClient.post()
        .uri( "https://emailapi.dynect.net/rest/json/send" )
        .headers( headers )
        .accept( MediaType.APPLICATION_JSON )
        .body( BodyInserters.fromObject( options ) )
        .exchange()
        .flatMap( clientResponse -> clientResponse.bodyToMono( String.class ) );
    return result;

我的 DynEmailOptions 类

import java.util.Collections;
import java.util.Set;

public class DynEmailOptions

    public String getApikey()
    
        return apiKey_;
    

    public Set<String> getTo()
    
        return Collections.unmodifiableSet( to_ );
    

    public String getFrom()
    
        return from_;
    

    public String getSubject()
    
        return subject_;
    

    public String getBodytext()
    
        return bodytext_;
    

    protected DynEmailOptions(
        String apiKey,
        Set<String> to,
        String from,
        String subject,
        String bodytext
    )
    
        apiKey_ = apiKey;
        to_ = to;
        from_ = from;
        subject_ = subject;
        bodytext_ = bodytext;
    

    private Set<String> to_;
    private String from_;
    private String subject_;
    private String bodytext_;
    private String apiKey_;

【问题讨论】:

【参考方案1】:

您目前正在尝试“按原样”序列化请求正文,而不使用正确的BodyInserter

在这种情况下,我认为您应该将您的 DynEmailOptions 对象转换为 MultiValueMap&lt;String, String&gt; 然后:

MultiValueMap<String, String> formData = ...
Mono<String> result = webClient.post()
                .uri( "https://emailapi.dynect.net/rest/json/send" )
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .accept( MediaType.APPLICATION_JSON )
                .body( BodyInserters.fromFormData(formData))
                .retrieve().bodyToMono(String.class);

【讨论】:

嗨,Brian,我对问题的调试部分感兴趣,类似于 apaches HttpClient,能够转储传递的原始数据非常方便。 Spring的WebClient中是否有这样的可能? 你好,我也很好奇是否有办法转储正在传递的原始数据(主要是请求的主体,如果存在)。 我认为 logging.level.reactor=debug 应该可以解决问题 - 然后至少你会看到发布请求【参考方案2】:

问题是关于调试 WebClient POST。我在callicoder.com 中找到了很大的帮助。

关键是在WebClient中添加一个过滤器。过滤器允许轻松访问请求和响应。对于请求和响应,您可以访问方法、URL、标头和其他内容。但是,您无法访问正文。我希望我错了,但实际上,只有一个 body() 方法来设置身体。

在这里我不得不抱怨 WebClient POST 的怪异行为。有时,它不会立即获得 4XX 响应,而是永远阻塞。有时,它会给出 501 响应。我的建议是尝试使用 LinkedMultiValueMap 来承载正文,避免使用纯字符串或 java.util.Map。

这是我的示例代码,以 GitHub V3 API 为例:

@Bean
public WebClient client() 
    return WebClient.builder()
        .baseUrl("https://api.github.com")
        .defaultHeader("User-Agent", "Spring-boot WebClient")
        .filter(ExchangeFilterFunctions.basicAuthentication("YOUR_GITHUB_USERNAME", "YOUR_GITHUB_TOKEN"))
        .filter(printlnFilter).build();

ExchangeFilterFunction printlnFilter= new ExchangeFilterFunction() 
    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) 
        System.out.println("\n\n" + request.method().toString().toUpperCase() + ":\n\nURL:"
                + request.url().toString() + ":\n\nHeaders:" + request.headers().toString() + "\n\nAttributes:"
                + request.attributes() + "\n\n");

        return next.exchange(request);
    
;
//In some method:
String returnedJSON = client.post().uri(builder->builder.path("/user/repos").build())
                .contentType(MediaType.APPLICATION_JSON)
                .syncBody(new LinkedMultiValueMap<String, String>()
                    put("name", "tett");
                )
                .retrieve()
                .bodyToMono(String.class)
                .block(Duration.ofSeconds(3))

您会看到如下内容:

2018-04-07 12:15:57.823  INFO 15448 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8084
2018-04-07 12:15:57.828  INFO 15448 --- [           main] c.e.w.WebclientDemoApplication           : Started WebclientDemoApplication in 3.892 seconds (JVM running for 8.426)


POST:

URL:https://api.github.com/user/repos:

Headers:Content-Type=[application/json], User-Agent=[Spring-boot WebClient], Authorization=[Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]

Attributes:

有两点需要注意: 1.过滤器的顺序很重要。交换这 2 个过滤器,身份验证标头将不包括在内。 2. 过滤器实际上适用于通过此 WebClient 实例的所有请求。

https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/ 很有帮助,也许您应该阅读并下载他的示例代码。

【讨论】:

问题是如何打印出请求正文,因为BodyInserter 似乎没有 toString()

以上是关于Spring WebFlux,如何调试我的 WebClient POST 交换?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Webflux / Reactor Netty Web 应用程序中执行阻塞调用

如何在 webflux 中调试?

传统的Spring Web MVC如何像WebFlux一样异步处理请求?

Spring WebFlux 教程:如何构建反应式 Web 应用程序

新一代Spring Web框架WebFlux!

Spring WebFlux创建了无阻塞线程池