WebClient (史上最全)
技術標籤:java
-
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備 【部落格園總入口 】
-
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高併發核心程式設計》 大廠必備 + 大廠必備 + 大廠必備 【部落格園總入口 】
-
入大廠+漲工資 必備的 高併發社群: 【部落格園總入口 】
推薦: 地表最強 開發環境 系列
工欲善其事 必先利其器 |
---|
地表最強 開發環境: vagrant+java+springcloud+redis+zookeeper映象下載(&製作詳解) |
地表最強 熱部署:java SpringBoot SpringCloud 熱部署 熱載入 熱除錯 |
地表最強 發請求工具(再見吧, PostMan ):IDEA HTTP Client(史上最全) |
地表最強 PPT 小工具: 屌炸天,像寫程式碼一樣寫PPT |
無程式設計不創客,無程式設計不創客,一大波程式設計高手正在瘋狂創客圈交流、學習中! 找組織,GO |
推薦: springCloud 微服務 系列
推薦閱讀 |
---|
nacos 實戰(史上最全) |
sentinel (史上最全+入門教程) |
springcloud + webflux 高併發實戰 |
Webflux(史上最全) |
SpringCloud gateway (史上最全) |
無程式設計不創客,無程式設計不創客,一大波程式設計高手正在瘋狂創客圈交流、學習中! 找組織,GO |
1. 什麼是 WebClient
Spring WebFlux包括WebClient對HTTP請求的響應式,非阻塞式。WebFlux客戶端和伺服器依靠相同的非阻塞編解碼器對請求和響應內容進行編碼和解碼。
WebClient 內部委託給HTTP客戶端庫。預設情況下,WebClient 使用 Reactor Netty,內建了對Jetty 反應式HttpClient的支援,其他的則可以通過插入ClientHttpConnector
。
方式一:通過靜態工廠方法建立響應式WebClient例項
建立最簡單方法WebClient是通過靜態工廠方法之一:
-
WebClient.create()
-
WebClient.create(String baseUrl)
eg:一個使用Webclient(響應式HttpClient) 的Rest請求示例
package com.crazymaker.springcloud.reactive.rpc.mock;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import java.io.IOException;
public class WebClientDemo
{
/**
* 測試用例
*/
@Test
public void testCreate() throws IOException
{
//響應式客戶端
WebClient client = null;
WebClient.RequestBodySpec request = null;
String baseUrl = "http://crazydemo.com:7700/demo-provider/";
client = WebClient.create(baseUrl);
/**
* 是通過 WebClient 元件構建請求
*/
String restUrl = baseUrl + "api/demo/hello/v1";
request = client
// 請求方法
.method(HttpMethod.GET)
// 請求url 和 引數
// .uri(restUrl, params)
.uri(restUrl)
// 媒體的型別
.accept(MediaType.APPLICATION_JSON);
.... 省略其他原始碼
}
}
上面的方法使用 HttpClient 具有預設設定的Reactor Netty ,並且期望 io.projectreactor.netty:reactor-netty在類路徑上。
您還可以使用WebClient.builder()其他選項:
- uriBuilderFactory:自定義UriBuilderFactory用作基本URL(BaseUrl)。
- defaultHeader:每個請求的標題。
- defaultCookie:針對每個請求的Cookie。
- defaultRequest:Consumer自定義每個請求。
- filter:針對每個請求的客戶端過濾器。
- exchangeStrategies:HTTP訊息讀取器/寫入器定製。
- clientConnector:HTTP客戶端庫設定。
方式二:使用builder(構造者)建立響應式WebClient例項
//方式二:使用builder(構造者)建立響應式WebClient例項
client = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
傳送請求
get請求
/**
* 測試用例
*/
@Test
public void testGet() throws IOException
{
String restUrl = baseUrl + "api/demo/hello/v1";
Mono<String> resp = WebClient.create()
.method(HttpMethod.GET)
.uri(restUrl)
.cookie("token", "jwt_token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
// 訂閱結果
resp.subscribe(responseData ->
{
log.info(responseData.toString());
}, e ->
{
log.info("error:" + e.getMessage());
});
//主執行緒等待, 一切都是為了檢視到非同步結果
ThreadUtil.sleepSeconds(1000);
}
方式三:WebClient例項克隆
一旦建立,WebClient例項是不可變的。但是,您可以克隆它並構建修改後的副本,而不會影響原始例項,如以下示例所示:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
抽取公用的baseUrl
如果要訪問的URL都來自同一個應用,只是對應不同的URL地址,這個時候可以把公用的部分抽出來定義為baseUrl,然後在進行WebClient請求的時候只指定相對於baseUrl的URL部分即可。
這樣的好處是你的baseUrl需要變更的時候可以只要修改一處即可。
下面的程式碼在建立WebClient時定義了baseUrl為http://localhost:8081,在發起Get請求時指定了URL為/user/1,而實際上訪問的URL是http://localhost:8081/user/1。
String baseUrl = "http://localhost:8081";
WebClient webClient = WebClient.create(baseUrl);
Mono<User> mono = webClient.get().uri("user/{id}", 1).retrieve().bodyToMono(User.class);
2 請求提交
傳送get請求
/**
* 測試用例: 傳送get請求
*/
@Test
public void testGet() throws IOException
{
String restUrl = baseUrl + "api/demo/hello/v1";
Mono<String> resp = WebClient.create()
.method(HttpMethod.GET)
.uri(restUrl)
.cookie("token", "jwt_token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
// 訂閱結果
resp.subscribe(responseData ->
{
log.info(responseData.toString());
}, e ->
{
log.info("error:" + e.getMessage());
});
//主執行緒等待, 一切都是為了檢視到非同步結果
ThreadUtil.sleepSeconds(1000);
}
提交Json Body
請求體的 mime型別 application/x-www-form-urlencoded
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
例子:
/**
* 測試用例: 傳送post 請求 mime為 application/json
*/
@Test
public void testJSONParam(){
String restUrl = baseUrl + "api/demo/post/demo/v2";
LoginInfoDTO dto=new LoginInfoDTO("lisi","123456");
Mono<LoginInfoDTO> personMono =Mono.just(dto);
Mono<String> resp = WebClient.create().post()
.uri(restUrl)
.contentType(MediaType.APPLICATION_JSON)
// .contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(personMono,LoginInfoDTO.class)
.retrieve().bodyToMono(String.class);
// 訂閱結果
resp.subscribe(responseData ->
{
log.info(responseData.toString());
}, e ->
{
log.info("error:" + e.getMessage());
});
//主執行緒等待, 一切都是為了檢視到非同步結果
ThreadUtil.sleepSeconds(1000);
}
提交表單
請求體的 mime型別 application/x-www-form-urlencoded
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
或者
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
例子:
/**
* 提交表單 mime型別 application/x-www-form-urlencoded
*
* @return RestOut
*/
// @PostMapping("/post/demo/v1")
@RequestMapping(value = "/post/demo/v1", method = RequestMethod.POST)
@ApiOperation(value = "post請求演示")
public RestOut<LoginInfoDTO> postDemo(@RequestParam String username, @RequestParam String password)
{
/**
* 直接返回
*/
LoginInfoDTO dto = new LoginInfoDTO();
dto.setUsername(username);
dto.setPassword(password);
return RestOut.success(dto).setRespMsg("body的內容回顯給客戶端");
}
上傳檔案
請求體的 mime型別"multipart/form-data";
例子:
@Test
public void testUploadFile()
{
String restUrl = baseUrl + "/api/file/upload/v1";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity<ClassPathResource> entity =
new HttpEntity<>(new ClassPathResource("logback-spring.xml"), headers);
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("file", entity);
Mono<String> resp = WebClient.create().post()
.uri(restUrl)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(parts))
.retrieve().bodyToMono(String.class);
log.info("result:{}", resp.block());
}
3錯誤處理
-
可以使用onStatus根據status code進行異常適配
-
可以使用doOnError異常適配
-
可以使用onErrorReturn返回預設值
/**
* 測試用例: 錯誤處理
*/
@Test
public void testFormParam4xx()
{
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<String> mono = responseSpec
.onStatus(e -> e.is4xxClientError(), resp ->
{
log.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 ->
{
log.info("ERROR status:{},msg:{}", err.getRawStatusCode(), err.getResponseBodyAsString());
throw new RuntimeException(err.getMessage());
})
.onErrorReturn("fallback");
String result = mono.block();
System.out.print(result);
}
4 響應解碼
有兩種對響應的處理方法:
-
retrieve
retrieve方法是直接獲取響應body。
-
exchange
但是,如果需要響應的頭資訊、Cookie等,可以使用exchange方法,exchange方法可以訪問整個ClientResponse。
非同步轉同步:
由於響應的得到是非同步的,所以都可以呼叫 block 方法來阻塞當前程式,等待獲得響應的結果。
4.1 retrieve
該retrieve()方法是獲取響應主體並對其進行解碼的最簡單方法。以下示例顯示瞭如何執行此操作:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
預設情況下,4XX或5xx狀態程式碼的應答導致 WebClientResponseException或它的HTTP狀態的具體子類之一,比如 WebClientResponseException.BadRequest,WebClientResponseException.NotFound和其他人。您還可以使用該onStatus方法來自定義所產生的異常
4.2 exchange()
該exchange()方法比該方法提供更多的控制retrieve。以下示例等效於retrieve()但也提供對的訪問ClientResponse:
ono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
請注意(與不同retrieve()),對於exchange(),沒有4xx和5xx響應的自動錯誤訊號。您必須檢查狀態碼並決定如何進行。
與相比retrieve(),當使用時exchange(),應用程式有責任使用任何響應內容,而不管情況如何(成功,錯誤,意外資料等),否則會導致記憶體洩漏.
eg: 下面的例子,使用exchange 獲取ClientResponse,並且進行狀態位的判斷:
/**
* 測試用例: Exchange
*/
@Test
public void testExchange()
{
String baseUrl = "http://localhost:8081";
WebClient webClient = WebClient.create(baseUrl);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", "u123");
map.add("password", "p123");
Mono<ClientResponse> loginMono = webClient.post().uri("login").syncBody(map).exchange();
ClientResponse response = loginMono.block();
if (response.statusCode() == HttpStatus.OK) {
Mono<RestOut> resultMono = response.bodyToMono(RestOut.class);
resultMono.subscribe(result -> {
if (result.isSuccess()) {
ResponseCookie sidCookie = response.cookies().getFirst("sid");
Mono<LoginInfoDTO> dtoMono = webClient.get().uri("users").cookie(sidCookie.getName(), sidCookie.getValue()).retrieve().bodyToMono(LoginInfoDTO.class);
dtoMono.subscribe(System.out::println);
}
});
}
}
response body 轉換響應流
將response body 轉換為物件/集合
-
bodyToMono
如果返回結果是一個Object,WebClient將接收到響應後把JSON字串轉換為對應的物件,並通過Mono流彈出。
-
bodyToFlux
如果響應的結果是一個集合,則不能繼續使用bodyToMono(),應該改用bodyToFlux(),然後依次處理每一個元素,並通過Flux流彈出。
5 請求和響應過濾
WebClient也提供了Filter,對應於org.springframework.web.reactive.function.client.ExchangeFilterFunction介面,其介面方法定義如下。
Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
在進行攔截時可以攔截request,也可以攔截response。
增加基本身份驗證:
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.build();
使用過濾器過濾response:
@Test
void filter() {
Map<String, Object> uriVariables = new HashMap<>();
uriVariables.put("p1", "var1");
uriVariables.put("p2", 1);
WebClient webClient = WebClient.builder().baseUrl("http://www.ifeng.com")
.filter(logResposneStatus())
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.build();
Mono<String> resp1 = webClient
.method(HttpMethod.GET)
.uri("/")
.cookie("token","xxxx")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve().bodyToMono(String.class);
String re= resp1.block();
System.out.print("result:" +re);
}
private ExchangeFilterFunction logResposneStatus() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("Response Status {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
使用過濾器記錄請求日誌:
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.filter(logRequest())
.build();
private ExchangeFilterFunction logRequest() {
return (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);
};
}
參考:
https://www.jb51.net/article/133384.htm
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client
https://www.jianshu.com/p/15d0a2bed6da