webclient 和webflux(參考(webflux)) 這一系列,我們可以開好多節課程來講解
什麼是webclient,在spring5中,出現了reactive 響應式編程思想(參考響應式編程文章),並且爲網絡編程提供相關響應式編程的支持,如提供webflux(參考另外一個教程),他是spring 提供的異步非阻塞的響應式的網絡框架,可以充分利用多cpu並行處理一些功能,雖然不能提高單個請求的響應能力,但是總體可以提高多核的服務器性能,提高系統吞吐量和伸縮性,特別適合於i/o密集型服務,webflux對應的是老式的sevlet的阻塞式服務容器。
而webclient提供的基於響應式的非阻塞的web請求客戶端,對應的是老式的restTemplate這類,相對於老式的restTemplate,他不阻塞代碼、異步執行。
他的執行流程是先創建個webclient.create()實例,之後調用get(),post()等調用方式,uri()指定路徑,retrieve()用來發起請求並獲得響應,bodyToMono(String.class)
用來指定請求結果需要處理爲String,幷包裝爲Reactor的Mono對象,
簡單實例如下:
WebClient webClient = WebClient.create();
Mono<String> mono = webClient.get().uri("https://www.baidu.com").retrieve().bodyToMono(String.class);
mono.subscribe(System.out::println);
以下我們講解具體的實例
以下加webclient的pom
<dependencies>
<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>org.projectreactor</groupId>
<artifactId>reactor-spring</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
一、webclient的使用
1、創建實例
WebClient webClient = WebClient.create();
// 如果是調用特定服務的API,可以在初始化webclient 時使用,baseUrl
WebClient webClient = WebClient.create("https://api.github.com");
或者用構造者模式構造
WebClient webClient1 = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
2、get請求
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、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());
}
可以使用BodyInserters類提供的各種工廠方法來構造BodyInserter對象並將其傳遞給body方法。BodyInserters類包含從Object,Publisher,Resource,FormData,MultipartData等創建BodyInserter的方法。
4、post json
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<String> 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());
}
5、上傳文件:
@Test
public void testUploadFile(){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
HttpEntity<ClassPathResource> entity = new HttpEntity<>(new ClassPathResource("parallel.png"), headers);
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("file", entity);
Mono<String> 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());
6、失敗處理
@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);
}
-
可以使用onStatus根據status code進行異常適配
-
可以使用doOnError異常適配
-
可以使用onErrorReturn返回默認值
7、指定url,並且替換api路徑
在應用中使用WebClient時也許你要訪問的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);
8、retrieve和exchange
retrieve方法是直接獲取響應body,但是,如果需要響應的頭信息、Cookie等,可以使用exchange方法,該方法可以訪問整個ClientResponse。由於響應的得到是異步的,所以都可以調用 block 方法來阻塞當前程序,等待獲得響應的結果。
以下代碼是把獲取寫入的cookie
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> mono = webClient.post().uri("login").syncBody(map).exchange();
ClientResponse response = mono.block();
if (response.statusCode() == HttpStatus.OK) {
Mono<Result> resultMono = response.bodyToMono(Result.class);
resultMono.subscribe(result -> {
if (result.isSuccess()) {
ResponseCookie sidCookie = response.cookies().getFirst("sid");
Flux<User> userFlux = webClient.get().uri("users").cookie(sidCookie.getName(), sidCookie.getValue()).retrieve().bodyToFlux(User.class);
userFlux.subscribe(System.out::println);
}
});
}
9、filter
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);
};
}
注意:
將response body 轉換爲對象/集合
-
bodyToMono
如果返回結果是一個Object,WebClient將接收到響應後把JSON字符串轉換爲對應的對象。
-
bodyToFlux
如果響應的結果是一個集合,則不能繼續使用bodyToMono(),應該改用bodyToFlux(),然後依次處理每一個元素。
二、webclient原理
webclient的整個實現技術基本採用reactor-netty實現,下章我講述相關細節