对其他服务进行 HTTP 调用是 Web 应用程序中的常见要求。所以,我们需要一个网络客户端工具。
1、RestTemplate也是阻塞的
长期以来,Spring 一直提供RestTemplate作为 Web 客户端抽象。在底层,RestTemplate使用 Java Servlet API,它基于每请求线程模型。
这意味着线程将阻塞,直到 Web 客户端收到响应。阻塞代码的问题是由于每个线程消耗了一些内存和 CPU 周期。
当应用程序中有大量的请求时,就会有大量的线程和连接。这会给服务器资源带来负担。
如果服务器速度很慢,用户很快就会开始看到应用程序的性能下降甚至无响应。
2、WebClient 是非阻塞的
WebClient使用 Spring Reactive 框架提供的异步、非阻塞解决方案。
RestTemplate为每个事件(HTTP 调用)使用调用者线程,而WebClient将为每个事件创建类似于“任务”的东西。在幕后,Reactive 框架会将这些“任务”排队并仅在适当的响应可用时执行它们。
Reactive 框架使用事件驱动的架构。它提供了通过Reactive Streams API组合异步逻辑的方法。因此,与同步/阻塞方法相比,反应式方法可以处理更多逻辑,同时使用更少的线程和系统资源。
WebClient是Spring WebFlux库的一部分。因此,我们还可以使用具有反应类型(Mono和Flux)的功能性、流畅的 API 作为声明性组合来编写客户端代码。
3、JDK11的HttpClient
JDK11的HttpClient可以发送同步请求、也可以发送异步请求。windows64为的JDK11的下载地址,oracle官方的下载有些慢。
链接:https://pan.baidu.com/s/1LaM0b1qv2eG01ITd6vxg6g
提取码:tiq4
同步方法
public abstract <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException
异步方法
public abstract <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
4、比较示例
为了演示这三种方法之间的性能差异,我们需要进行多并发客户端请求进行性能测试。
在一定数量的并行客户端请求之后,我们会看到阻塞方法的性能显着下降。
但是,无论请求的数量如何,反应式/非阻塞方法都应该提供恒定的性能。
对于本文,我们将实现三个REST接口,一个使用RestTemplate,另一个使用WebClient,最后一个使用jdk11中的HttpClient。
首先,我们需要Spring Boot WebFlux 启动器依赖项(建议修改idea的maven远程仓库地址):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
下面的代码我们的服务,返回一个list,但是休眠2秒。
private final static Logger log = LoggerFactory.getLogger(UserController.class);
private final String url = "http://127.0.0.1:9001/demo-service";
@GetMapping("/demo-service")
private List<String> getAllTweets() {
try {
Thread.sleep(2000L); // delay
} catch (InterruptedException e) {
e.printStackTrace();
}
return Arrays.asList("RestTemplate","WebClient","HttpClient");
}
(1)使用RestTemplate调用服务
现在让我们实现另一个 REST,它将通过 Web 客户端调用我们的服务。
@GetMapping("/resttemplate-blocking")
public List<String> getResttemplateBlocking() {
log.info("请求开始");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<List<String>> response = restTemplate.exchange(
url, HttpMethod.GET, null,
new ParameterizedTypeReference<List<String>>(){});
List<String> result = response.getBody();
log.info("请求完成");
return result;
}
当我们调用这个端点时,由于RestTemplate的同步特性,代码将阻塞等待来自我们服务的响应。此方法中的其余代码将仅在收到响应时运行。
以下是我们将在日志中看到的内容,按时间看确认是阻塞的:
2022-05-10 19:39:07.740 INFO 21088 --- [nio-9001-exec-1] c.h.skydance.controller.UserController : 请求开始
2022-05-10 19:39:09.799 INFO 21088 --- [nio-9001-exec-1] c.h.skydance.controller.UserController : 请求完成
(2)使用WebClient调用服务
@GetMapping(value = "/flux-non-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getFluxNonBlocking() {
log.info("Flux请求开始!");
Flux<String> flux = WebClient.create()
.get()
.uri(url)
.retrieve()
.bodyToFlux(String.class);
log.info("Flux请求结束!");
flux.subscribe(result -> log.info(result.toString()));
log.info("Flux数据接收完成!");
return flux;
}
在这种情况下,WebClient返回一个Flux发布者,方法执行完成。一旦结果可用,发布者将开始向其订阅者发送推文。
让我们参考代码观察一下日志,确认是非阻塞的,请注意,此endpoint方法在收到响应之前完成。
2022-05-10 19:40:38.551 INFO 21088 --- [nio-9001-exec-9] c.h.skydance.controller.UserController : Flux请求开始!
2022-05-10 19:40:38.552 INFO 21088 --- [nio-9001-exec-9] c.h.skydance.controller.UserController : Flux请求结束!
2022-05-10 19:40:38.552 INFO 21088 --- [nio-9001-exec-9] c.h.skydance.controller.UserController : Flux数据接收完成!
2022-05-10 19:40:40.574 INFO 21088 --- [ctor-http-nio-2] c.h.skydance.controller.UserController : ["RestTemplate","WebClient","HttpClient"]
(3)使用JDK11的HttpClient的同步方式
@GetMapping(value = "/httpclient-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public String getHttpclientBlocking() {
log.info("httpclient send请求开始");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
HttpResponse<String> response = null;
try {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("httpclient send请求结束");
return response.body();
}
查看日志输出,是阻塞式的。
2022-05-10 19:42:27.862 INFO 21088 --- [nio-9001-exec-3] c.h.skydance.controller.UserController : httpclient send请求开始
2022-05-10 19:42:29.884 INFO 21088 --- [nio-9001-exec-3] c.h.skydance.controller.UserController : httpclient send请求结束
(4)使用JDK11的HttpClient的异步方式
@GetMapping(value = "/httpclient-non-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public String getHttpclientAsyncNonBlocking() {
try {
log.info("httpclient sendAsync请求开始!");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
CompletableFuture<String> result = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body);
log.info("httpclient sendAsync请求结束!");
String arr = result.get();
log.info("httpclient sendAsync请求结束 打印请求结果:" + arr);
return arr;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
查看结果日志的顺序,请求是非阻塞的,但是与Flux不同,这里的CompletableFuture的get方法是阻塞的。
2022-05-10 19:43:42.396 INFO 21088 --- [nio-9001-exec-2] c.h.skydance.controller.UserController : httpclient sendAsync请求开始!
2022-05-10 19:43:42.398 INFO 21088 --- [nio-9001-exec-2] c.h.skydance.controller.UserController : httpclient sendAsync请求结束!
2022-05-10 19:43:44.404 INFO 21088 --- [nio-9001-exec-2] c.h.skydance.controller.UserController : httpclient sendAsync请求结束 打印请求结果:["RestTemplate","WebClient","HttpClient"]
参考代码地址