1、什么是 WebClient

Spring WebFlux包括WebClient对HTTP请求的响应式,非阻塞式。WebFlux客户端和服务器依靠相同的非阻塞编解码器对请求和响应内容进行编码和解码。

WebClient内部委托给HTTP客户端库。默认情况下,WebClient使用Reactor Netty,内置了对Jetty反应式HttpClient的支持,其他的则可以通过插入ClientHttpConnector。

这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》PDF,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从公众号 【技术自由圈】获取

方式一:通过静态工厂方法创建响应式WebClient实例

创建最简单方法WebClient是通过静态工厂方法之一:

  • WebClient.create()
  • WebClient.create(String baseUrl)

eg:一个使用Webclient(响应式HttpClient) 的Rest请求示例

package com.crazymaker.springcloud.reactive.rpc.mock;

import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.IOException;

public class WebClientDemo
{

  /**
     * 测试用例
     */
  @Test
  public void testCreate() throws IOException
  {

    //响应式客户端
    WebClient client = null;

    WebClient.RequestBodySpec request = null;

    String baseUrl = "http://crazydemo.com:7700/demo-provider/";
    client = WebClient.create(baseUrl);

    /**
         * 是通过 WebClient 组件构建请求
         */
    String restUrl = baseUrl + "api/demo/hello/v1";
    request = client
    // 请求方法
      .method(HttpMethod.GET)
    // 请求url 和 参数
    //                .uri(restUrl, params)
      .uri(restUrl)
    // 媒体的类型
      .accept(MediaType.APPLICATION_JSON);

    .... 省略其他源码
  }    
}

上面的方法使用 HttpClient 具有默认设置的Reactor Netty ,并且期望
io.projectreactor.netty:reactor-netty在类路径上。

您还可以使用WebClient.builder()其他选项:

  • uriBuilderFactory:自定义UriBuilderFactory用作基本URL(BaseUrl)。
  • defaultHeader:每个请求的标题。
  • defaultCookie:针对每个请求的Cookie。
  • defaultRequest:Consumer自定义每个请求。
  • filter:针对每个请求的客户端过滤器。
  • exchangeStrategies:HTTP消息读取器/写入器定制。
  • clientConnector:HTTP客户端库设置。

方式二:使用builder(构造者)创建响应式WebClient实例

//方式二:使用builder(构造者)创建响应式WebClient实例
client = WebClient.builder()
  .baseUrl("https://api.github.com")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
  .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
  .build();

发送请求

get请求

/**
  * 测试用例
  */
@Test
public void testGet() throws IOException
{
  String restUrl = baseUrl + "api/demo/hello/v1";

  Mono<String> resp = WebClient.create()
    .method(HttpMethod.GET)
    .uri(restUrl)
    .cookie("token", "jwt_token")
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .retrieve().bodyToMono(String.class);

  // 订阅结果
  resp.subscribe(responseData ->
                 {
    log.info(responseData.toString());
}, e ->
  {
  log.info("error:" + e.getMessage());
});
//主线程等待, 一切都是为了查看到异步结果
ThreadUtil.sleepSeconds(1000);
}

方式三:WebClient实例克隆

一旦建立,WebClient实例是不可变的。但是,您可以克隆它并构建修改后的副本,而不会影响原始实例,如以下示例所示:

WebClient client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

抽取公用的baseUrl

如果要访问的URL都来自同一个应用,只是对应不同的URL地址,这个时候可以把公用的部分抽出来定义为baseUrl,然后在进行WebClient请求的时候只指定相对于baseUrl的URL部分即可。
这样的好处是你的baseUrl需要变更的时候可以只要修改一处即可。

下面的代码在创建WebClient时定义了baseUrl为http://localhost:8081,在发起Get请求时指定了URL为/user/1,而实际上访问的URL是
http://localhost:8081/user/1。

String baseUrl = "http://localhost:8081";

WebClient webClient = WebClient.create(baseUrl);

Mono<User> mono = webClient.get().uri("user/{id}", 1).retrieve().bodyToMono(User.class);

2、请求提交

发送get请求

/**
  * 测试用例: 发送get请求
  */
@Test
public void testGet() throws IOException
{
  String restUrl = baseUrl + "api/demo/hello/v1";

  Mono<String> resp = WebClient.create()
    .method(HttpMethod.GET)
    .uri(restUrl)
    .cookie("token", "jwt_token")
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .retrieve().bodyToMono(String.class);

  // 订阅结果
  resp.subscribe(responseData ->
                 {
    log.info(responseData.toString());
}, e ->
  {
  log.info("error:" + e.getMessage());
});
//主线程等待, 一切都是为了查看到异步结果
ThreadUtil.sleepSeconds(1000);
}

