-
2022-02-13 15:09:00
现在,越来越多的项目都开始使用反应式编程以及异步处理请求了。在
Spring 5
中,引入了反应式WebClient
实现作为WebFlux
框架的一部分。今天,我们就来学习下如何使用WebClient
反应式地请求 REST API。定义 REST API
首先,我们先定义一些
REST API
(假设我们数据库里保存了一系列的事件,这些事件有id、属性、分类及标签等):- /events - 获取所有的事件
- /events/[id] - 通过 id 获取事件
- /events/[id]/atrributes/[attributeId] - 通过属性id获取某个事件的属性
- /events?name=[name]&startDate=[startDate] - 根据给定条件获取事件
- /events?tag[]=[tag1]&tag[]=[tag2] - 根据标签获取事件
- /events?category=[category1]&category=[category2] - 根据分类获取事件
以上,我们定义了一些不同的
URI
。等下,我们就来看下如何使用WebClient
来构建和发送每种类型的URI
。需要注意的是,通过标签和分类查找事件都包含了数组查询参数,但是它们的语法不同。因为在
URI
中数组的表示没有严格定义,这主要取决于服务端的实现。在这里,我们两种方式都覆盖一下。WebClient 设置
首先,我们需要创建一个
WebClient
实例。在这篇文章中,我们使用mocked
的对象来验证是否请求了一个有效的URI
。我们先定义一个 client 以及相关的 mocked 对象。
this.exchangeFunction = mock(ExchangeFunction.class); ClientResponse mockResponse = mock(ClientResponse.class); when(this.exchangeFunction.exchange(this.argumentCaptor.capture())).thenReturn(Mono.just(mockResponse)); this.webClient = WebClient .builder() .baseUrl("https://example.com/api") .exchangeFunction(exchangeFunction) .build();
在上面的定义中,我们设置了一个参数
baseUrl
,webClient 会将这个参数的值添加到它发送的所有请求之前。最后,要验证特定的 URI 是否已经传递给底层的 ExchangeFunction 实例,我们需要使用以下方法:
private void verifyCalledUrl(String relativeUrl) { ClientRequest request = this.argumentCaptor.getValue(); Assert.assertEquals(String.format("%s%s", BASE_URL, relativeUrl, request.url().toString()); Mockito.verify(this.exchangeFunction).exchange(request); verifyNoMoreInteractions(this.exchangeFunction); }
WebClientBuilder
类具有将 UriBuilder 实例作为参数提供的 uri() 方法。通常,API 调用通过以下方式进行:this.webClient.get() .uri(uriBuilder -> uriBuilder // ... builder a URI .build()) .retrieve());
后面,我们将广泛使用
UriBuilder
来构建URI
。需要注意的是,我们可以使用任何其它方式构建 URI,然后将生成的 URI 作为字符串传递进去。URI 路径构成
URI 路径构成由一系列的由斜杠(/)分隔的路径段组成。首先,我们先看下最简单的没有任何可变段的
/events
简单案例。this.webClient.get() .uri("/events") .retrieve(); verifyCalledUrl("/events");
在这个例子中,我们仅传递了一个String类型的数据作为参数。
接着,我们使用
/events/{id}
访问点并构建相应的 URI:this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events/{id}") .build(2)) .retrieve(); verifyCalledUrl("/events/2");
从上面的代码中,我们可以看到实际的 {id} 值 2 被传递给了 biild() 方法。
同样我们可以为
/events/{id}/attributes/{attributeId}
访问点创建一个包含多个路径段的 URI:this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events/{id}/atrributes/{attributeId}") .build(3, 12)) .retrieve(); verifyCalledUrl("/events/3/attributes/13");
在最终的 URI 长度没有超出限制的情况下,一个 URI 可以有很多路径段。我们只需要确保传递给 build() 方法的实际字段值的顺序需要正确。
URI 查询参数
通常情况下,一个查询参数是一个简单的键值对,比如 name=dengkaiting。我们来看下如何构建这样的 URI。
单值参数
我们从单值参数开始,采用
/events?name=[name]&startDate=[startDate]
访问点。要设置查询参数,我们需要调用UriBuilder
接口的queryParam()
方法:this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events") .queryParam("name", "InitFailed") .queryParam("startDate", "13/02/2021") .build()) .retrieve(); verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021")
我们添加了两个查询参数并给他们设置了实际值。此外,我们也可以使用占位符而不立即设置实际值:
this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events") .queryParam("name", "{name}") .queryParam("startDate", "{startDate}") .build("InitFailed", "13/02/2021")) .retrieve(); verifyCalledUrl("/events?name=InitFailed&startTime=13%2F02%2F2021")
当我们需要在调用链中进一步传递参数时,后面这种方式就比较方便了。上面两个代码片段里有一个重要的区别:
我们可以看到对于预期的 URI,它们两个的编码方式是不同的。在后面这个例子中,斜线(/)被转义了。一般来说,RFC3986
不需要在查询中对斜杠进行编码。但是,某些服务端应用可能需要这种转换。我们在后面会介绍下如果更改此行为。数组参数
有时,我们可能需要传递一个值数组。其实,在查询字符串中传递数组并没有严格的限制。对于数组的表示通常取决于底层框架。下面,我们介绍最广泛使用的格式。
我们以
/events?tag[]=[tag1]&tag[]=[tag2]
访问点开始:this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events") .queryParam("tag[]", "node", "service") .build()) .retrieve(); verifyCalledUrl("/events?tag%5B%5D=node&tag%5B%5D=service")
最终的 URL 包含多个标记参数,后面紧跟编码后的方括号。
queryParam()
方法接受可变参数作为值,因此我们不需要多次调用该方法。同样地,我们还可以省略方括号,只传递具有相同键但值不同的多个查询参数 -
/events?category=[category1]&category=[category2]
this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events") .queryParams("category", "info", "warn") .build()) .retrieve(); verifyCalledUrl("/events?category=info&category=warn")
还有一种更广泛使用的对数组进行编码的方法是传递逗号分隔的值。我们把前面的示例转为逗号分隔值:
this.webClient.get() .uri(uriBuilder -> uriBuilder .path("/events") .queryParam("category", String.join(",", "info", "warn")) .build()) .retrieve(); verifyCalledUrl("/events?category=info,warn");
我们只是使用 String 类的 join() 方法创建了一个逗号分隔的字符串。当然,我们可以使用应用期望的任何其它分隔符。
编码模式
我们在上面提到了 URL 的编码。
如果默认行为不符合我们的要求,我们可以更改它。我们在创建
WebClient
实例的时候,可以提供一个UriBuilderFactory
的实现来更改默认的编码模式。这里,我们使用DefaultBuilderFactory
类。要设置编码,需要调用setEncodingMode()
方法。可以使用的模式有:- TEMPLATE_AND_VALUES:对 URI 模板进行预编码,扩展时对 URI 变量进行严格编码
- VALUES_ONLY: 不对 URL 模板进行编码,而是将 URI 变量展开成模板后严格编码
- URI_COMPONENTS:扩展 URI 变量后编码 URI 的组件值
- NONE:不会应用任何编码
默认值是
TEMPLATE_AND_VALUES
。我们把模式设置成URI_COMPONENTS
。DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL); factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT); this.webClient = WebClient .builder() .uriBuilderFactory(factory) .baseUrl(BASE_URL) .exchangeFunction(exchangeFunction) .build();
最终,下面的断言可以成功:
this.webClient.get() .uri(uriBuilder -> uriBuilder) .path("/events") .queryParam("name", "InitFailed") .queryParam("startDate", "13/02/2021") .build() .retrieve(); verifyCalledUrl("/events?name=InitFailed&startDate=13/02/2021");
当然,我们也可以提供一个完全自定义的 URIBuilderFactory 实现来手动处理 URI 创建。
结论
在这篇文章中,我们了解了如何使用
WebClient
和DefaultUriBuilder
构建不同类型的 URI。在此过程中,我们介绍了各种类型和格式的查询参数。最后,我们更改了 URL 构建器的默认编码模式。
标题:Spring WebClient 使用简介
作者:末日没有进行曲
链接:link
时间:2021-02-12
声明:本博客所有文章均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。更多相关内容 -
Spring5中的WebClient使用方法详解
2020-08-25 08:47:50主要给大家介绍了关于Spring5中WebClient使用方法的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Spring5具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 -
spring5 webclient使用指南详解
2020-08-28 05:08:35本篇文章主要介绍了spring 5 webclient使用指南详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 -
Spring WebClient使用
2021-03-09 03:39:39如下为一个使用了线程池,SSL,出现指定异常后充实,超时配置的demoSSL配置packagecom.demo.client;importjava.io.FileInputStream;importjava.io.InputStream;importjava.security.KeyStore;importjava.security....如下为一个使用了线程池,SSL,出现指定异常后充实,超时配置的demo
SSL配置
packagecom.demo.client;importjava.io.FileInputStream;importjava.io.InputStream;importjava.security.KeyStore;importjava.security.PublicKey;importjava.security.cert.CertificateFactory;importjava.security.cert.X509Certificate;importjava.util.ArrayList;importjava.util.LinkedList;importjava.util.List;importjava.util.function.Consumer;importjavax.net.ssl.KeyManagerFactory;importjavax.net.ssl.SNIMatcher;importjavax.net.ssl.SNIServerName;importjavax.net.ssl.SSLEngine;importjavax.net.ssl.SSLParameters;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importio.netty.handler.ssl.SslContext;importio.netty.handler.ssl.SslContextBuilder;importreactor.netty.tcp.SslProvider.SslContextSpec;public class SslConsumer implements Consumer{private Logger logger = LoggerFactory.getLogger(SslConsumer.class);privateString keyStorePath;private String password = "123456";private boolean trustServer = true;//测试,默认为true
private String serverCaPath = "";publicSslConsumer(String keyStorePath) {this.keyStorePath =keyStorePath;
}
@Overridepublic voidaccept(SslContextSpec t) {try{
t.sslContext(createSslContext(loadKeyStore(keyStorePath))).handlerConfigurator(handler->{
SSLEngine engine=handler.engine();
List matchers = new LinkedList();
SNIMatcher matcher= new SNIMatcher(0) {
@Overridepublic booleanmatches(SNIServerName serverName) {//返回true,不验证主机名
return true;
}
};
matchers.add(matcher);
SSLParameters params= newSSLParameters();
params.setSNIMatchers(matchers);
engine.setSSLParameters(params);
});
}catch(Exception e) {
}
}private SslContext createSslContext(KeyStore keyStore) throwsException {
SslContextBuilder builder=SslContextBuilder.forClient();
KeyManagerFactory keyMgrFactory= KeyManagerFactory.getInstance("SunX509");
keyMgrFactory.init(keyStore, password.toCharArray());
builder.keyManager(keyMgrFactory);
SSLX509TrustMgr trustMgr= null;if(trustServer) {
trustMgr= newSSLX509TrustMgr();
}else{
trustMgr= newSSLX509TrustMgr(getSeverPublicKey(serverCaPath));
}
builder.trustManager(trustMgr);
List ciper = new ArrayList();
ciper.add("TLS_RSA_WITH_AES_128_GCM_SHA256");
ciper.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
ciper.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
builder.ciphers(ciper);returnbuilder.build();
}privateKeyStore loadKeyStore(String keyStorePath) {
KeyStore keyStore= null;
InputStream in= null;try{
in= newFileInputStream(keyStorePath);
keyStore=KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(in, password.toCharArray());
}catch(Exception e) {
logger.error("", e);
}returnkeyStore;
}privatePublicKey getSeverPublicKey(String serverCaPath) {
PublicKey key= null;
InputStream in= null;try{
in= newFileInputStream(serverCaPath);
X509Certificate cert= (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
key=cert.getPublicKey();
}catch(Exception e) {
logger.error("", e);
}returnkey;
}
}
验证服务器整数实现类
目前没有做任何处理,有需要验证服务器证书时,可以在对应的重写方法中进行处理
packagecom.demo.client;importjava.security.PublicKey;importjava.security.cert.CertificateException;importjava.security.cert.X509Certificate;importjavax.net.ssl.X509TrustManager;public class SSLX509TrustMgr implementsX509TrustManager {privatePublicKey serverPublicKey;publicSSLX509TrustMgr() {
}publicSSLX509TrustMgr(PublicKey serverPublicKey) {this.serverPublicKey =serverPublicKey;
}
@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType) throwsCertificateException {//TODO Auto-generated method stub
}
@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType) throwsCertificateException {//TODO Auto-generated method stub
}
@OverridepublicX509Certificate[] getAcceptedIssuers() {//TODO Auto-generated method stub
return null;
}
}
WebClient工具类
packagecom.demo.utils;importjava.net.ConnectException;importjava.net.NoRouteToHostException;importjava.time.Duration;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.http.MediaType;importorg.springframework.http.client.reactive.ReactorClientHttpConnector;importorg.springframework.web.reactive.function.client.WebClient;importcom.demo.entity.SslConsumer;importio.netty.channel.ChannelOption;importio.netty.channel.ConnectTimeoutException;importio.netty.handler.timeout.ReadTimeoutHandler;importio.netty.handler.timeout.WriteTimeoutHandler;importreactor.core.publisher.Mono;importreactor.netty.http.client.HttpClient;importreactor.netty.resources.ConnectionProvider;importreactor.netty.resources.LoopResources;importreactor.netty.tcp.TcpClient;importreactor.retry.Backoff;importreactor.retry.Retry;public classWebClientUtil {private static Logger logger = LoggerFactory.getLogger(WebClientUtil.class);private static Map webClientMap = new ConcurrentHashMap();public staticWebClient createWebClient(String keyStorePath) {
ConnectionProvider provider= ConnectionProvider.builder("wc-").maxConnections(30)
.maxIdleTime(Duration.ofSeconds(30)).maxLifeTime(Duration.ofSeconds(30))
.pendingAcquireTimeout(Duration.ofSeconds(30)).build();
LoopResources loop= LoopResources.create("loop-", 20, 20, true);
TcpClient tcpClient= TcpClient.create(provider).secure(newSslConsumer(keyStorePath)).runOn(loop)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,2000).doOnConnected(conn ->conn
.addHandlerLast(new ReadTimeoutHandler(30)).addHandlerLast(new WriteTimeoutHandler(20)));return WebClient.builder().clientConnector(newReactorClientHttpConnector(HttpClient.from(tcpClient))).build();//如下方式可能会一直循环生成loop线程,直到程序僵死//HttpClient httpClient = HttpClient.create(provider).secure(new SslConsumer(keyStorePath))//.tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)//.doOnConnected(con -> con.addHandlerLast(new ReadTimeoutHandler(20))//.addHandlerLast(new WriteTimeoutHandler(10)))//.runOn(LoopResources.create("loop-", 20, 20, true)));//
//return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}public static MonodoPost(String url, String data, String keyStorePath) {
WebClient webClient=webClientMap.get(url);if (webClient == null) {
webClient=createWebClient(keyStorePath);
WebClient putIfAbsent=webClientMap.putIfAbsent(url, webClient);if (putIfAbsent != null) {
webClient=putIfAbsent;
}
}
Retry> retry = Retry.anyOf(ConnectTimeoutException.class, NoRouteToHostException.class).retryMax(3)
.backoff(Backoff.fixed(Duration.ofMillis(100)));
Mono mono =webClient.post().uri(url).contentType(MediaType.APPLICATION_JSON).bodyValue(data).retrieve()
.bodyToMono(String.class).timeout(Duration.ofSeconds(30)).doOnError(ConnectException.class, e ->{
logger.error("", e);
}).doOnError(NoRouteToHostException.class, e ->{
logger.error("", e);
}).retryWhen(retry);returnmono;
}
}
1、demo使用的springboot版本为2.3.3
2、HttpClient的create方法与secure方法不能分开,否则secure方法可能不生效
3、 ConnectionProvider中有很多方法已经过时,比如fixed,elastic,不建议使用,在过时的方法上,源码中也给出了对应替换使用的方法,比如fixed可以使用create方法替代,elastic方法可以用builder方法替代,但create方法直接创建的就为ConnectionProvider,重载的方法可以设置maxConnections,但不能设置连接最大空闲时间,连接最大生命周期等,builder方法可以对线程更精细的管理,故本例使用的builder方法和build来创建ConnectionProvider,如果使用默认的provider,线程数默认500,且maxIdleTime和maxLifeTime为-1
4、LoopResources为处理响应消息的线程数,2.3.3版本最小值为4
5、ReactorClientHttpConnector存在一个重构方法
在该方法中,factory可以设置ConnectionProvider和LoopResources,mapper可以设置HttpClient,但在使用时可能是使用方式不正确,一直都是要么线程池配置上了,但SSL不可用,或者是SSL可用,但线程池不可用,后在ReactorClientHttpConnector两个参数的构造方法中看到initHttpClient方法,该方法中,使用了runOn配置LoopResources,故本例中也是用了runOn来配置。
测试:
@Overridepublic void run(String... args) throwsException {int count = 6;for (int i = 0; i < count; i++) {
logger.info("start: {}", i);
JsonObject obj= newJsonObject();
obj.addProperty("name", "name"+i);
obj.addProperty("age", i);
Mono mono =WebClientUtil.doPost(url, obj.toString(), keyStorePath);
mono.subscribe(newMainConsumer(i));
logger.info("end: {}", i);
}
}class MainConsumer implements Consumer{private inti;public MainConsumer(inti) {this.i =i;
}
@Overridepublic voidaccept(String t) {
logger.info("receive: {}, loop: {}", t, i);
}
}
View Code
结果:
[INFO] main 14:44:30 WebclientdemoApplication:61(logStarted) Started WebclientdemoApplication in 3.453 seconds (JVM running for 4.137)
[INFO] main 14:44:30 MainBusiStart:29(run) start: 0
[INFO] main 14:44:31 MainBusiStart:35(run) end: 0
[INFO] main 14:44:31 MainBusiStart:29(run) start: 1
[INFO] main 14:44:31 MainBusiStart:35(run) end: 1
[INFO] main 14:44:31 MainBusiStart:29(run) start: 2
[INFO] main 14:44:31 MainBusiStart:35(run) end: 2
[INFO] main 14:44:31 MainBusiStart:29(run) start: 3
[INFO] main 14:44:31 MainBusiStart:35(run) end: 3
[INFO] main 14:44:31 MainBusiStart:29(run) start: 4
[INFO] main 14:44:31 MainBusiStart:35(run) end: 4
[INFO] main 14:44:31 MainBusiStart:29(run) start: 5
[INFO] main 14:44:31 MainBusiStart:35(run) end: 5
[INFO] loop--nio-4 14:44:31 MainBusiStart:48(accept) receive: {"name":"name3","age":3.0}, loop: 3
[INFO] loop--nio-5 14:44:31 MainBusiStart:48(accept) receive: {"name":"name4","age":4.0}, loop: 4
[INFO] loop--nio-1 14:44:31 MainBusiStart:48(accept) receive: {"name":"name0","age":0.0}, loop: 0
[INFO] loop--nio-2 14:44:31 MainBusiStart:48(accept) receive: {"name":"name1","age":1.0}, loop: 1
[INFO] loop--nio-3 14:44:31 MainBusiStart:48(accept) receive: {"name":"name2","age":2.0}, loop: 2
[INFO] loop--nio-6 14:44:31 MainBusiStart:48(accept) receive: {"name":"name5","age":5.0}, loop: 5
View Code
6、本例中,使用了一个reactor-extra进行重试处理
io.projectreactor.addons
reactor-extra
但这种重试的方法虽然比较简单好用,但已经被标记为过时
应该使用传递一个reactor.util.retry.Retry类型参数的方法
7、如果使用springclloud,则需要考虑springboot和springcloud的兼容问题,以及当前程序的springboot和springcloud版本与其他需要远程调用的版本的兼容
-
WebClient 使用学习记录
2021-06-02 15:37:15文章目录前言一、相关技术背景介绍二、IO相关知识点三、 WebClient的使用3.1 引入依赖3.2 创建实例3.3 Get, Mono是WebClint返回的结果类型3.4 POST 普通形式3.5 POST 业务需求,参数通过读取文件获取,文件内容是...文章目录
前言
最近接手了一个新的需求,需要使用到网络请求相关,经过思考决定使用比restTemplate更新的WebClient。
WebClient是Spring 5.0推出的用来替代restTemplate的非阻塞式Reactive响应式编程思想,并且为网络编程提供相关响应式编程的支持.
一、相关技术背景介绍
相比传统阻塞IO模式,每次连接都需要独立的线程,导致并发量变大后,会产生大量线程,占用很大一部分的系统资源。
为了解决Spring MVC等框架在高并发场景下的性能瓶颈,推出了Spring WebFlux,Spring WebFlux是基于响应式流的,因此可以用来建立异步的、非阻塞的、事件驱动的服务。它采用Reactor作为首选的响应式流的实现库。
由于响应式编程的特性,Spring WebFlux和Reactor底层需要支持异步的运行环境,比如Netty(非阻塞IO通信框架)。Netty 是一个广泛使用的 Java 网络编程框架,前面说过,推出WebFlux是为了提高并发的,为什么选择Netty,是因为Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),NIO的并发性能有很大的提高。
二、IO相关知识点
-
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
-
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
-
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
上面我们知道了,BIO每次请求都要一次线程,NIO一个线程能处理多个请求,这是为什么呢?
原因就是那个多路复用器,当一个连接建立之后,他有两个步骤要做,
- 第一步是接收完客户端发过来的全部数据
- 第二步是服务端处理完请求业务之后返回response给客户端。
NIO和BIO的区别主要是在第一步。
在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
而NIO中,当一个Socket建立好之后,线程并不会阻塞去接受这个Socket,而是将这个请求交给多路复用器,多路复用器会不断的去遍历所有的请求连接,一旦有一个请求连接建立完成,他会通知线程,然后线程处理完数据再返回给客户端——这个过程是不阻塞的,这样就能让一个线程处理更多的请求了。除了BIO和NIO,还有其他几种IO模型
- BIO,同步阻塞IO,阻塞整个步骤,如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
- NIO,同步非阻塞IO,阻塞业务处理但不阻塞数据接收,适用于高并发且处理简单的场景,比如聊天软件。
- 多路复用IO,他的两个步骤处理是分开的,也就是说,一个连接可能他的数据接收是线程a完成的,数据处理是线程b完成的,他比BIO能处理更多请求。
- 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
- 异步IO,他的数据请求和数据处理都是异步的,数据请求一次返回一次,适用于长连接的业务场景。
其中,Java的NIO对应linux的multiplexing io,也就是多路复用IO。
三、 WebClient的使用
3.1 引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <!-- <version>2.8.2</version>--> </dependency>
3.2 创建实例
WebClient webClient = WebClient.create(); // 如果是调用特定服务的API,可以在初始化webclient 时使用,baseUrl WebClient webClient = WebClient.create("https://api.github.com");
或者使用构造器
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();
3.3 Get, Mono是WebClint返回的结果类型
Mono<String> resp = WebClient.create() .method(HttpMethod.GET) .uri("http://www.baidu.com") .cookie("token","xxxx") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .retrieve().bodyToMono(String.class);
3.4 POST 普通形式
@Test public void testFormParam(){ MultiValueMap<String, String> formData = new LinkedMultiValueMap(); formData.add("name1","value1"); formData.add("name2","value2"); Mono<String> resp = WebClient.create().post() .uri("http://www.w3school.com.cn/test/demo_form.asp") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(BodyInserters.fromFormData(formData)) .retrieve().bodyToMono(String.class); System.out.print("result:" + resp.block()); }
3.5 POST 业务需求,参数通过读取文件获取,文件内容是JSON格式
其实重要的是动态生成MultiValueMap的,其他一样
// 读取request.body文件的数据 byte[] bytes; try { try(InputStream inputStream = new FileInputStream(requestParameter.getBodyFile())) { bytes = IOUtils.toByteArray(inputStream); } } catch (IOException e) { log.error(message); return Mono.error(new ServiceRequestException(message, e)); } // 字节数组转字符串 String strBody = new String(bytes, "UTF-8"); // 字符串转Map Gson gson = new Gson(); Map<String, Object> requestParams = new HashMap<String, Object>(); requestParams = gson.fromJson(strBody, requestParams.getClass()); // 动态构建参数 MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); requestParams.forEach((key, value) -> { if (value instanceof String) { map.add(key.toString(), value.toString()); } else { map.add(key.toString(), JsonUtil.serializeObject(value)); } }); String url = requestParameter.getUrl(); String method = requestParameter.getMethod().toString(); Mono<ResponseResult> mono = null; if (method.equalsIgnoreCase("post")){ mono = webClient.post().uri(url).bodyValue(map).exchange().flatMap(response -> { return responsePackager.pack(requestParameter.getId(), requestParameter.getServiceName(), response,requestParamsStr); }); System.out.println("customResult[POST]:" + mono.block()); } ResponseResult block = mono.block(); System.out.println(block.toString());
在body使用MultiValueMap类型的参数,如果没设置,默认就是表单的形式发送。
3.6 retrieve和exchange的区别
retrieve方法是直接获取响应body,但是,如果需要响应的头信息、Cookie等,可以使用exchange方法,该方法可以访问整个ClientResponse。
四、参考材料
-
-
Spring的WebClient使用
2020-12-29 19:51:42WebClient使用 引言 Spring Framework 5 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对REST,HTML浏览器和 WebSocket风格交互的响应式服务器Web应用程序的支持。...WebClient使用
引言
Spring Framework 5 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对REST,HTML浏览器和 WebSocket风格交互的响应式服务器Web应用程序的支持。本文主要介绍WebClient的使用,包括通过WebClient请求接口及实现接口的文件上传下载。
1.引入依赖
在pom.xml中引入WebClient所需的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.projectreactor</groupId> <artifactId>reactor-spring</artifactId> <version>1.0.1.RELEASE</version> </dependency>
2.一个简单的请求示例
请求百度首页示例
// 1.创建WebClient实例 WebClient webClient = WebClient .builder() .baseUrl("https://www.baidu.com") .build(); // 2.通过通过retrieve()请求并获得响应 Mono<String> bodyToMono = webClient .get() .retrieve() // 将请求结果处理为String类型 .bodyToMono(String.class); log.info("请求返回的数据内容:{}", bodyToMono.block());
3.WebClient配置
在创建WebClient时可对一些变量进行默认设置
WebClient client3 = WebClient .builder() .baseUrl("http://localhost:8080") .defaultCookie("cookieKey", "cookieValue") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080")) .build();
也可在发起请求时填入cookie、header等参数
webClient .get() .uri("https://www.baidu.com") .cookie("cookieKey", "cookieValue") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .retrieve();
4.在请求中传参
在日常使用中传递参数的方法
WebClient webClient = WebClient.create(); Mono<String> bodyToMono = webClient.post() .uri(uriBuilder -> uriBuilder .scheme("https") .host("localhost:8080") .path("/api/test") .queryParam("id", "123") .queryParam("invalid", "invalid123") .build() ) // requestbody,也可以使用MultiValueMap<String, String> map = new LinkedMultiValueMap<>(),传入参数,发起form提交。或直接使用HashMap传参 .syncBody(new Student()) .retrieve() .bodyToMono(String.class);
5.设置连接超时时间
设置请求超时时间
TcpClient tcpClient = TcpClient .create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .doOnConnected(connection -> { connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)); connection.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)); }); WebClient client = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) .build();
6.文件上传
上传文件代码
HttpHeaders headers = new HttpHeaders(); headers.add("content-type", "application/x-www-form-urlencoded"); HttpEntity<ClassPathResource> entity = new HttpEntity<>(new ClassPathResource("a.txt"), headers); MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); parts.add("file", entity); Mono<String> bodyToMono = WebClient.create().post() .uri("http://localhost:8080/upload") .contentType(MediaType.MULTIPART_FORM_DATA) .body(BodyInserters.fromMultipartData(parts)) .retrieve().bodyToMono(String.class);
7.文件下载
通过将.retrieve()方法换成.exchange(),可以获取到响应的头信息、Cookie等信息,与restTemplate相似。
Mono<ClientResponse> responseMono = WebClient.create().get() .uri("http://localhost:8080/file/download") .accept(MediaType.APPLICATION_OCTET_STREAM) .exchange(); ClientResponse response = responseMono.block(); // 可从headers中获取filename等信息 ClientResponse.Headers headers = response.headers(); Resource resource = response.bodyToMono(Resource.class).block(); // 获取文件流 InputStream inputStream = resource.getInputStream();
8.WebTestClient
The WebTestClient is the main entry point for testing WebFlux server endpoints. It has a very similar API to the WebClient, and it delegates most of the work to an internal WebClient instance focusing mainly on providing a test context. The DefaultWebTestClient class is a
single interface implementation.主要意思是WebTestClient用于测试WebFlux服务器端点,他和WebClient相似,且将大部分工作委托给了内部的WebClient,主要用于测试上下文,此部分后期在单独整理。
-
webclient使用介绍
2019-12-14 23:49:16webclient 和webflux(参考(webflux)) 这一系列,我们可以开好多节课程来讲解 什么是webclient,在spring5中,出现了reactive 响应式编程思想(参考响应式编程文章),并且为网络编程提供相关响应式编程的支持,... -
SpringBoot - 网络请求客户端WebClient使用详解
2021-08-17 10:31:16在 Spring 5 之前,如果我们想要调用其他系统提供的 HTTP 服务,通常可以使用 ... 作为替代,Spring 官方已在 Spring 5 中引入了 WebClient 作为非阻塞式 Reactive HTTP 客户端。下面通过样例演示如何使用 WebClie -
C#中在WebClient中使用post发送数据实现方法
2020-09-04 07:41:07主要介绍了C#中在WebClient中使用post发送数据实现方法,需要的朋友可以参考下 -
SpringBoot - 网络请求客户端WebClient使用详解3(POST请求)
2021-02-27 21:44:48四、POST 请求1,发送一个 JSON 格式数据(使用 json 字符串)(1)下面代码使用 post方式发送一个 json格式的字符串,并将结果打印出来(以字符串的形式)。@RestControllerpublic class HelloController {// 创建 ... -
Spring WebFlux (5): WebClient使用
2020-08-13 13:20:45可以使用WebFlux的WebClient类很方便的对网络请求进行代理处理操作,我想Spring Cloud Gateway中主要用的就是WebClient进行操作,这里简单介绍一下WebClient的用法,想要深度学习的话,看一下Spring Cloud Gateway... -
spring 5 webclient使用指南
2019-06-13 10:39:33WebClient是从Spring WebFlux 5.0版本开始提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具。它的响应式编程的基于Reactor... 下面是链接是讲解webclient的使用指南,推荐一下,感兴趣的可以学习一下。... -
java相关:spring5 webclient使用指南详解
2021-03-18 00:58:53java相关:spring5 webclient使用指南详解发布于 2020-3-23|复制链接本篇文章主要介绍了spring 5 webclient使用指南详解,小妖觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小妖过来看看吧之前写了一篇... -
rt-thread中webclient使用
2021-12-17 09:11:25rt_thread中webclient 使用记录 -
SpringBoot - 网络请求客户端WebClient使用详解(异常处理)
2021-08-17 11:31:51SpringBoot - 网络请求客户端WebClient使用详解(异常处理) 请求异常处理 1,默认异常 (1)当我们使用 WebClient 发送请求时, 如果接口返回的不是 200 状态(而是 4xx、5xx 这样的异常状态),则会抛出 ... -
Webflux函数式编程模型以及WebClient使用
2022-03-22 10:32:11Webflux函数式编程模型以及WebClient使用 (1)在使用函数式编程模型操作的时候,需要自己初始化服务器 (2)基于函数式编程模型,有两个核心接口:RouterFunction(实现路由功能,请求转发给对应handler)和Handler... -
WEBCLIENT使用方法
2013-11-06 17:04:11WEBCLIENT使用方法 -
(转发) spring 5 webclient使用指南
2019-02-19 16:00:45spring 5 webclient使用指南 请求携带header 携带cookie @Test public void testWithCookie(){ Mono<String> resp = WebClient.create() .method(HttpMethod.GET) .uri("... -
Spring5的WebClient使用详解
2020-04-09 16:34:461 前言 以前文章介绍了: RestTemplate:https://blog.csdn.net/zzhongcy/article/details/104674808 AsyncRestTemplate:... 这里介绍一下另外一个新兴的http客户端:WebClient 2 ... -
springboot webflux webclient 使用说明
2020-06-23 10:26:26springbootwebfluxwebclient使用说明 -
SpringBoot使用WebClient发送请求
2021-11-02 17:20:01文章目录前言在SpringBoot中使用1、...本文主要说一下 WebClient的基本使用。 主要原因及场景如下: 在一个类似监听机器的项目中,需要发送警告到某群聊。 这种警告可能会堆积,需要异步发送出去。 官方文档如下: ... -
spring 5 webclient使用指南-转
2019-06-21 20:11:56spring 5 webclient使用指南 序 之前写了一篇restTemplate使用实例,由于spring 5全面引入reactive,同时也有了restTemplate的reactive版webclient,本文就来对应展示下webclient的基本使用。 请求携带header ... -
springboot webflux webclient 使用示例
2020-06-23 11:06:20springbootwebfluxwebclient使用示例 -
WebClient使用http访问服务器
2012-03-10 19:51:06Symbian S60中使用http协议访问服务器资源的例子 -
Spring的WebClient基本使用
2021-02-12 14:21:25WebClient是从Spring WebFlux 5.0版本开始提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具。它的响应式编程的基于Reactor的。WebClient中提供了标准Http请求方式对应的get、post、put、delete等方法,... -
WebClient的使用示例
2018-11-02 17:08:12从Web站点检索数据,使用OpenRead()方法返回一个Stream引用。ReadLine()方法从数据流中以文本的形式获取数据。 下例从百度网页上读取数据,然后逐行显示在屏幕上。 static void Main(string[] args) { ...