# WebFlux 之 WebClient 的使用记录

WebFlux 是一款基于 Netty 和 Reactive 技术栈构开发的一款 Spring 框架。该框架相比原来 Web 框架能有效提高 TPS 。其原理是基于 Netty 实现的事件驱动达到对线程的高可用,也实现了非阻塞。通过 Reactor 设计模式,使原有请求 (线程) 不需要通过等待过程实现 (如:方法内的处理过程或远程调用或者数据库调用) 导致系统资源的损耗。而 WebFlux 则是请求调用后则高高挂起,等待程序通知返回响应结果,然后它再离开。这个过程就是一种观察者模式。观察是否有处理结果了再做反应。而这也是 WebFlux 的响应式编程。

# WebClient 是什么

WebClient 是非阻塞式 Reactive HTTP 客户端,是 Spring 5 中引入了。而以前用的 RestTemplate 是阻塞式 HTTP 客户端。

# WebClient 使用

普通的接口

@GetMapping(value = "/test")
public String monoDemo(String name) {
    try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    return name;
}

WebClient 调用

@GetMapping(value = "/test2")
public Mono<String> monoDemo2() {
    System.out.println("start...");
	Mono<String> stringMono = WebClient.create("http://127.0.0.1:8080/test?name=昔日长廊")
        .get() // 请求方式 也可 .method (HttpMethod.GET)
        .retrieve() // 对返回结果的简易处理,对应结果集 .exchange () 是高级处理,对应响应体
        .bodyToMono(String.class) // 对数据进行转为 Mono (T)
        .flatMap(f -> { // 对第一个数据进行处理并返回一个 Mono (T) 格式对象。也有可能是 Flux 要看上游
            System.out.println(f);
            return Mono.just(f); // 将数据转为 Mono (T)
        });
    System.out.println("end...");
    return stringMono;
}

调用 test2。打印结果

start...
end...
昔日长廊
// 返回结果是 5 秒后的 “昔日长廊” 字符串

# WebClient 发送带有自定义字段的请求头

@GetMapping(value = "/mono4")
public Mono<String> monoDemo3() {
    WebClient webClient = WebClient.create();
    Map<String, String> stringStringMap = new HashMap<>();
    stringStringMap.put("name","昔日织");
    stringStringMap.put("password","昔日长廊");
    return webClient.method(HttpMethod.GET)
            .uri("http://127.0.0.1:8090/list?current=1&size=1000&buildState=true")
            .headers(p->{
                // 重点:通过设置 Access-Control-Expose-Headers 防止自定义请求头丢失。可设 token
                p.add("Access-Control-Expose-Headers","*");
                // 携带自定义请求头凭据
				p.add("token","eyJleHBpcmF0aW9uIjoxNj");
            })
            .body(BodyInserters.fromValue(stringStringMap))
            .retrieve()
            .bodyToMono(String.class);
}

# 使用 WebClient 做网关转发

WebFlux 框架的特性非常适用于网关。像 Gateway 也是基于 WebFlux 基础实现的一个微服务网关。

实现:将 /almp/api/user 接口转发到 /api/user 接口下。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.RouterFunction;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.MediaType;
import reactor.core.publisher.Mono;
import java.util.*;
@Configuration
public class RouterConfig {
    @Bean
    public RouterFunction<ServerResponse> dynamicRouter() {
        return route(path("/almp/**"),res->this.send(res,"almp"));
    }
    /**
     * 转发器
     * @param serverRequest
     * @return
     */
    public Mono<ServerResponse> send(ServerRequest serverRequest,String appName) {
        //url 去除前面的 almp 
        String url = serverRequest.path().replace("/"+appName, "");
        // 拼接一个带 GET 请求参数的路径
        String path = String.format("http://127.0.0.1:8080%s?%s",url,serverRequest.uri().getQuery());
        WebClient webClient = WebClient.create();
        Mono<String> stringMono = serverRequest.bodyToMono(Map.class) // 将 json 传参转义为 Map
            //// 如果参数为空则设置一个空的 Map。不然无法进入 flatMap
            .switchIfEmpty(Mono.just(new HashMap<>()))
            .flatMap(p -> {
         		return webClient.method(serverRequest.method()) // 设置请求的类型
                				.uri(path)// 设置接口
                				.headers(g -> {
                                    // 设置请求头防止过滤自定义字段
                    g.setAll(serverRequest.headers().asHttpHeaders().toSingleValueMap());
                    g.set("Access-Control-Expose-Headers","*");
                			})
                         		.body(BodyInserters.fromValue(p)) // 传递 json 格式参数
                         		.retrieve() // 获取简易的返回结果
                         		.bodyToMono(String.class);// 将返回结果转为字符串
        });
        //contentType 返回内容是 application/json。可理解为被 @ResponseBody 注解的接口返回的格式
        //body 这个 Mono (T) T 的类型是什么
        return ok().contentType(MediaType.APPLICATION_JSON).body(stringMono, String.class);
    }
}

经测试可以转发 GET 请求,PROD 请求 JSON 传值方式的接口。不支持 form 表达传值的方式。未测试应该可实现对其它请求传值方式的接口转发。

# 遇到的问题

# 从 ServerRequest 取出参数问题

ServerRequest 类在使用过程中要和最后的返回 body 里面要有关联,不然会导致 ServerRequest bodyToMono 产生的 Mono 或者 Flux 不执行。如果使用 block 等结束操作会直接导致报错,报错的原因应该是连接断掉了。也不能通过 subscribe 订阅的方式获取参数。只有让 ServerRequest 产生的 Mono 或者 Flux 在返回结果的 ServerResponse 里,整体构成一个回路。才会去执行 ServerRequestbodyToMono 后面的数据操作流程。所以如果我将上面情况改成如下,则 从 serverRequest.bodyToMono 开始不会运行。

return ok().contentType(MediaType.APPLICATION_JSON).body(stringMono, String.class);
return ok().contentType(MediaType.APPLICATION_JSON).body(Mono.just("自定义字符串"), String.class);

其问题的原因是 MonoFlux 的执行原理。而 ServerRequest 正好又是一个特殊的东西。所以才会有这种情况。

# 设置 headers 问题

// 最开始的代码
.headers(g -> {
    g = serverRequest.headers().asHttpHeaders();
    g.set("Access-Control-Expose-Headers","*");
})
// 现在
.headers(g -> {
    g.setAll(serverRequest.headers().asHttpHeaders().toSingleValueMap());
    g.set("Access-Control-Expose-Headers","*");
})

最开始直接将 ServerRequest 的请求头对象替换 WebClient 发起的请求头,这个步骤没有报错,但之后对 WebClient 的请求头设置 Access-Control-Expose-Headers=* 的时候会报错。

原因是 ServerRequest 请求体内的 HttpHeaders 对象是 基于 HttpHeaders 接口实现的 ReadOnlyHttpHeaders 实例对象。这个实例的对象不能对请求头进行编辑修改。所以之后请求头设置 Access-Control-Expose-Headers=* 的时候会报错。

# 总结

第一次知道 WebFlux 并对此进行了解和学习。感觉 WebFlux 的响应式编程有点烧脑。

本文如果有什么问题还望各位多多包涵,请指出我的错误。互相学习。