推荐: 地表最强 开发环境 系列

工欲善其事 必先利其器
地表最强 开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制作详解)
地表最强 热部署:java SpringBoot SpringCloud 热部署 热加载 热调试
地表最强 发请求工具(再见吧, PostMan ):IDEA HTTP Client(史上最全)
地表最强 PPT 小工具: 屌炸天,像写代码一样写PPT
无编程不创客,无编程不创客,一大波编程高手正在疯狂创客圈交流、学习中! 找组织,GO

推荐: springCloud 微服务 系列

推荐阅读
nacos 实战(史上最全)
sentinel (史上最全+入门教程)
springcloud + webflux 高并发实战
Webflux(史上最全)
SpringCloud gateway (史上最全)
无编程不创客,无编程不创客,一大波编程高手正在疯狂创客圈交流、学习中! 找组织,GO

1. 什么是 WebClient

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

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

方式一:通过静态工厂方法创建响应式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);
};
}

参考:

https://www.jb51.net/article/133384.htm

https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client

https://www.jianshu.com/p/15d0a2bed6da

WebClient (史上最全)的更多相关文章

  1. Webflux(史上最全)

    文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...

  2. 史上最全Windows版本搭建安装React Native环境配置

    史上最全Windows版本搭建安装React Native环境配置 配置过React Native 环境的都知道,在Windows React Native环境配置有很多坑要跳,为了帮助新手快速无误的 ...

  3. 【Tips】史上最全H1B问题合辑——保持H1B身份终级篇

    [Tips]史上最全H1B问题合辑——保持H1B身份终级篇 2015-04-10留学小助手留学小助手 留学小助手 微信号 liuxue_xiaozhushou 功能介绍 提供最真实全面的留学干货,帮您 ...

  4. 史上最全的java随机数生成算法分享(转)

    这篇文章主要介绍了史上最全的java随机数生成算法,我分享一个最全的随机数的生成算法,最代码的找回密码的随机数就是用的这个方法 String password = RandomUtil.generat ...

  5. 【2016年特别福利】史上最全CSS学习资料大全

    css学习篇 [2016年特别福利]史上最全CSS学习资料大全

  6. [No00004F]史上最全Vim快捷键键位图(入门到进阶)

    史上最全Vim快捷键键位重磅来袭!!学习Linux的朋友看过来啦,你是不是觉得Linux编辑器Vim操作复杂,步骤繁琐呢?Linux工程师是不是想大幅度提升自己的工作效率呢? 经典版        下 ...

  7. 开源框架】Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发

    [原][开源框架]Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发,欢迎各位... 时间 2015-01-05 10:08:18 我是程序猿,我为自己代言 原文  http: ...

  8. 史上最全的CSS hack方式一览

    做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...

  9. [转]史上最全的CSS hack方式一览

    做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...

随机推荐

  1. 使用花生壳、瑞友天翼远程访问金蝶K3

    金蝶版本号 WISE15.1 瑞友6.0系列 花生壳5 金蝶软件无法通过外网直接访问 因此需要使用瑞友天翼来实现远程访问 一般来说 金蝶服务器固定了IP地址以后 通过路由器开房两个外网端口即可实现瑞友 ...

  2. C# 泛型Generic

    泛型(Generic),是将不确定的类型预先定义下来的一种C#高级语法,我们在使用一个类,接口或者方法前,不知道用户将来传什么类型,或者我们写的类,接口或方法相同的代码可以服务不同的类型,就可以定义为 ...

  3. 关于调试器中int3断点引发异常的思考

    INT3断点 INT3断点是利用0Xcc指令实现的,cpu在执行0xcc指令时会引发断点异常调试器会捕捉这个异常. INT3断点引发的异常属于陷阱型异常,在执行完0xcc指令后eip指向下一条指令.但 ...

  4. 用fread和fwrite实现文件复制操作

    #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc,char ...

  5. java的"\\s+"什么意思?

    例如:String[] tt=addr.split("\\s+");\\s ==\s 表示转义字符 ,\s表示匹配任意空格(包括空格,制表符,换页符)\\s+中的'+'表示多次匹配

  6. 技术干货 | 基于MindSpore更好的理解Focal Loss

    [本期推荐专题]物联网从业人员必读:华为云专家为你详细解读LiteOS各模块开发及其实现原理. 摘要:Focal Loss的两个性质算是核心,其实就是用一个合适的函数去度量难分类和易分类样本对总的损失 ...

  7. [刷题] PTA 02-线性结构4 Pop Sequence

    模拟栈进出 方法一: 1 #include<stdio.h> 2 #define MAXSIZE 1000 3 4 typedef struct{ 5 int data[MAXSIZE]; ...

  8. [刷题] 51 N-Queens

    要求 将n个皇后摆放在n*n的棋盘中,使横.竖和两个对角线方向均不会同时出现两个皇后 思路 尝试在一行中摆放,如果摆不下,回到上一行重新摆放,直到所有行都摆下 快速判断不合法情况 竖向:col[i]表 ...

  9. 如何对你的Linux系统进行基准测试: 3开源基准测试工具

    如何对你的Linux系统进行基准测试: 3开源基准测试工具   0 赞0 评论 文章标签:SYS  Source  benchmark  tool  开源  基准  系统     linux实用程序的 ...

  10. S7 Linux用户管理及用户信息查询命令

    7.1 useradd:创建用户 7.2-5 usermod 7.6 passwd:修改用户密码 7.7-9 chage.chpasswd.su 7.10-11 visudo.sudo 7.12-7. ...