Spring WebFlux学习笔记:4 异步HTTP客户端WebClient

7,121 阅读4分钟
是一个异步的非阻塞的客户端, client和server都依赖同一套codec进行编码和解码.

4.1 配置

可以通过以下静态方法创建:

WebClient.create(); 
WebClient.create(String baseUrl);

可以通过WebClient.builder来创建, 可以配置这些属性:
uriBuilderFactory: 通过这个来配置BaseUrl;
  • dafaultHeader;
  • defaultCookie
  • defaultRequest;
  • filter;
  • exchangeStategies: http消息reader/ writer自定义
  • clientConnector: HTTP client library settings


通过exchangeStrategis可以配置http codec
一旦webclient被创建, 它就是不可变的, 不过可以通过创建另外一个实例来重新指定属性:

WebClient client1 = WebClient.builder()
            .filter(filterA).filter(filterB).build();

    WebClient client2 = client1.mutate()
            .filter(filterC).filter(filterD).build();



Reactor Netty
如果要自定义这个的属性, 比如说配置ssl等, 只需要提供一个配置好的httpClient给webClient实例:

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();



1 配置Resource

这里的Resource比如说有event loop线程, 连接池等. 默认的配置是在应用里全局共享.


配置为非全局共享的例子:

@Bean
    public ReactorResourceFactory resourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setGlobalResources(false); 
        return factory;
    }

    @Bean
    public WebClient webClient() {

        Function<HttpClient, HttpClient> mapper = client -> {
            // Further customizations...
        };

        ClientHttpConnector connector =
                new ReactorClientHttpConnector(resourceFactory(), mapper); 

        return WebClient.builder().clientConnector(connector).build(); 
    }



2 配置Timeouts

方式1:

 HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

方式2, 分别配置读timeout和写timeout:

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10))));



已jetty作为httpclient的底层时配置jetty:

HttpClient httpClient = new HttpClient();
    httpClient.setCookieStore(...);
    ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

    WebClient webClient = WebClient.builder().clientConnector(connector).build();





4.2 retrieve方法

用来获取请求的响应:

WebClient client = WebClient.create("https://example.org");
    Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Person.class);

获取响应流:

Flux<Quote> result = client.get()
            .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
            .retrieve()
            .bodyToFlux(Quote.class);


默认4xx或者5xx状态将导致一个异常, 对于其他状态码, 可以用onStatus方法来指定它们的处理:

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .onStatus(HttpStatus::is4xxServerError, response -> ...)
            .onStatus(HttpStatus::is5xxServerError, response -> ...)
            .bodyToMono(Person.class);

当onStatus指定的处理器被触发时, 即使处理器不处理这个响应, 这个响应也会自动被消费以保证资源的释放.





4.3 exchange()方法

相对于retrieve方法, 可以进行更加多的控制

Mono<Person> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.bodyToMono(Person.class));


Mono<ResponseEntity<Person>> result = client.get()
            .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
            .exchange()
            .flatMap(response -> response.toEntity(Person.class));



exchange方法, 不会自动处理4xx或者5xx状态码, 需要应用自己来决定怎么处理这些异常;


当使用exchange方法时, 必须调用CLientResponse的bodyxx或者toEntity方法来保证连接等资源可以被释放. 假如不期望有响应, 可以使用bodyToMono(Void.class).






4.4 请求体的构造

以json做为请求体, 发送一个实体的例子:

Mono<Person> personMono = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .body(personMono, Person.class)
            .retrieve()
            .bodyToMono(Void.class);


发送一个对象流的例子:

Flux<Person> personFlux = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_STREAM_JSON)
            .body(personFlux, Person.class)
            .retrieve()
            .bodyToMono(Void.class);


假如持有一个实际值(而不是Mono或者FLux这种当前可能不存在, 在未来某个时间可以通过这个东西来获取的这种抽象), 可以这样发送:

Person person = ... ;

    Mono<Void> result = client.post()
            .uri("/persons/{id}", id)
            .contentType(MediaType.APPLICATION_JSON)
            .syncBody(person)
            .retrieve()
            .bodyToMono(Void.class);


form data的发送:
使用MultiValueMap<String, String> 作为body, 这种body会被FormHttpMessgeWriter自动转才能够appliction/ x-www-form-urlencoded格式.

MultiValueMap<String, String> formData = ... ;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(formData)
            .retrieve()
            .bodyToMono(Void.class);


可以用流畅风格来配置formbody

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);





Multipart Data
通过 MultipartBodyBuilder来创建请求体:

MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("fieldPart", "fieldValue");
    builder.part("filePart", new FileSystemResource("...logo.png"));
    builder.part("jsonPart", new Person("Jason"));

    MultiValueMap<String, HttpEntity<?>> parts = builder.build();


不需要指定Multipart中每一个部分的格式, 一般会自动检测, 比如说文件会根据扩张名来检测.
当然也可以通过传递一个MediaType显式指定.

通过sysnBody方法来发送一个MultipartBody:

MultipartBodyBuilder builder = ...;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(builder.build())
            .retrieve()
            .bodyToMono(Void.class);


流畅风格:

MultipartBodyBuilder builder = ...;

    Mono<Void> result = client.post()
            .uri("/path", id)
            .syncBody(builder.build())
            .retrieve()
            .bodyToMono(Void.class);




4.5 clientFilter

示例如下:

WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();

一般用作一些横切逻辑, base authentication的例子:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();



filter将作用在所有请求上, 假如不想这样, 官方推荐的方法是set这请求的属性, 根据这个属性, filter执行不同的策略:

WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("https://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }



重新配置filter:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();


web同步的使用方法
如下

Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();


如果有多个调用, 这种方式是低效的. 多个调用的推荐方式:

Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", personName);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();




未完待续...

后续会分享sample项目。