提交Json Body

请求体的 mime类型
application/x-www-form-urlencoded

Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(personMono, Person.class)
        .retrieve()
        .bodyToMono(Void.class);

例子:

/**
  * 测试用例: 发送post 请求 mime为 application/json
  */
@Test
public void testJSONParam(){
  String restUrl = baseUrl + "api/demo/post/demo/v2";
  LoginInfoDTO dto=new LoginInfoDTO("lisi","123456");
  Mono<LoginInfoDTO> personMono =Mono.just(dto);

  Mono<String> resp = WebClient.create().post()
    .uri(restUrl)
    .contentType(MediaType.APPLICATION_JSON)
  //                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
    .body(personMono,LoginInfoDTO.class)
    .retrieve().bodyToMono(String.class);

  // 订阅结果
  resp.subscribe(responseData ->
                 {
    log.info(responseData.toString());
}, e ->
  {
  log.info("error:" + e.getMessage());
});
//主线程等待, 一切都是为了查看到异步结果
ThreadUtil.sleepSeconds(1000);
}

提交表单

请求体的 mime类型
application/x-www-form-urlencoded

MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
  .uri("/path", id)
  .bodyValue(formData)
  .retrieve()
  .bodyToMono(Void.class);

或者

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
  .uri("/path", id)
  .body(fromFormData("k1", "v1").with("k2", "v2"))
  .retrieve()
  .bodyToMono(Void.class);

例子:

/**
  * 提交表单  mime类型 application/x-www-form-urlencoded
  *
  * @return RestOut
  */
//    @PostMapping("/post/demo/v1")
@RequestMapping(value = "/post/demo/v1", method = RequestMethod.POST)
@ApiOperation(value = "post请求演示")
public RestOut<LoginInfoDTO> postDemo(@RequestParam String username, @RequestParam String password)
{
/**
  * 直接返回
  */
  LoginInfoDTO dto = new LoginInfoDTO();
  dto.setUsername(username);
  dto.setPassword(password);
  return RestOut.success(dto).setRespMsg("body的内容回显给客户端");
}

上传文件

请求体的 mime类型"multipart/form-data";

例子:

@Test
public void testUploadFile()
{
  String restUrl = baseUrl + "/api/file/upload/v1";

  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(MediaType.IMAGE_PNG);
  HttpEntity<ClassPathResource> entity = 
    new HttpEntity<>(new ClassPathResource("logback-spring.xml"), headers);
  MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
  parts.add("file", entity);
  Mono<String> resp = WebClient.create().post()
    .uri(restUrl)
    .contentType(MediaType.MULTIPART_FORM_DATA)
    .body(BodyInserters.fromMultipartData(parts))
    .retrieve().bodyToMono(String.class);
  log.info("result:{}", resp.block());
}

3、错误处理

  • 可以使用onStatus根据status code进行异常适配
  • 可以使用doOnError异常适配
  • 可以使用onErrorReturn返回默认值
/**
  * 测试用例: 错误处理
  */
