webclient使用介紹

      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);
    }
  1. 可以使用onStatus根據status code進行異常適配

  2. 可以使用doOnError異常適配

  3. 可以使用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實現,下章我講述相關細節

參考:https://www.jb51.net/article/133384.htm

               https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章