响应式编程大行其道的当下,WebClient大大简化了交互的复杂性,如有响应式需求场景,好好读一下这个系列绝不会有错
WebClient
- 1: 1.基础使用姿势
- 2: 2.文件上传
- 3: 3.请求头设置
- 4: 4.Basic Auth授权
- 5: 5.超时设置
- 6: 6.retrieve与exchange的使用区别介绍
- 7: 7.非200状态码信息捕获
- 8: 8.策略设置
- 9: 9.同步与异步
1 - 1.基础使用姿势
前面在介绍使用AsyncRestTemplate
来实现网络异步请求时,当时提到在Spring5+之后,建议通过WebClient来取代AsyncRestTemplate来实现异步网络请求;
那么WebClient又是一个什么东西呢,它是怎样替代AsyncRestTemplate
来实现异步请求的呢,接下来我们将进入Spring Web工具篇中,比较重要的WebClient系列知识点,本文为第一篇,基本使用姿势一览
I. 项目环境
我们依然采用SpringBoot来搭建项目,版本为 2.2.1.RELEASE
, maven3.2
作为构建工具,idea
作为开发环境
1. pom依赖
SpringBoot相关的依赖就不贴出来了,有兴趣的可以查看源码,下面是关键依赖
<dependencies>
<!-- 请注意这个引入,是最为重要的 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
请注意一下上面的两个依赖包,对于使用WebClient
,主要需要引入spring-boot-starter-webflux
包
2. 测试REST接口
接下来我们直接在这个项目中写几个用于测试的REST接口,因为项目引入的webflux的依赖包,所以我们这里也采用webflux的注解方式,新增用于测试的GET/POST接口
对于WebFlux用法不太清楚的小伙伴也没有关系,WebClient的发起的请求,后端是基于传统的Servlet也是没有问题的;关于WebFlux的知识点,将放在WebClient系列博文之后进行介绍
@Data
public class Body {
String name;
Integer age;
}
@RestController
public class ReactRest {
@GetMapping(path = "header")
public Mono<String> header(@RequestHeader(name = "User-Agent") String userAgent,
@RequestHeader(name = "ck", required = false) String cookie) {
return Mono.just("userAgent is: [" + userAgent + "] ck: [" + cookie + "]");
}
@GetMapping(path = "get")
public Mono<String> get(String name, Integer age) {
return Mono.just("req: " + name + " age: " + age);
}
@GetMapping(path = "mget")
public Flux<String> mget(String name, Integer age) {
return Flux.fromArray(new String[]{"req name: " + name, "req age: " + age});
}
/**
* form表单传参,映射到实体上
*
* @param body
* @return
*/
@PostMapping(path = "post")
public Mono<String> post(Body body) {
return Mono.just("post req: " + body.getName() + " age: " + body.getAge());
}
// 请注意,这种方式和上面的post方法两者不一样,主要区别在Content-Type
@PostMapping(path = "body")
public Mono<String> postBody(@RequestBody Body body) {
return Mono.just("body req: " + body);
}
}
针对上面的两个POST方法,虽然参数都是Body,但是一个有@RequestBody
,一个没有,这里需要额外注意
从下图也可以看出,两者的区别之处
II. WebClient使用说明
接下来我们将进入WebClient的使用说明,主要针对最常见的GET/POST请求姿势进行实例展示,目标是看完下面的内容之后,可以愉快的进行最基本(手动加强语气)的GET/POST请求发送
以下所有内容,参考or启发与官方文档: https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client
1. WebClient创建姿势
一般有三种获得WebClient的方式,基于WebClient#create
创建默认的WebClient,基于WebClient#builder
创建有自定义需求的WebClient,基于已有的webclient#mutate
创建
a. create方式
这种属于最常见,也是最基础的创建方式,通常有两种case
WebClient.create()
WebClient.create(String baseUrl)
:与上面一个最主要的区别在于指定了baseUrl,后面再发起的请求,就不需要重复这个baseUrl了;- 举例说明:baseUrl指定为
http://127.0.0.1:8080
;那么后面的请求url,直接填写/get
,/header
,/post
这种path路径即可
- 举例说明:baseUrl指定为
下面给出一个实例说明
// 创建WebClient实例
WebClient webClient= WebClient.create();
// 发起get请求,并将返回的数据格式转换为String;因为是异步请求,所以返回的是Mono包装的对象
Mono<String> ans = webClient.get().uri("http://127.0.0.1:8080/get?name=一灰灰&age=18").retrieve().bodyToMono(String
.class);
ans.subscribe(s -> System.out.println("create return: " + s));
b. builder方式
builder方式最大的区别在于它可以为WebClient
“赋能”, 比如我们希望所有的请求都有通用的请求头、cookie等,就可以通过builder
的方式,在创建WebClient
的时候就进行指定
官方支持的可选配置如下:
uriBuilderFactory
: Customized UriBuilderFactory to use as a base URL.defaultHeader
: Headers for every request.defaultCookie
: Cookies for every request.defaultRequest
: Consumer to customize every request.filter
: Client filter for every request.exchangeStrategies
: HTTP message reader/writer customizations.clientConnector
: HTTP client library settings.
关于上面这些东西有啥用,怎么用,会在后续的系列博文中逐一进行介绍,这里就不详细展开;有兴趣的小伙伴可以关注收藏一波
给出一个设置默认Header的实例
webClient = WebClient.builder().defaultHeader("User-Agent", "WebClient Agent").build();
ans = webClient.get().uri("http://127.0.0.1:8080/header").retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("builderCreate with header return: " + s));
c. mutate方式
这种方式主要是在一个已经存在的WebClient
基础上,再创建一个满足自定义需求的WebClient
为什么要这样呢?
- 因为WebClient一旦创建,就是不可修改的
下面给出一个在builder创建基础上,再添加cookie的实例
// 请注意WebClient创建完毕之后,不可修改,如果需要设置默认值,可以借助 mutate 继承当前webclient的属性,再进行扩展
webClient = webClient.mutate().defaultCookie("ck", "--web--client--ck--").build();
ans = webClient.get().uri("http://127.0.0.1:8080/header").retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("webClient#mutate with cookie return: " + s));
d. 测试输出
查看项目源码的小伙伴,会看到上面三个代码片段是在同一个方法内部,测试输出如下
你会看到一个有意思的地方,第一种基础的创建方式输出在第二种之后,这个是没有问题的哈(有疑问的小伙伴可以看一下文章开头,我们介绍WebClient的起因是啥)
2. GET请求
上面其实已经给出了GET的请求姿势,一般使用姿势也比较简单,我们需要重点关注一下这个传参问题
常见的使用姿势
webClient.get().uri(xxx).retrieve().bodyToMono/bodyToFlux
get的传参,除了在uri中直接写死之外,还有几种常见的写法
a. uri参数
可变参数
查看源码的小伙伴,可以看到uri方法的接口声明为一个可变参数,所以就有一种uri用占位{}
表示参数位置,后面的参数对应参数值的时候用方式
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
Mono<String> ans = webClient.get().uri("/get?name={1}", "一灰灰").retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("basic get with one argument res: " + s));
// p1对应后面第一个参数 "一灰灰" p2 对应后面第二个参数 18
ans = webClient.get().uri("/get?name={p1}&age={p2}", "一灰灰", 18).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("basic get with two arguments res: " + s));
请注意,上面两个参数的case中,p1对应的是一灰灰
,p2对应的是18
;这里的p1和p2可以替换为任意的其他字符,它们是按照顺序进行填充的,即第一个参数值填在第一个{}
坑位
map参数映射
另外一种方式就是通过map来绑定参数名与参数值之间的映射关系
// 使用map的方式,来映射参数
Map<String, Object> uriVariables = new HashMap<>(4);
uriVariables.put("p1", "一灰灰");
uriVariables.put("p2", 19);
Flux<String> fAns =
webClient.get().uri("/mget?name={p1}&age={p2}", uriVariables).retrieve().bodyToFlux(String.class);
fAns.subscribe(s -> System.out.println("basic mget return: " + s));
b. 获取ResponseEntity
请仔细观察上面的使用姿势,调用了retrieve()
方法,这个主要就是用来从返回结果中“摘出”responseBody
,那么如果我们希望后去返回的请求头,返回的状态码,则需要将这个方法替换为exchange()
下面给出一个获取返回的请求头实例
// 获取请求头等相关信息
Mono<ResponseEntity<String>> response = webClient.get().uri("/get?name={p1}&age={p2}", "一灰灰", 18).exchange()
.flatMap(r -> r.toEntity(String.class));
response.subscribe(
entity -> System.out.println("res headers: " + entity.getHeaders() + " body: " + entity.getBody()));
和前面的时候姿势大同小异,至于flatMap这些知识点会放在后续的WebFlux中进行介绍,这里知道它是用来ResponseBody格式转换关键点即可
c. 测试返回
测试输出结果如下(当然实际输出顺序和上面定义的先后也没有什么关系)
3. POST请求
对于post请求,我们一般最长关注的就是基本的表单传参和json body方式传递,下面分别给与介绍
a. 表单参数
借助MultiValueMap
来保存表单参数用于提交
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
// 通过 MultiValueMap 方式投递form表单
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(4);
formData.add("name", "一灰灰Blog");
formData.add("age", "18");
// 请注意,官方文档上提示,默认的ContentType就是"application/x-www-form-urlencoded",所以下面这个contentType是可以不显示设置的
Mono<String> ans = webClient.post().uri("/post")
// .contentType(MediaType.APPLICATION_FORM_URLENCODED)
.bodyValue(formData).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("post formData ans: " + s));
上面注释了一行contentType(MediaType.APPLICATION_FORM_URLENCODED)
,因为默认的ContentType就是这个了,所以不需要额外指定(当然手动指定也没有任何毛病)
除了上面这种使用姿势之外,在官方教程上,还有一种写法,特别注意下面这种写法的传参是用的body
,而上面是bodyValue
,千万别用错,不然…
// 请注意这种方式与上面最大的区别是 body 而不是 bodyValue
ans = webClient.post().uri("/post").body(BodyInserters.fromFormData(formData)).retrieve()
.bodyToMono(String.class);
ans.subscribe(s -> System.out.println("post2 formData ans: " + s));
b. json body传参
post一个json串,可以说是比较常见的case了,在WebClient中,使用这种方式特别特别简单,感觉比前面那个还方便
- 指定ContentType
- 传入Object对象
// post body
Body body = new Body();
body.setName("一灰灰");
body.setAge(18);
ans = webClient.post().uri("/body").contentType(MediaType.APPLICATION_JSON).bodyValue(body).retrieve()
.bodyToMono(String.class);
ans.subscribe(s -> System.out.println("post body res: " + s));
c. 测试输出
4. 小结
本文为WebClient系列第一篇,介绍WebClient的基本使用姿势,当然看完之后,发起GET/POST请求还是没有什么问题的;但是仅限于此嘛?
- builder创建方式中,那些可选的条件都是啥,有什么用,什么场景下会用呢?
- 请求超时时间可设置么?
- 可以同步阻塞方式获取返回结果嘛?
- 代理怎么加
event-stream
返回方式的数据怎么处理- 如何上传文件
- Basic Auth身份鉴权
- 异步线程池可指定么,可替换为自定义的么
- 返回非200状态码时,表现如何,又该如何处理
- ….
后续的系列博文将针对上面提出or尚未提出的问题,一一进行介绍,看到的各位大佬按按鼠标点赞收藏评论关注加个好友呗
II. 其他
0. 项目
2 - 2.文件上传
在上一篇WebClient基本使用姿势中,介绍了如何借助WebClient来实现异步的GET/POST访问,接下来这篇文章则主要介绍文件上传的使用姿势
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. 文件上传接口
借助WebFlux,写一个简单的文件上传的REST接口(关于WebFlux的使用姿势不属于本文重点,下面的代码如有不懂的地方,可以直接忽略掉或者关注一下接下来的WebFlux系列博文)
/**
* 文件上传
*
* @param filePart
* @return
*/
@PostMapping(path = "upload", produces = MediaType.MULTIPART_MIXED_VALUE)
public Mono<String> upload(@RequestPart(name = "data") FilePart filePart, ServerWebExchange exchange)
throws IOException {
Mono<MultiValueMap<String, Part>> ans = exchange.getMultipartData();
StringBuffer result = new StringBuffer("【basic uploads: ");
ans.subscribe(s -> {
for (Map.Entry<String, List<Part>> entry : s.entrySet()) {
for (Part part : entry.getValue()) {
result.append(entry.getKey()).append(":");
dataBuffer2str(part.content(), result);
}
}
});
result.append("】");
return Mono.just(result.toString());
}
private void dataBuffer2str(Flux<DataBuffer> data, StringBuffer buffer) {
data.subscribe(s -> {
byte[] bytes = new byte[s.readableByteCount()];
s.read(bytes);
buffer.append(new String(bytes)).append(";");
});
}
3. 待上传文件
在项目的资源目录resources
下,新建一个文本文件,用于测试上传时使用
test.txt
hello 一灰灰😝ddd
II. 文件上传
在前面介绍RestTemplate的系列博文中,同样有一篇关于RestTemplate文件上传的博文,建议两篇对照阅读,可以获取双倍的收获哦
1. 单个文件上传
请注意,文件上传依然是POST请求,一般来讲,请求头的Content-Type
为multipart/form-data
前面介绍WebClient的POST传参时,参数是封装在MultiValueMap
中的,在文件上传中,依然如此,不同的是这个参数的构建方式
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("data",
new FileSystemResource(this.getClass().getClassLoader().getResource("test.txt").getFile()));
// 表单参数
builder.part("name", "一灰灰");
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
Mono<String> ans = webClient.post().uri("/upload").bodyValue(parts).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("upload file return : " + s));
重点关注一下借助MultipartBodyBuilder
创建请求参数的过程
剩下的发起请求的姿势,与之前介绍的POST方式,没有什么区别
2. 流上传
当然需要上传时,多半也不会是上传文件,比如一个常见的case可能是下载远程资源,并上传给内部服务;所以我们会使用InputStreamResource
来替换FileSystemResource
// 以流的方式上传资源
builder = new MultipartBodyBuilder();
final InputStream stream = this.getClass().getClassLoader().getResourceAsStream("test.txt");
builder.part("data", new InputStreamResource(stream) {
@Override
public long contentLength() throws IOException {
// 这个方法需要重写,否则无法正确上传文件;原因在于父类是通过读取流数据来计算大小
return stream.available();
}
@Override
public String getFilename() {
return "test.txt";
}
});
parts = builder.build();
ans = webClient.post().uri("/upload").bodyValue(parts).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("upload stream return: " + s));
请注意:当不重写InpustStreamResource
的contentLength
与getFilename
方法时,没法实现我们上传的目的哦
3. 字节数组上传
有流的方式,当然就不会缺少字节数组的方式,基本姿势与上面并无二样
// 以字节数组的方式上传资源
builder = new MultipartBodyBuilder();
builder.part("data", new ByteArrayResource("hello 一灰灰😝!!!".getBytes()) {
@Override
public String getFilename() {
return "test.txt";
}
});
parts = builder.build();
ans = webClient.post().uri("/upload").bodyValue(parts).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("upload bytes return: " + s));
4. 多文件上传
除了一个一个文件上传之外,某些case下也可能出现一次上传多个文件的情况,对于WebClient而言,无非就是构建上传参数的时候,多一个add而言
// 多文件上传,key都是data,存value的是一个列表哦,所以没调用一次,表示新塞入一个资源
builder.part("data", new ByteArrayResource("hello 一灰灰😝!!!".getBytes()) {
@Override
public String getFilename() {
return "test.txt";
}
});
builder.part("data", new ByteArrayResource("welcome 二灰灰😭!!!".getBytes()) {
@Override
public String getFilename() {
return "test2.txt";
}
});
parts = builder.build();
ans = webClient.post().uri("/upload").bodyValue(parts).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("batch upload bytes return: " + s));
5. BodyInserters方式
除了上面的MultipartBodyBuilder
创建传参之外,还可以借助BodyInserters
来处理,前面在接收Post传参的两种姿势中也介绍过;
不过不同于之前的BodyInserters#fromFormData
,我们这里使用的是BodyInserters#fromMultipartData
(从调用的方法签名上,也知道两者的各自应用场景)
ans = webClient.post().uri("/upload").body(BodyInserters.fromMultipartData("data",
new FileSystemResource(this.getClass().getClassLoader().getResource("test.txt").getFile()))
.with("name", "form参数")).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("upload file build by BodyInserters return: " + s));
请注意,我们传参是通过body
方法,而不是前面的bodyValue
方法;如果使用错了,将无法达到预期的目的,而且极有可能调试半天也不知道啥原因…
6. 测试输出
所有上面的代码可以在文末的工程源码连接中获取,下面是执行的输出结果
II. 其他
0. 项目
系列博文
源码
3 - 3.请求头设置
在网络请求中,根据请求头的参数来做校验属于比较常见的一种case了,很多场景下我们发起的请求都需要额外的去设置请求头,本文将介绍WebClient设置请求头的两种姿势
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. REST接口
基于WebFlux提供一个http接口,返回请求参数与请求头(关于webflux的用法在后续的WebFlux系列篇中给与介绍)
/**
* 返回请求头
*
* @return
*/
@GetMapping(path = "withHeader")
public Mono<String> withHeader(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
MultiValueMap<String, String> params = request.getQueryParams();
return Mono.just("-->headers: " + JSONObject.toJSONString(headers) + ";\t-->params:" +
JSONObject.toJSONString(params));
}
II. 设置请求头
1. 默认请求头
在第一篇介绍WebClient创建的几种姿势中,WebClient.builder()
方式创建时,可以指定默认的请求头,因此我们可以在创建时指定
// 1. 在创建时,指定默认的请求头
WebClient webClient = WebClient.builder().defaultHeader("User-Agent", "SelfDefine Header")
.defaultHeader("referer", "localhost").baseUrl("http://127.0.0.1:8080").build();
Mono<String> ans =
webClient.get().uri("/withHeader?name={1}&age={2}", "一灰灰", 19).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("basic get with default header return: " + s));
通过上面这种方式创建的webclient,所有的请求都会携带User-Agent: SelfDefine Header
这个请求头哦
2. filter方式
除了上面这种姿势之外,WebClient还支持了Filter,对于Filter你可以将它理解为Servlet中的Filter,主要用于在发起请求时,做一些过滤操作,因此我们完全可以写一个塞入自定义请求头的Filter
// 2. 使用filter
webClient = WebClient.builder().filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request).header("filter-header", "self defined header").build();
// 下面这一行可不能忘记,不然会出大事情
return next.exchange(filtered);
}).baseUrl("http://127.0.0.1:8080").build();
ans = webClient.get().uri("/withHeader?name={1}&age={2}", "一灰灰", 19).retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("basic get with filter header return: " + s));
请注意上面添加header
的用法
3. 测试&小结
关于详细的测试代码可以在下面的工程源码中获取,下面给出上面两种姿势的返回结果
basic get with default header return: -->headers: {"accept-encoding":["gzip"],"host":["127.0.0.1:8080"],"accept":["*/*"],"User-Agent":["SelfDefine Header"],"referer":["localhost"]}; -->params:{"name":["一灰灰"],"age":["19"]}
basic get with filter header return: -->headers: {"accept-encoding":["gzip"],"user-agent":["ReactorNetty/0.9.1.RELEASE"],"host":["127.0.0.1:8080"],"accept":["*/*"],"filter-header":["self defined header"]}; -->params:{"name":["一灰灰"],"age":["19"]}
小结:
本文介绍两种请求头的设置方式
- 通过
WebClient.builder
在创建时,通过defaultHeader
指定默认的请求头 - 借助Filter,主动在
Request
中塞入请求头
III. 其他
0. 项目
系列博文
源码
4 - 4.Basic Auth授权
关于BasicAuth是什么,以及如何实现鉴权的知识点可以在之前的博文 【WEB系列】RestTemplate之Basic Auth授权 中已经介绍过了,因此本篇将直接进入正文,介绍一下如何在WebClient中进行Basic Auth授权
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. REST接口
基于WebFlux提供一个http接口,根据请求头解析Basic Auth是否合法,一个最原始的简单实现方式如下
@GetMapping(path = "auth")
public Mono<String> auth(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
List<String> authList = request.getHeaders().get("Authorization");
if (CollectionUtils.isEmpty(authList)) {
response.setStatusCode(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
return Mono.just("no auth info!");
}
String auth = authList.get(0);
String[] userAndPass = new String(new BASE64Decoder().decodeBuffer(auth.split(" ")[1])).split(":");
if (userAndPass.length < 2) {
response.setStatusCode(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
return Mono.just("illegal auth info!");
}
if (!("user".equalsIgnoreCase(userAndPass[0]) && "pwd".equalsIgnoreCase(userAndPass[1]))) {
response.setStatusCode(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
return Mono.just("error auth info!");
}
return Mono.just("auth success: " + JSONObject.toJSONString(request.getQueryParams()));
}
当鉴权成功之后,正常返回;当鉴权失败之后,返回403状态码,并返回对应的提示信息
II. Basic Auth鉴权
理解Basic Auth实现原理的小伙伴,可以很简单的实现,比如直接设置请求头
1. 设置请求头
直接在WebClient创建的时候,指定默认的请求头即可
// 最原始的请求头设置方式
WebClient webClient = WebClient.builder()
.defaultHeader("Authorization", "Basic " + Base64Utils.encodeToString("user:pwd".getBytes()))
.baseUrl("http://127.0.0.1:8080").build();
Mono<ResponseEntity<String>> response =
webClient.get().uri("/auth?name=一灰灰&age=18").exchange().flatMap(s -> s.toEntity(String.class));
2. filter方式
在上一篇介绍WebClient请求头的使用姿势中,除了默认请求头设置之外,还有一个filter的方式,而WebClient正好提供了一个专门用于Basic Auth的Filter
// filter方式
webClient = WebClient.builder().filter(ExchangeFilterFunctions.basicAuthentication("user", "pwd"))
.baseUrl("http://127.0.0.1:8080").build();
response = webClient.get().uri("/auth?name=一灰灰&age=18").exchange().flatMap(s -> s.toEntity(String.class));
response.subscribe(s -> System.out.println("auth return: " + s));
3. 测试与小结
以上代码可以在后文的工程源码中获取,测试输出如下
header auth return: <200 OK OK,auth success: {"name":["一灰灰"],"age":["18"]},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"49"]>
filter auth return: <200 OK OK,auth success: {"name":["一灰灰"],"age":["18"]},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"49"]>
本文主要介绍了两种WebClient的Basic Auth使用姿势,其原理都是基于设置请求头的方式来实现的
- 基于
WebClient.builder().defaultHeader
来手动设置默认请求头 - 基于
WebClient.builder().filter
与ExchangeFilterFunctions.basicAuthentication
,通过filter来处理请求头
II. 其他
0. 项目
系列博文
源码
5 - 5.超时设置
为所有的第三方接口调用设置超时时间是一个比较推荐的做法,避免自己的任务被所依赖的服务给拖死;在WebClient发起的异步网络请求调用中,应该如何设置超时时间呢?
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. REST接口
基于WebFlux提供一个http接口,内部sleep 5s,来响应后续的超时case
@GetMapping(path = "timeout")
public Mono<String> timeout(String name, Integer age) throws InterruptedException {
Thread.sleep(5_000);
return Mono.just("timeout req: " + name + " age: " + age);
}
II. 超时
本篇实例主要来自于官方文档: webflux-client-builder-reactor-timeout
1. 实例演示
在WebClient的创建中,实际上并没有找到有设置超时的入口,基于之前RestTemplate的超时设置中的经验,我们可能需要将目标放在更底层实现网络请求的HttpClient上
// 设置连接超时时间为3s
HttpClient httpClient = HttpClient.create().tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3_000)
.doOnConnected(
conn -> conn.addHandlerLast(new ReadTimeoutHandler(3))
.addHandlerLast(new WriteTimeoutHandler(3))));
上面虽然获取了一个带超时设置的HttpCilent,但是我们需要用它来设置WebClient,这里就需要借助WebClient.builder().clientConnector
来实现了
// 设置httpclient
WebClient webClient = WebClient.builder().baseUrl("http://127.0.0.1:8080")
.clientConnector(new ReactorClientHttpConnector(httpClient)).build();
Mono<ResponseEntity<String>> ans =
webClient.get().uri("/timeout").exchange().flatMap(s -> s.toEntity(String.class));
ans.subscribe(s -> System.out.println("timeout res code: " + s.getStatusCode()));
2. 测试与小结
本文所有源码可以在后面的项目地址中获取,测试输出结果如下
虽然上面的输出提示了超时,但是奇怪的是居然不像RestTemplate的超时抛异常,上面这个流程可以正常走通,那么如何捕获这个超时异常呢,WebClient访问出现非200状态码返回的又可以如何处理呢,下篇博文将给与介绍
小结
- 通过
HttpClient
来设置超时时间 - 借助
WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient))
来绑定HttpClient
II. 其他
0. 项目
系列博文
源码
6 - 6.retrieve与exchange的使用区别介绍
前面介绍了几篇WebCilent的使用姿势博文,其中大部分的演示case,都是使用retrieve
来获取返回ResponseBody,我国我们希望获取更多的返回信息,比如获取返回头,这个时候exchange则是更好的选择;
本文将主要介绍一下,在WebClient中retrieve和exchange的各自使用场景
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. REST接口
添加一个简单的Rest接口,用于后续的测试
@GetMapping(path = "get")
public Mono<String> get(String name, Integer age) {
return Mono.just("req: " + name + " age: " + age);
}
II. 实例演示
通过前面的几篇学习,我们知道WebClient发起请求的一般使用姿势如下
Mono<ClientResponse> res = webCient.get().uri(xxx).exchange()
WebClient.ResponseSpec responseSpec = webClient.get().uri(xxx).retrieve()
这两个方法都是用来获取返回结果的,最大的区别在于通过exchange接收完整的ResponseEntity;而retrieve接收的则是ResponseBody
1. exchange使用实例
The exchange() method provides more control than the retrieve method, provides access to the ClientResponse
下面给出一个简单的,获取返回的状态码,cookies等请求头信息的case
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
// 返回结果
Mono<ClientResponse> res = webClient.get().uri("/get?name={1}&age={2}", "一灰灰", 18).exchange();
res.subscribe(s -> {
HttpStatus statusCode = s.statusCode();
ClientResponse.Headers headers = s.headers();
MultiValueMap<String, ResponseCookie> ans = s.cookies();
s.bodyToMono(String.class).subscribe(body -> {
System.out.println(
"response detail: \nheader: " + headers.asHttpHeaders() + "\ncode: " + statusCode + "\ncookies: " + ans +
"\nbody:" + body);
});
});
上面这段代码中,主要的核心点就是ClientResponse
的解析,可以直接通过它获取返回头,响应状态码,其次提供了一些对ResponseBody的封装调用
返回结果
response detail:
header: [Content-Type:"text/plain;charset=UTF-8", Content-Length:"22"]
code: 200 OK
cookies: {}
body:req: 一灰灰 age: 18
如果我们只关注ResponseBody,用exchange也是可以直接写的,如下,相比retrieve稍微饶了一道
Mono<String> result = client.get()
.uri("/get?name={1}&age={2}", "一灰灰", 18).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(String.class));
另外一个更加推荐的写法是直接返回Mono<ResponseEntity<?>>
,更友好的操作姿势,返回结果如下
response detail2:
code: 200 OK
headers: [Content-Type:"text/plain;charset=UTF-8", Content-Length:"22"]
body: req: 一灰灰 age: 18
2. retrieve使用实例
The retrieve() method is the easiest way to get a response body and decode it.
前面已经多次演示retrieve的使用姿势,基本上是在后面带上bodyToMono
或bodyToFlux
来实现返回实体的类型转换
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
Mono<String> ans = webClient.get().uri("/get?name={1}", "一灰灰").retrieve().bodyToMono(String.class);
ans.subscribe(s -> System.out.println("basic get with one argument res: " + s));
3. 小结
对于retrieve与exchange来说,最简单也是最根本的区别在于,是否需要除了ResponseBody之外的其他信息
- 如果只关注
ResponseBody
: 推荐使用retrieve
- 如果还需要获取其他返回信息: 请选择
exchange
II. 其他
0. 项目
系列博文
- 【WEB系列】WebClient之超时设置
- 【WEB系列】WebClient之Basic Auth授权
- 【WEB系列】WebClient之请求头设置
- 【WEB系列】WebClient之文件上传
- 【WEB系列】WebClient之基础使用姿势
源码
7 - 7.非200状态码信息捕获
前面介绍的WebClient的使用姿势都是基于正常的200状态码返回,当然在实际的使用中,我们并不是总能获取到200的状态码的,在RestTemplate的学习中,我们知道如果不特殊处理,那么是无法正确获取非200状态码的ResponseBody的,会直接抛出异常,那么在WebClient中又应该怎样应对非200状态码返回的场景呢?
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. REST
一个简单的403返回
@GetMapping(path = "403")
public Mono<String> _403(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
return Mono.just("403 response body!");
}
II. 实例说明
1. retrieve方式
上一篇介绍retrieve与exchange区别的博文中,我们知道retrieve更适用于只希望获取ResponseBody的场景;使用retrieve时,如需要捕获其他状态码的返回,可以如下操作
Mono<String> ans = webClient.get().uri("403").retrieve().onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("inner retrieve 403 res: " + response.headers().asHttpHeaders() + "|" + response.statusCode());
response.bodyToMono(String.class).subscribe(s -> System.out.println("inner res body: " + s));
return Mono.just(new RuntimeException("403 not found!"));
}).bodyToMono(String.class);
ans.subscribe(s -> System.out.println("retrieve 403 ans: " + s));
请注意上面的 onStatus
, 上面演示的是4xx的捕获,返回如下
inner retrieve 403 res: [Content-Type:"text/plain;charset=UTF-8", Content-Length:"18"]|403 FORBIDDEN
2. exchange方式
exchange本身就可以获取完整的返回信息,所以异常的case需要我们自己在内部进行处理
webClient.get().uri("403").exchange().subscribe(s -> {
HttpStatus statusCode = s.statusCode();
ClientResponse.Headers headers = s.headers();
MultiValueMap<String, ResponseCookie> cookies = s.cookies();
s.bodyToMono(String.class).subscribe(body -> {
System.out.println(
"error response detail: \nheader: " + headers.asHttpHeaders() + "\ncode: " + statusCode +
"\ncookies: " + cookies + "\nbody:" + body);
});
});
返回结果如下
exchange error response detail:
header: [Content-Type:"text/plain;charset=UTF-8", Content-Length:"18"]
code: 403 FORBIDDEN
cookies: {}
body:403 response body!
II. 其他
0. 项目
系列博文
- 【WEB系列】WebClient之retrieve与exchange的使用区别介绍
- 【WEB系列】WebClient之超时设置
- 【WEB系列】WebClient之Basic Auth授权
- 【WEB系列】WebClient之请求头设置
- 【WEB系列】WebClient之文件上传
- 【WEB系列】WebClient之基础使用姿势
源码
8 - 8.策略设置
在前面介绍WebClient的常见参数中,有一个exchangeStrategies
参数设置,通过它我们可以设置传输数据的内存占用大小限制,避免内存问题;也可以通过它设置数据的编解码
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. 测试接口
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Body implements Serializable {
private static final long serialVersionUID = 1210673970258821332L;
String name;
Integer age;
}
@GetMapping(path = "get")
public Mono<String> get(String name, Integer age) {
return Mono.just("req: " + name + " age: " + age);
}
@PostMapping(path = "body2")
public Mono<Body> postBody2(@RequestBody Body body) {
return Mono.just(body);
}
II. 使用说明
1. MaxInMemorySize设置
默认情况下,内存中接收数据的buffering data size为256KB,可以根据实际的需要进行改大or调小
// 默认允许的内存空间大小为256KB,可以通过下面的方式进行修改
webClient = WebClient.builder().exchangeStrategies(
ExchangeStrategies.builder().codecs(codec -> codec.defaultCodecs().maxInMemorySize(10)).build())
.baseUrl("http://127.0.0.1:8080").build();
String argument = "这也是一个很长很长的文本,用于测试超出上限!";
Mono<String> ans = webClient.get().uri("/get?name={1}", argument).retrieve().bodyToMono(String.class)
// 异常处理
.doOnError(WebClientResponseException.class, err -> {
System.out.println(err.getRawStatusCode() + "," + err.getResponseBodyAsString());
throw new RuntimeException(err.getMessage());
}).onErrorReturn("fallback");
ans.subscribe(s -> System.out.println("exchange strategy: " + ans));
2. 编解码设置
比如最常见的json编解码
WebClient webClient = WebClient.builder().exchangeStrategies(ExchangeStrategies.builder().codecs(codec -> {
codec.customCodecs().decoder(new Jackson2JsonDecoder());
codec.customCodecs().encoder(new Jackson2JsonEncoder());
}).build()).baseUrl("http://127.0.0.1:8080").build();
Body body = new Body("一灰灰😝", 18);
Mono<Body> ans =
webClient.post().uri("/body2").contentType(MediaType.APPLICATION_JSON).bodyValue(body).retrieve()
.bodyToMono(Body.class);
ans.subscribe(s -> System.out.println("retreive res: " + s));
上面两个测试之后,返回结果如下
II. 其他
0. 项目
系列博文
- 【WEB系列】WebClient之非200状态码信息捕获
- 【WEB系列】WebClient之retrieve与exchange的使用区别介绍
- 【WEB系列】WebClient之超时设置
- 【WEB系列】WebClient之Basic Auth授权
- 【WEB系列】WebClient之请求头设置
- 【WEB系列】WebClient之文件上传
- 【WEB系列】WebClient之基础使用姿势
源码
9 - 9.同步与异步
回顾一下最开始介绍WebClient的使用姿势之前,我们介绍了AsyncRestTemplate来实现异步的网络请求;但是在Spring5之后,官方推荐使用WebClient来替换AsyncRestTemplate实现异步请求;所以一般来讲,WebClient适用于异步的网络访问,但是,假设我需要同步获取返回结果,可行么?
I. 项目环境
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1. 依赖
使用WebClient,最主要的引入依赖如下(省略掉了SpringBoot的相关依赖,如对于如何创建SpringBoot项目不太清楚的小伙伴,可以关注一下我之前的博文)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2. 测试接口
@GetMapping(path = "get")
public Mono<String> get(String name, Integer age) {
return Mono.just("req: " + name + " age: " + age);
}
@GetMapping(path = "mget")
public Flux<String> mget(String name, Integer age) {
return Flux.fromArray(new String[]{"req name: " + name, "req age: " + age});
}
II. 同步返回
需要同步返回结果还是比较简单的,获取对应的Mono/Flux之后调用一下block()
方法即可,但是需要注意,这里也有一个潜在的坑
1. 实现方式
public void sync() {
// 同步调用的姿势
// 需要特别注意,这种是使用姿势,不能在响应一个http请求的线程中执行;
// 比如这个项目中,可以通过 http://127.0.0.1:8080/test 来调用本类的测试方法;但本方法如果被这种姿势调用,则会抛异常;
// 如果需要正常测试,可以看一下test下的调用case
WebClient webClient = WebClient.create("http://127.0.0.1:8080");
String ans = webClient.get().uri("/get?name=一灰灰").retrieve().bodyToMono(String.class).block();
System.out.println("block get Mono res: " + ans);
Map<String, Object> uriVariables = new HashMap<>(4);
uriVariables.put("p1", "一灰灰");
uriVariables.put("p2", 19);
List<String> fans =
webClient.get().uri("/mget?name={p1}&age={p2}", uriVariables).retrieve().bodyToFlux(String.class)
.collectList().block();
System.out.println("block get Flux res: " + fans);
}
项目启动之后,我们写一个测试类来调用这个方法
@Test
public void sync() {
WebClientTutorial web = new WebClientTutorial();
web.sync();
}
如果我们换成下面这种写法,就会报错了
@GetMapping(path = "test")
public String test() {
WebClientTutorial web = new WebClientTutorial();
web.sync();
return "over";
}
III. 其他
0. 项目
系列博文
- 【WEB系列】WebClient之策略设置
- 【WEB系列】WebClient之非200状态码信息捕获
- 【WEB系列】WebClient之retrieve与exchange的使用区别介绍
- 【WEB系列】WebClient之超时设置
- 【WEB系列】WebClient之Basic Auth授权
- 【WEB系列】WebClient之请求头设置
- 【WEB系列】WebClient之文件上传
- 【WEB系列】WebClient之基础使用姿势
源码