以前文章介绍了:
RestTemplate:https://blog.csdn.net/zzhongcy/article/details/104674808
AsyncRestTemplate:https://blog.csdn.net/zzhongcy/article/details/105410316
这里介绍一下另外一个新兴的http客户端:WebClient
在Spring 5之前,如果我们想要调用其他系统提供的HTTP服务,我们通常可以使用Spring提供的RestTemplate来访问。
RestTemplate用法很简单,但是它的不足之处在于它的请求是同步阻塞模式,因此存在一定性能瓶颈。当然,如果想要使用异步方式请求,也可以使用AsyncRestTemplate。
从Spring 5开始,Spring中全面引入了Reactive响应式编程,WebClient就属于Spring WebFlux的一部分。WebClient的请求模式属于异步非阻塞、反应式的,能够以少量固定的线程处理高并发的HTTP请求。它对同步和异步以及流方案都有很好的支持。
因此,从Spring 5开始,HTTP服务之间的通信我们就可以考虑使用WebClient来取代之前的RestTemplate。
有个这样的说法:RestTemplate将在将来版本中弃用,并且不会向前添加主要新功能。
WebClient是一个功能完善的Http请求客户端,与RestTemplate相比,WebClient支持以下内容:
org.springframework.boot
spring-boot-starter-webflux
Spring5的WebClient客户端和WebFlux服务器都依赖于相同的非阻塞编解码器来编码和解码请求和响应内容。默认底层使用Netty,内置支持Jetty反应性HttpClient实现。同时,也可以通过编码的方式实现ClientHttpConnector接口自定义新的底层库;如切换Jetty实现:
WebClient.builder()
.clientConnector(new JettyClientHttpConnector())
.build();
WebClient实例构造器可以设置一些基础的全局的web请求配置信息,比如默认的cookie、header、baseUrl等
WebClient.builder()
.defaultCookie("kl","kl")
.defaultUriVariables(ImmutableMap.of("name","kl"))
.defaultHeader("header","kl")
.defaultHeaders(httpHeaders -> {
httpHeaders.add("header1","kl");
httpHeaders.add("header2","kl");
})
.defaultCookies(cookie ->{
cookie.add("cookie1","kl");
cookie.add("cookie2","kl");
})
.baseUrl("http://www.kailing.pub")
.build();
通过定制Netty底层库,可以配置SSl安全连接,以及请求超时,读写超时等。这里需要注意一个问题,默认的连接池最大连接500。获取连接超时默认是45000ms,你可以配置成动态的连接池,就可以突破这些默认配置,也可以根据业务自己制定。包括Netty的select线程和工作线程也都可以自己设置。
//配置动态连接池
//ConnectionProvider provider = ConnectionProvider.elastic("elastic pool");
//配置固定大小连接池,如最大连接数、连接获取超时、空闲连接死亡时间等
ConnectionProvider provider = ConnectionProvider.fixed("fixed", 45, 4000, Duration.ofSeconds(6));
HttpClient httpClient = HttpClient.create(provider)
.secure(sslContextSpec -> {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()
.trustManager(new File("E://server.truststore"));
sslContextSpec.sslContext(sslContextBuilder);
}).tcpConfiguration(tcpClient -> {
//指定Netty的select 和 work线程数量
LoopResources loop = LoopResources.create("kl-event-loop", 1, 4, true);
return tcpClient.doOnConnected(connection -> {
//读写超时设置
connection.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(10));
})
//连接超时设置
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.option(ChannelOption.TCP_NODELAY, true)
.runOn(loop);
});
WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
关于连接池的设置,据群友反馈,他们在使用WebClient是并发场景下会抛获取连接异常。异常如下:
Caused by: reactor.netty.internal.shaded.reactor.pool.PoolAcquireTimeoutException: Pool#acquire(Duration) has been pending for more than the configured timeout of 45000ms
后经博主深入研究发现,WebClient底层依赖库reactory-netty在不同的版本下,初始化默认TcpTcpResources策略不一样,博主在网关系统中使用的reactory-netty版本是0.8.3,默认创建的是动态的连接池,即使在并发场景下也没发生过这种异常。而在0.9.x后,初始化的是固定大小的连接池,这位群友正是因为使用的是0.9.1的reactory-netty,在并发时导致连接不可用,等待默认的45s后就抛异常了。所以,使用最新版本的WebClient一定要根据自己的业务场景结合博主上面的Netty HttpClient配置示例合理设置好底层资源。
针对特定的数据交互格式,可以设置自定义编解码的模式,如下:
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
configurer.customCodecs().decoder(new Jackson2JsonDecoder());
configurer.customCodecs().encoder(new Jackson2JsonEncoder());
})
.build();
WebClient.builder()
.exchangeStrategies(strategies)
.build();
uri构造时支持属性占位符,真实参数在入参时排序好就可以。同时可以通过accept设置媒体类型,以及编码。最终的结果值是通过Mono和Flux来接收的,在subscribe方法中订阅返回值。
WebClient client = WebClient.create("http://www.kailing.pub");
Mono result = client.get()
.uri("/article/index/arcid/{id}.html", 256)
.acceptCharset(StandardCharsets.UTF_8)
.accept(MediaType.TEXT_HTML)
.retrieve()
.bodyToMono(String.class);
result.subscribe(System.err::println);
如果需要携带复杂的查询参数,可以通过UriComponentsBuilder构造出uri请求地址,如:
//定义query参数
MultiValueMap params = new LinkedMultiValueMap<>();
params.add("name", "kl");
params.add("age", "19");
//定义url参数
Map uriVariables = new HashMap<>();
uriVariables.put("id", 200);
String uri = UriComponentsBuilder.fromUriString("/article/index/arcid/{id}.html")
.queryParams(params)
.uriVariables(uriVariables)
.toUriString();
下载文件时,因为不清楚各种格式文件对应的MIME Type,可以设置accept为MediaType.ALL,然后使用Spring的Resource来接收数据即可,如:
WebClient.create("https://kk-open-public.oss-cn-shanghai.aliyuncs.com/xxx.xlsx")
.get()
.accept(MediaType.ALL)
.retrieve()
.bodyToMono(Resource.class)
.subscribe(resource -> {
try {
File file = new File("E://abcd.xlsx");
FileCopyUtils.copy(StreamUtils.copyToByteArray(resource.getInputStream()), file);
}catch (IOException ex){}
});
@Test
public void testUrlPlaceholder(){
Mono resp = WebClient.create()
.get()
//多个参数也可以直接放到map中,参数名与placeholder对应上即可
.uri("http://www.baidu.com/s?wd={key}&other={another}","北京天气","test") //使用占位符
.retrieve()
.bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testUrlBiulder(){
Mono resp = WebClient.create()
.get()
.uri(uriBuilder -> uriBuilder
.scheme("http")
.host("www.baidu.com")
.path("/s")
.queryParam("wd", "北京天气")
.queryParam("other", "test")
.build())
.retrieve()
.bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
post请求示例演示了一个比较复杂的场景,同时包含表单参数和文件流数据。如果是普通post请求,直接通过bodyValue设置对象实例即可。不用FormInserter构造。
WebClient client = WebClient.create("http://www.kailing.pub");
FormInserter formInserter = fromMultipartData("name","kl")
.with("age",19)
.with("map",ImmutableMap.of("xx","xx"))
.with("file",new File("E://xxx.doc"));
Mono result = client.post()
.uri("/article/index/arcid/{id}.html", 256)
.contentType(MediaType.APPLICATION_JSON)
.body(formInserter)
//.bodyValue(ImmutableMap.of("name","kl"))
.retrieve()
.bodyToMono(String.class);
result.subscribe(System.err::println);
@Test
public void testFormParam(){
MultiValueMap formData = new LinkedMultiValueMap<>();
formData.add("name1","value1");
formData.add("name2","value2");
Mono 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);
LOGGER.info("result:{}",resp.block());
}
static class Book {
String name;
String title;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
@Test
public void testPostJson(){
Book book = new Book();
book.setName("name");
book.setTitle("this is title");
Mono resp = WebClient.create().post()
.uri("http://localhost:8080/demo/json")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Mono.just(book),Book.class)
.retrieve().bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testPostRawJson(){
Mono resp = WebClient.create().post()
.uri("http://localhost:8080/demo/json")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject("{\n" +
" \"title\" : \"this is title\",\n" +
" \"author\" : \"this is author\"\n" +
"}"))
.retrieve().bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testUploadFile(){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
MultiValueMap parts = new LinkedMultiValueMap<>();
parts.add("file", entity);
Mono resp = WebClient.create().post()
.uri("http://localhost:8080/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(parts))
.retrieve().bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
上面演示的都是异步的通过mono的subscribe订阅响应值。当然,如果你想同步阻塞获取结果,也可以通过.block()阻塞当前线程获取返回值。
WebClient client = WebClient.create("http://www.kailing.pub");
String result = client .get()
.uri("/article/index/arcid/{id}.html", 256)
.retrieve()
.bodyToMono(String.class)
.block();
System.err.println(result);
但是,如果需要进行多个调用,则更高效地方式是避免单独阻塞每个响应,而是等待组合结果,如:
WebClient client = WebClient.create("http://www.kailing.pub");
Mono result1Mono = client .get()
.uri("/article/index/arcid/{id}.html", 255)
.retrieve()
.bodyToMono(String.class);
Mono result2Mono = client .get()
.uri("/article/index/arcid/{id}.html", 254)
.retrieve()
.bodyToMono(String.class);
Map map = Mono.zip(result1Mono, result2Mono, (result1, result2) -> {
Map arrayList = new HashMap<>();
arrayList.put("result1", result1);
arrayList.put("result2", result2);
return arrayList;
}).block();
System.err.println(map.toString());
可以通过设置filter拦截器,统一修改拦截请求,比如认证的场景,如下示例,filter注册单个拦截器,filters可以注册多个拦截器,basicAuthentication是系统内置的用于basicAuth的拦截器,limitResponseSize是系统内置用于限制响值byte大小的拦截器
WebClient.builder()
.baseUrl("http://www.kailing.pub")
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.filters(filters ->{
filters.add(ExchangeFilterFunctions.basicAuthentication("username","password"));
filters.add(ExchangeFilterFunctions.limitResponseSize(800));
})
.build().get()
.uri("/article/index/arcid/{id}.html", 254)
.retrieve()
.bodyToMono(String.class)
.subscribe(System.err::println);
WebClient不支持websocket请求,请求websocket接口时需要使用WebSocketClient,如:
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
@Test
public void testWithCookie(){
Mono resp = WebClient.create()
.method(HttpMethod.GET)
.uri("http://baidu.com")
.cookie("token","xxxx")
.cookie("JSESSIONID","XXXX")
.retrieve()
.bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testWithBasicAuth(){
String basicAuth = "Basic "+ Base64.getEncoder().encodeToString("user:pwd".getBytes(StandardCharsets.UTF_8));
LOGGER.info(basicAuth);
Mono resp = WebClient.create()
.get()
.uri("http://baidu.com")
.header(HttpHeaders.AUTHORIZATION,basicAuth)
.retrieve()
.bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testWithHeaderFilter(){
WebClient webClient = WebClient.builder()
.defaultHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36")
.filter(ExchangeFilterFunctions
.basicAuthentication("user","password"))
.filter((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);
})
.build();
Mono resp = webClient.get()
.uri("https://baidu.com")
.retrieve()
.bodyToMono(String.class);
LOGGER.info("result:{}",resp.block());
}
@Test
public void testDownloadImage() throws IOException {
Mono resp = WebClient.create().get()
.uri("http://www.toolip.gr/captcha?complexity=99&size=60&length=9")
.accept(MediaType.IMAGE_PNG)
.retrieve().bodyToMono(Resource.class);
Resource resource = resp.block();
BufferedImage bufferedImage = ImageIO.read(resource.getInputStream());
ImageIO.write(bufferedImage, "png", new File("captcha.png"));
}
@Test
public void testDownloadFile() throws IOException {
Mono resp = WebClient.create().get()
.uri("http://localhost:8080/file/download")
.accept(MediaType.APPLICATION_OCTET_STREAM)
.exchange();
ClientResponse response = resp.block();
String disposition = response.headers().asHttpHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION);
String fileName = disposition.substring(disposition.indexOf("=")+1);
Resource resource = response.bodyToMono(Resource.class).block();
File out = new File(fileName);
FileUtils.copyInputStreamToFile(resource.getInputStream(),out);
LOGGER.info(out.getAbsolutePath());
}
@Test
public void testRetrieve4xx(){
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 mono = responseSpec
.onStatus(e -> e.is4xxClientError(),resp -> {
LOGGER.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 -> {
LOGGER.info("ERROR status:{},msg:{}",err.getRawStatusCode(),err.getResponseBodyAsString());
throw new RuntimeException(err.getMessage());
})
.onErrorReturn("fallback");
String result = mono.block();
LOGGER.info("result:{}",result);
}
https://mp.weixin.qq.com/s/udVIPbkuoHRyGbIgZDtrpg
我们已经在业务api网关、短信平台等多个项目中使用WebClient,从网关的流量和稳定足以可见WebClient的性能和稳定性。响应式编程模型是未来的web编程趋势,RestTemplate会逐步被取缔淘汰,并且官方已经不在更新和维护。WebClient很好的支持了响应式模型,而且api设计友好,是博主力荐新的HttpClient库。赶紧试试吧。
webclient是新一代的async rest template,api也相对简洁,而且是reactive的,非常值得使用。
https://segmentfault.com/a/1190000021133071
https://www.zifangsky.cn/1343.html
https://segmentfault.com/a/1190000012916413
https://stackoverflow.com/questions/44514263/decode-bytearray-with-spring-5-webflux-framework
https://www.594cto.com/content/049cf4caac6941a7b94f7190dcb0d66f
http://blog.didispace.com/spring-cloud-alibaba-2/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
https://mp.weixin.qq.com/s/udVIPbkuoHRyGbIgZDtrpg