@Test
public void testFormParam4xx()
{
  WebClient webClient = WebClient.builder()
    .baseUrl("https://api.github.com")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
    .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
    .build();
  WebClient.ResponseSpec responseSpec = webClient.method(HttpMethod.GET)
    .uri("/user/repos?sort={sortField}&direction={sortDirection}",
         "updated", "desc")
    .retrieve();
  Mono<String> mono = responseSpec
    .onStatus(e -> e.is4xxClientError(), resp ->
              {
    log.error("error:{},msg:{}", resp.statusCode().value(), resp.statusCode().getReasonPhrase());
  return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
  .bodyToMono(String.class)
    .doOnError(WebClientResponseException.class, err ->
               {
    log.info("ERROR status:{},msg:{}", err.getRawStatusCode(), err.getResponseBodyAsString());
throw new RuntimeException(err.getMessage());
})
  .onErrorReturn("fallback");
String result = mono.block();
System.out.print(result);
}

4、响应解码

有两种对响应的处理方法:

  • retrieve
  • retrieve方法是直接获取响应body。
  • exchange
  • 但是,如果需要响应的头信息、Cookie等,可以使用exchange方法,exchange方法可以访问整个ClientResponse。

异步转同步

由于响应的得到是异步的,所以都可以调用 block 方法来阻塞当前程序,等待获得响应的结果。

4.1 retrieve

该retrieve()方法是获取响应主体并对其进行解码的最简单方法。以下示例显示了如何执行此操作:

Mono<Person> result = client.get()
  .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .onStatus(HttpStatus::is4xxClientError, response -> ...)
  .onStatus(HttpStatus::is5xxServerError, response -> ...)
  .bodyToMono(Person.class);

默认情况下,4XX或5xx状态代码的应答导致
WebClientResponseException或它的HTTP状态的具体子类之一,比如 WebClientResponseException.BadRequest,WebClientResponseException.NotFound和其他人。您还可以使用该onStatus方法来自定义所产生的异常

4.2 exchange()

该exchange()方法比该方法提供更多的控制retrieve。以下示例等效于retrieve()但也提供对的访问ClientResponse:

ono<ResponseEntity<Person>> result = client.get()
  .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
  .exchange()
  .flatMap(response -> response.toEntity(Person.class));

请注意(与不同retrieve()),对于exchange(),没有4xx和5xx响应的自动错误信号。您必须检查状态码并决定如何进行。
与相比retrieve(),当使用时exchange(),应用程序有责任使用任何响应内容,而不管情况如何(成功,错误,意外数据等),否则会导致内存泄漏.

eg: 下面的例子,使用exchange 获取ClientResponse,并且进行状态位的判断:

/**
  * 测试用例: Exchange
  */
@Test
public void testExchange()
{
  String baseUrl = "http://localhost:8081";
  WebClient webClient = WebClient.create(baseUrl);

  MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
  map.add("username", "u123");
  map.add("password", "p123");

  Mono<ClientResponse> loginMono = webClient.post().uri("login").syncBody(map).exchange();
  ClientResponse response = loginMono.block();
  if (response.statusCode() == HttpStatus.OK) {
    Mono<RestOut> resultMono = response.bodyToMono(RestOut.class);
    resultMono.subscribe(result -> {
      if (result.isSuccess()) {
      ResponseCookie sidCookie = response.cookies().getFirst("sid");
    Mono<LoginInfoDTO> dtoMono = webClient.get().uri("users").cookie(sidCookie.getName(), sidCookie.getValue()).retrieve().bodyToMono(LoginInfoDTO.class);
    dtoMono.subscribe(System.out::println);
  }
});
}
}

response body 转换响应流

将response body 转换为对象/集合

  • bodyToMono
  • 如果返回结果是一个Object,WebClient将接收到响应后把JSON字符串转换为对应的对象,并通过Mono流弹出。
  • bodyToFlux
  • 如果响应的结果是一个集合,则不能继续使用bodyToMono(),应该改用bodyToFlux(),然后依次处理每一个元素,并通过Flux流弹出。

5、请求和响应过滤

WebClient也提供了Filter,对应于
org.springframework.web.reactive.function.client.ExchangeFilterFunction接口,其接口方法定义如下。

Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)

在进行拦截时可以拦截request,也可以拦截response。

增加基本身份验证:

WebClient webClient = WebClient.builder()
  .baseUrl(GITHUB_API_BASE_URL)
  .defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
  .filter(ExchangeFilterFunctions
          .basicAuthentication(username, token))
  .build();

使用过滤器过滤response

@Test
void filter() {
  Map<String, Object> uriVariables = new HashMap<>();
  uriVariables.put("p1", "var1");
  uriVariables.put("p2", 1);
  WebClient webClient = WebClient.builder().baseUrl("http://www.ifeng.com")
    .filter(logResposneStatus())
    .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
    .build();
  Mono<String> resp1 = webClient
    .method(HttpMethod.GET)
    .uri("/")
    .cookie("token","xxxx")
    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .retrieve().bodyToMono(String.class);
  String re=  resp1.block();
  System.out.print("result:" +re);

}

private ExchangeFilterFunction logResposneStatus() {
  return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
    log.info("Response Status {}", clientResponse.statusCode());
  return Mono.just(clientResponse);
});
}

使用过滤器记录请求日志

WebClient webClient = WebClient.builder()
  .baseUrl(GITHUB_API_BASE_URL)
  .defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
  .filter(ExchangeFilterFunctions
          .basicAuthentication(username, token))
  .filter(logRequest())
  .build();

private ExchangeFilterFunction logRequest() {
  return (clientRequest, next) -> {
    logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
  clientRequest.headers()
    .forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
  return next.exchange(clientRequest);
};
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