网站首页 > 技术文章 正文
多年来,Spring框架的RestTemplate一直是客户端HTTP访问的首选解决方案,它提供了同步、阻塞API以简洁的方式处理HTTP请求。然而,随着对非阻塞、反应式编程以更少的资源处理并发的需求不断增加,特别是在微服务架构中,RestTemplate已经显露出其局限性。从Spring Framework 5开始,RestTemplate已被标记为已弃用,Spring团队推荐WebClient作为其继任者。在这篇文章中,我们将通过实际示例深入探讨RestTemplate被弃用的原因、采用WebClient的优势以及如何有效过渡。
概述
什么是WebClient
WebClient是从Spring WebFlux 5.0版本开始提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具。它的响应式编程的基于Reactor的。WebClient中提供了标准Http请求方式对应的get、post、put、delete等方法,可以用来发起相应的请求。
与RestTemplate的区别
与 RestTemplate 相比,它具有许多优势:
- 非阻塞操作: WebClient 使用 Project Reactor 在非阻塞、反应式范例上运行,使其能够以更少的线程和更少的开销处理并发,从而显著提高可扩展性和资源利用率。
- 反应式堆栈: WebClient 支持反应式堆栈,使其适合基于事件循环的运行时环境。它可以在微服务架构中典型的高并发场景下高效工作。
- JSON 处理及更多: WebClient 通过 Jackson 库提供与 JSON 的无缝集成,类似于 RestTemplate,但具有增强的处理能力。它还支持服务器发送事件 (SSE)、流场景和其他高级用例。
当我们通过WebClient调用返回 Mono或 Flux 的API 时,API 会立即返回。而调用结果将通过 mono 或 flux 回调传递给调用端。
请注意,如果需要,我们可以通过WebClient.block()方法实现类似RestTemplate的同步处理。
@Service
public class MyService {
private final WebClient webClient;
@Autowired
public MyService(WebClient webClient) {
this.webClient = webClient;
}
public Mono<String> getData() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class)
.subscribe(
// onSuccess callback
result -> {
System.out.println("Success: " + result);
},
// onError callback
error -> {
System.err.println("Error: " + error.getMessage());
}
);
}
}
为什么弃用RestTemplate
RestTemplate本质上是阻塞的,并使用 Java Servlet API 的每个请求一个线程模型。这意味着RestTemplate一旦向远程服务器发送请求,就会等待响应。默认情况下,每次RestTemplate都会创建新的,并在收到并处理响应后关闭连接。Httpconnection 创建和关闭 URL 连接是一项成本高昂的操作。为了在生产类应用程序中有效地使用RestTemplate ,我们必须使用HTTP 连接池,否则性能会快速下降。当应用程序中有大量请求时,线程和连接的数量也会按比例增加。这会给服务器资源带来负担。如果服务器速度缓慢,用户很快就会发现应用程序性能下降,甚至无响应。
- 阻塞性质: RestTemplate 是一个阻塞、同步客户端。这意味着执行请求的线程会阻塞,直到操作完成,这可能会导致线程池耗尽,并在重负载下导致更高的延迟。此模型不能很好地扩展,特别是在应用程序必须有效处理数千个并发请求的微服务环境中。
- 可扩展性有限: RestTemplate 的同步特性限制了可扩展性。需要高吞吐量、低延迟能力的现代系统发现这种方法不够。事件驱动、反应式编程范式的兴起是对这些需求的回应,导致了 WebClient 等非阻塞 API 的采用。
- 缺乏反应式编程支持: RestTemplate 不支持反应式编程,而反应式编程在基于云的生态系统中日益增长。响应式编程使系统更具响应性、弹性和弹性,但这是 RestTemplate 的阻塞性质无法实现的。
请注意,RestTemplate 是线程安全的,并且可以随时在多个连接之间共享单个实例。
@Service
public class MyService {
private final RestTemplate restTemplate;
@Autowired
public MyService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getData() {
ResponseEntity<String> responseEntity
= restTemplate.getForEntity("https://api.example.com/data", String.class);
String responseBody = responseEntity.getBody();
return responseBody;
}
}
使用指南
一、block()阻塞式获取响应结果
WebClient客户端既支持同步异步、阻塞与非阻塞IO,我们先来为大家介绍一下同步阻塞式的编程方式。即:在请求发送之后使用block()方法,阻塞当前线程等待获取响应结果。
1.1.使用Mono接收单个对象
创建测试用例,成员变量WebClient,以 "http://jsonplaceholder.typicode.com" 为访问服务基础路径,该网站是一个免费提供RESTful API进行接口测试的一个网站。
public class GetTest {
//创建webClient
private WebClient webClient = WebClient.builder()
.baseUrl("http://jsonplaceholder.typicode.com")
.build();
@Test
public void testMono() {
Mono<PostDTO> mono = webClient
.get() // 发送GET 请求
.uri("/posts/1") //服务请求路径,基于baseurl
.retrieve() // 获取响应体
.bodyToMono(PostDTO.class); //响应数据类型转换
System.out.println(mono.block());
}
}
- get() 方法表示使用HTTP GET method
- uri() 指定服务接口路径,以baseurl为基础
- retrieve() 获取响应体,即HTTP body
- bodyToMono()将响应体转换为一个对象,Mono英文是单声道、单体的意思,用于接收单个对象
通过浏览器访问 "http://jsonplaceholder.typicode.com/posts/1" 得到JSON响应结果,和我们通过程序打印出的响应结果数据内容一致。
接收响应结果的java POJO实体对象如下:
import lombok.Data;
@Data
public class PostDTO {
private int userId;
private int id;
private String title;
private String body;
}
1.2.使用Flux接收集合对象
访问http://jsonplaceholder.typicode.com/posts 可以获得JSON数组方式的请求结果如图(一共100条我截图截取3条记录):
所以我们需要通过bodyToFlux方法将请求结果转为Flux<PostDTO>,通过flux.collectList().block();接收响应数据为List<PostDTO>集合。Flux英文含义:流动的,用于接收集合元素响应结果。
@Test
public void testFlux() {
Flux<PostDTO> flux = webClient
.get() // 发送GET 请求
.uri("/posts") //服务请求路径,基于baseurl
.retrieve() // 获取响应体
.bodyToFlux(PostDTO.class); //响应数据类型转换
List<PostDTO> posts = flux.collectList().block();
assert posts != null;
System.out.println("获取posts集合元素数量:" + posts.size());
}
控制台打印结果如下:
二、subscribe()非阻塞式获取响应结果
与block()阻塞式获取响应结果不同,使用subscribe()异步订阅响应结果,不会阻塞主线程继续向下执行。获取到响应结果之后,由回调函数handleResponse处理响应结果。
@Test
public void testSubscribe() throws InterruptedException {
Mono<PostDTO> mono = webClient
.get() // 发送GET 请求
.uri("/posts/1") //服务请求路径,基于baseurl
.retrieve() // 获取响应体
.bodyToMono(PostDTO.class); //响应数据类型转换
//异步非阻塞处理响应结果
mono.subscribe(GetTest::handleResponse);
//为了避免测试用例主线程执行完成,导致看不到异步处理结果
Thread.currentThread().sleep(10000);
}
//响应结果处理回调方法
private static void handleResponse(PostDTO postDTO) {
System.out.println("handle response:=======================");
System.out.println(postDTO);
}
三、exchange()获取HTTP响应完整内容
上文中retrieve()只能获取HTTP报文中的Body,也就是响应体。如果我们想获取HTTP报文中的状态码、headers、cookies等信息,需要使用exchange()方法。
@Test
public void testExchange() {
Mono<ClientResponse> mono = webClient
.get() // 发送GET 请求
.uri("/posts/1") //服务请求路径,基于baseurl
.exchange();
System.out.println(mono.block());
// 获取完整的响应对象
ClientResponse response = mono.block();
assert response != null;
HttpStatus httpStatus = response.statusCode(); // 获取响应状态
int statusCodeValue = response.rawStatusCode(); // 获取响应状态码
ClientResponse.Headers headers = response.headers(); // 获取响应头
// 获取响应体
Mono<PostDTO> resultMono = response.bodyToMono(PostDTO.class);
PostDTO postDTO = resultMono.block();
// 输出结果
System.out.println("响应状态:" + httpStatus);
System.out.println("响应状态码值:" + statusCodeValue);
System.out.println("HTTP Headers:" + headers.asHttpHeaders());
System.out.println("响应体:" + postDTO);
}
HTTP报文信息详情控制台打印结果
四、占位符传参
非占位符传参,写死的参数方式不灵活
.uri("/posts/1") //服务请求路径,基于baseurl
第一种占位符传参:数字顺序占位符
Mono<String> mono = webClient.uri("/{1}/{2}", "posts", "1")
第二种占位符传参:参数名称占位符
String type = "posts";
int id = 1;
Mono<String> mono = webClient.uri("/{type}/{id}", type, id)
第三种通过map传参
Map<String,Object> map = new HashMap<>();
map.put("type", "posts");
map.put("id", 1);
Mono<String> mono = webClient
.uri("/{type}/{id}", map)
如何从 RestTemplate 过渡到 WebClient
执行 GET 请求:
RestTemplate
RestTemplaterestTemplate = new RestTemplate ( );
ResponseEntity<String> response = restTemplate.getForEntity( "https://www.baidu.com" , String.class);
WebClient
WebClient webClient = WebClient.create();
Mono<String> response = webClient.get()
.uri("https://www.baidu.com")
.retrieve()
.bodyToMono(String.class);
response.subscribe(result -> System.out.println(result));
使用 JSON 的 POST 请求:
当发出 POST 请求并提交 JSON 时,WebClient 以其流畅的 API 提供了更直接的方法。
RestTemplate
RestTemplaterestTemplate = new RestTemplate ( ) ;
HttpHeaders headers = new HttpHeaders ();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity <>( "{"key":"value"}" , headers);
ResponseEntity<String> response = restTemplate.postForEntity( "https://www.baidu.com" , request, String.class);
WebClient
WebClient webClient = WebClient.create();
Mono<String> response = webClient.post()
.uri("https://www.baidu.com")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue("{"key":"value"}")
.retrieve()
.bodyToMono(String.class);
使用 WebClient,设置标头和正文内容更加直观,并且需要更少的样板代码。和contentType()方法bodyValue()允许直接设置内容类型和正文。
异步处理:
使用 WebClient 最显着的优点之一是它支持异步处理。当您的应用程序需要进行多个独立的 API 调用时,这特别有用;这些可以同时执行,从而大大减少这些操作所需的总时间。
WebClient webClient = WebClient.create();
Mono<String> responseOne = webClient.get()
.uri( "http://demo.com/endpointOne" )
.retrieve()
.bodyToMono(String.class);
Mono<String> responseTwo = webClient.get()
.uri( "http://demo.com/endpointTwo" )
.retrieve()
.bodyToMono(String.class);
// 使用 Mono.zip 并发执行请求
Mono.zip(responseOne, responseTwo).subscribe(results -> {
System.out.println( "结果1: " + results.getT1());
System.out.println( "结果2:" + results.getT2());
});
在此示例中,Mono.zip用于组合多个请求的结果。这些请求是并发执行的,一旦所有请求完成,就会处理结果。这种方法比 RestTemplate 同步操作中固有的顺序执行效率要高得多。
流数据:
WebClient 还支持以数据流的形式检索响应主体,这在处理您不想一次将其全部保存在内存中的大量数据时特别有用。
WebClient webClient = WebClient.create();
webClient.get()
.uri( "http://demo.com/stream" )
.accept(MediaType.TEXT_EVENT_STREAM) // 用于服务器发送事件 (SSE)
.retrieve()
.bodyToFlux(String.class) //将响应正文转换为 Flux
.subscribe(data -> System.out.println( "Received: " + data));
在此场景中,bodyToFlux用于将响应正文转换为Flux,表示数据流。然后,该subscribe方法用于在每条数据到达时对其进行处理。这与 RestTemplate 形成鲜明对比,后者要求在处理之前将整个响应主体加载到内存中,无论大小如何。
重试机制:
WebClient 利用反应式编程模型提供了一种更复杂的方法来重试失败的请求。
WebClient webClient = WebClient.builder().baseUrl( "http://demo.com" ).build();
Mono<String> response = webClient.get()
.uri( "/retry-endpoint" )
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff( 3 , Duration.ofSeconds( 1 )) //重试次数和退避配置
.maxBackoff(Duration.ofSeconds( 10 ))) // 最大退避时间
.onErrorResume(e -> Mono.just( "Fallback response" )); // 如果重试都失败则回退
response.subscribe(result -> System.out.println(result));
复制代码
如果重试都失败则回退 response.subscribe(result -> System.out.println(result));
在此示例中,该retryWhen方法用于定义重试策略,指定重试次数和退避配置。如果所有重试都失败,onErrorResume则提供后备机制。
自定义 Web 客户端配置:
WebClient 具有高度可配置性,您可能会发现默认设置无法满足您的需求。例如,您可能想要调整连接超时,或者可能需要添加应随每个请求发送的默认标头。
// 构建一个具有指定超时和默认标头的自定义 WebClient
WebClient customWebClient = WebClient.builder()
.baseUrl( "http://demo.com" )
.clientConnector( new ReactorClientHttpConnector (
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS ) , 2000 ) // 2 秒超时
.responseTimeout(Duration.ofSeconds( 2 )) // 设置响应超时
.doOnConnected(conn ->
conn.addHandlerLast( new ReadTimeoutHandler ( 2 )) // 2 秒读取超时
.addHandlerLast( new WriteTimeoutHandler ( 2 ))))) // 2 秒写入超时
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 默认标头
.defaultHeader( "Another-Header" , "Another-Value" ) // 另一个默认标头
.build ();
在此示例中,我们自定义 WebClient 以具有特定的超时配置和默认标头。WebClient 的此实例会将这些设置应用于它执行的所有请求,确保整个应用程序的行为一致。
网络客户端过滤器:
WebClient 支持使用过滤器来处理横切关注点。这些过滤器可用于操纵请求或响应,甚至可以处理日志记录、指标或授权等问题。
// 带过滤器的自定义 WebClient
WebClient FilteredWebClient = WebClient.builder()
.baseUrl( "http://demo.com" )
.filter((request, next) -> {
// 记录请求数据
System.out.println( "Request: " + request.method() + " " + request.url());
return next.exchange(request).doOnSuccessOrError((response, error) -> {
if (response != null ) {
// 日志响应数据
System.out.println( "响应状态: " + response.statusCode());
}
if (error != null ) {
// 记录错误
System.out.println( "错误: " + error.getMessage() );
}
});
})
.build();
个请求的 HTTP 方法和 URL,以及收到的每个响应的状态代码。它还记录交换期间可能发生的任何错误。
相互 TLS 身份验证:
在需要增强安全性的场景中(例如内部微服务通信),您可能需要相互 TLS (mTLS) 身份验证。可以针对此类场景配置 WebClient。
// 使用信任存储和密钥存储准备 SSL 上下文
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE) // 仅用于演示目的!
.keyManager( new File ( "path/to/client.crt" ), new File ( "path/to/client.key" )) // 客户端证书和私钥
.build();
// 使用 SSL 上下文配置 WebClient WebClient
secureWebClient = WebClient.builder ()
.clientConnector( new ReactorClientHttpConnector (HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext))))
.build();
在此示例中,我们通过使用包含客户端证书和私钥的 SSL 上下文配置 WebClient 来设置 mTLS。此设置可确保客户端和服务器在 SSL 握手期间相互验证。
网络客户端最佳应用
以下是一些确保您高效且有效地使用 WebClient 的指南:
单例模式:
与通常根据请求或服务实例化的 RestTemplate 不同,WebClient 被设计为用作单例。这意味着您应该创建 WebClient 的单个实例并在您的应用程序中重用它。这种方式保证了资源的高效利用,避免了重复创建和销毁WebClient实例的开销。
@Bean
public WebClient.Builder webClientBuilder () {
return WebClient.builder();
}
通过在配置中定义 WebClient.Builder bean,您可以在任何需要的地方自动装配它,并针对特定用例对其进行自定义,而无需每次都创建新的 WebClient 实例。
错误处理:
反应性流向下游传播错误,直到错误被处理或到达流的末尾。始终处理反应链中的错误,以避免意外行为。onErrorResume、onErrorReturn和运算doOnError符特别有用
webClient.get()
.uri( "/endpoint" )
.retrieve()
.bodyToMono(String.class)
.doOnError(e -> log.error( "发生错误" , e))
.onErrorResume(e -> Mono. just( "异常值" ));
超时配置:
始终配置超时。如果没有超时,如果服务器没有响应,WebClient 请求可能会无限期挂起。使用timeout运算符设置特定的持续时间,在此之后请求将被终止。
webClient.get()
.uri( "/endpoint" )
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds( 10 ));
限流:
反应式编程的核心原则之一是限流,它允许消费者向生产者发出他们可以处理多少数据的信号。处理Flux(0 到 N 个项目的流)时,请注意限流并确保让消费者无感知。使用运算符来limitRate控制数据流的速率。
日志记录:
日志记录对于调试和监控至关重要。WebClient 提供内置日志记录功能。通过将记录器设置reactor.netty.http.client.HttpClient为DEBUG,您可以查看请求和响应的详细日志。
线程上下文:
在反应式编程中,操作可能会多次切换线程。如果您依赖线程局部变量(如日志记录或安全上下文中使用的变量),请注意这些变量可能不会自动跨线程传播。像这样的库reactor-context可以帮助在反应流中跨线程传播上下文。
避免阻塞呼叫:
WebClient 和反应式编程的主要好处之一是操作的非阻塞性质。但是,如果您在反应式链中引入阻塞调用,那么这种好处就会被抵消。始终避免在反应流中阻塞操作。如果必须使用阻塞调用,请考虑使用将其卸载到单独的线程池subscribeOn。
Mono.fromCallable(() ->blockingMethod())
.subscribeOn(Schedulers.boundedElastic());
WebClient 提供了一种现代的、非阻塞的、反应式的方法来发出 HTTP 请求,使其成为大多数用例中优于已弃用的 RestTemplate 的选择。然而,随着它的力量而来的是正确使用它的责任。通过遵循最佳实践、了解响应式范例并意识到潜在的陷阱,您可以充分利用 WebClient 的潜力并构建高效、可扩展且响应迅速的应用程序。
新的技术还是要多多练习,熟练使用后再在日常工作中使用起来。
参考资料
- 链接:https://juejin.cn/post/7294150742113304602
- 链接:https://zhuanlan.zhihu.com/p/192194201
- 链接:https://zhuanlan.zhihu.com/p/659885945
- 链接:https://www.dandelioncloud.cn/article/details/1609177150911627266
猜你喜欢
- 2024-09-08 精讲RestTemplate第7篇-自定义请求失败异常处理
- 2024-09-08 java实现调用http请求的几种常见方式
- 2024-09-08 深度原理学习——白话TCP与HTTP的keep–alive机制
- 2024-09-08 有了WebClient还在用RestTemplate?
- 2024-09-08 Spring Boot外部接口调用:使用RestTemplate与WebClient操控HTTP
- 2024-09-08 Java服务优雅上下线(java项目如何上线)
- 2024-09-08 Spring 框架里的 HTTP 调用,RestTemplate 还是 WebClient
- 2024-09-08 微服务中如何使用RestTemplate优雅调用API(详细分析)
- 2024-09-08 真不是吹,Spring 里这款牛逼的网络工具库你可能没用过
- 2024-09-08 Java工具类封装微服务间HTTP通信(java md5工具类)
- 02-21走进git时代, 你该怎么玩?_gits
- 02-21GitHub是什么?它可不仅仅是云中的Git版本控制器
- 02-21Git常用操作总结_git基本用法
- 02-21为什么互联网巨头使用Git而放弃SVN?(含核心命令与原理)
- 02-21Git 高级用法,喜欢就拿去用_git基本用法
- 02-21Git常用命令和Git团队使用规范指南
- 02-21总结几个常用的Git命令的使用方法
- 02-21Git工作原理和常用指令_git原理详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)