WebClient (史上最全)
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 【博客园总入口 】
疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 大厂必备 + 大厂必备 + 大厂必备 【博客园总入口 】
入大厂+涨工资 必备的 高并发社群: 【博客园总入口 】
推荐: 地表最强 开发环境 系列
工欲善其事 必先利其器 |
---|
地表最强 开发环境: 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://www.jianshu.com/p/15d0a2bed6da
WebClient (史上最全)的更多相关文章
- Webflux(史上最全)
文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...
- 史上最全Windows版本搭建安装React Native环境配置
史上最全Windows版本搭建安装React Native环境配置 配置过React Native 环境的都知道,在Windows React Native环境配置有很多坑要跳,为了帮助新手快速无误的 ...
- 【Tips】史上最全H1B问题合辑——保持H1B身份终级篇
[Tips]史上最全H1B问题合辑——保持H1B身份终级篇 2015-04-10留学小助手留学小助手 留学小助手 微信号 liuxue_xiaozhushou 功能介绍 提供最真实全面的留学干货,帮您 ...
- 史上最全的java随机数生成算法分享(转)
这篇文章主要介绍了史上最全的java随机数生成算法,我分享一个最全的随机数的生成算法,最代码的找回密码的随机数就是用的这个方法 String password = RandomUtil.generat ...
- 【2016年特别福利】史上最全CSS学习资料大全
css学习篇 [2016年特别福利]史上最全CSS学习资料大全
- [No00004F]史上最全Vim快捷键键位图(入门到进阶)
史上最全Vim快捷键键位重磅来袭!!学习Linux的朋友看过来啦,你是不是觉得Linux编辑器Vim操作复杂,步骤繁琐呢?Linux工程师是不是想大幅度提升自己的工作效率呢? 经典版 下 ...
- 开源框架】Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发
[原][开源框架]Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发,欢迎各位... 时间 2015-01-05 10:08:18 我是程序猿,我为自己代言 原文 http: ...
- 史上最全的CSS hack方式一览
做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...
- [转]史上最全的CSS hack方式一览
做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...
随机推荐
- PHP 下载apk文件
方式一.public function downApkFile(){ $path = Env::get('root_path')."apk/"; //路径 $file_name = ...
- nginx 配置后页面访问是报500错
该问题是html文件权限问题. 用jenkins 并远程服务器上传到另一台服务器的html ,在配置好nginx 的location root 绝对位置后还是报错500 手工用root上传时访问正常 ...
- 多线程-4.wait() notify() notifyAll() 生产者消费者模型
1.wait()方法 该方法继承于Object类.在调用obj.wait()方法后,当前线程会失去obj的锁.待其他线程调用obj.notify()或notifyAll()方法后进入锁等待池,争抢到锁 ...
- vue中的nextTick
今天在浏览elementUI官网时,又一次看到了nextTick,其实nextTIck我已经不是第一次看到了,但之前都没怎么弄懂,这次决定好好研究一番-- 异步说明 vue是异步执行DOM更新的. ...
- [敏杰开发]Beta阶段测试报告
[敏杰开发]Beta阶段测试报告 这是一篇软工课程博客 项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 作业要求 Beta阶段测试报告 我们在这个课程的目标是 测试项目 这个作业在哪个 ...
- 1.5 RPM红帽软件包1.6 Yum软件仓库
1.5 RPM红帽软件包 在RPM(红帽软件包管理器)公布之前,要想在Linux系统中安装软件只能采取源码包的方式安装.早期在Linux系统中安装程序是一件非常困难.耗费耐心的事情,而且大多数的服务程 ...
- 搭建LNMP环境部署Wordpress博客
!!!首先要做的就是关闭系统的防火墙以及selinux: #systemctl stop firewalld #systemctl disable firewalld #sed -ri 's/^(SE ...
- shell初学之PHP
初次接触脚本,写了一个通过Apache实现PHP动态网站的脚本: #!/bin/bash yum -y install php rm -rf /etc/httpd/conf.d/welcome.con ...
- python基础之变量类型和数据运算
一.变量的类型 程序中,为了更充分的利用内存空间以及更有效率的管理内存,变量是有不同的类型的. 怎样知道一个变量的类型呢? 在python中,只要定义了一个变量,而且它有数据,那么它的类型就已经确定了 ...
- JAVA并发(3)-ReentrantReadWriteLock的探索
1. 介绍 本文我们继续探究使用AQS的子类ReentrantReadWriteLock(读写锁).老规矩,先贴一下类图 ReentrantReadWriteLock这个类包含读锁和写锁,这两种锁都存 ...