(farerboy)头像
响应式 HTTP 客户端实践:Spring Boot 整合 WebClient 实现连接池、重试与超时控制封面图

响应式 HTTP 客户端实践:Spring Boot 整合 WebClient 实现连接池、重试与超时控制

1. 我为什么把内部调用从 RestTemplate 切成 WebClient

最早我所在的团队全是 Spring MVC,服务之间调用一直用 RestTemplate,也没觉得多大问题。后来用户量上来,再叠加促销活动,单库单表的订单服务开始频繁报警,Tomcat 线程被下游调用占光。仔细排查下来,每天常规高峰期光等待商品服务和库存服务返回就把 200 个线程全部打满,新请求排不上队,页面超时,一台一台地加实例也只是缓解,加了半天 CPU 其实才用了 20% 不到。核心问题就是阻塞模型:一个请求一个线程,等 I/O 期间线程什么也干不了。

后来切了 WebClient,真是质的区别。同样是 200 个线程的 Tomcat,但只要底层用 Netty EventLoop 去发起下游调用,真正等待响应时没有线程在那里傻等,I/O 事件由极少数线程分发,整个系统的并发承载能力高了一个数量级。特别是云原生环境下面,Pod 内存有限、实例弹性伸缩,这种非阻塞模型更划算。

再说一点,响应式客户端天然能把多个调用拼起来。比如先查订单,拿到商品 ID 后再并行调商品服务和库存服务,都不需要手动写一堆 Future,直接用 Reactor 组合。消费速度受背压控制,不怕上游响应太大把内存撑爆。

也不是说哪都该用响应式。要是你的业务全是 CPU 计算,或者只能走 JDBC 这种阻塞库,那传统做法依然是合理的。WebClient 适合高并发访问外部 HTTP API 的场景,尤其在下游延迟不稳定的时候,它的优势最明显。

2. Spring Boot 3 的依赖到底怎么加

WebClient 本身是 spring-webflux 提供的,但真正干活的是 Reactor Netty。如果你的工程没有引入过任何 WebFlux 相关东西,通常加一个 spring-boot-starter-webflux 就行了:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

很多老项目本身是 spring-boot-starter-web,担心再加 webflux 会冲突。实际上不会,Spring Boot 检测到 DispatcherServlet 存在时,默认仍然启动成 Servlet 容器,WebFlux 在这个工程里只是被 WebClient 使用,不会把 Tomcat 换成 Netty Server。我已经在好几个 Spring MVC 项目里这么干过,没遇到问题。

既然说到 RestClient,Spring Boot 3.2 确实推出了基于 JDK 的同步客户端 RestClient,API 风格和 WebClient 很像,但它是阻塞的,目的只是为了让你从 RestTemplate 迁移更舒服。如果追求高并发调用链,WebClient 还是优先选择。

3. 基础封装:GET、POST、Header、Cookie、文件上传和流式响应都用得上

日常业务代码不要直接 WebClient.create() 怼上去,那样会到处是重复样板。我在项目里习惯先做一个薄薄的 HttpService,把所有通用逻辑收敛起来。

先定义一个基础的 WebClient Bean:

@Configuration
public class WebClientConfig {

    @Bean
    WebClient webClient(WebClient.Builder builder) {
        return builder
            .baseUrl("https://default-backend.example.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}

如果你的 WebClient 需要自定义连接池,就不要用自动配置的 WebClient.Builder 了,下面的章节会说如何手动 WebClient.builder() 构建。

封装 GET 和 POST:

@Component
public class HttpService {

    private final WebClient webClient;

    public HttpService(WebClient webClient) {
        this.webClient = webClient;
    }

    public <T> Mono<T> get(String url, Map<String, String> headers,
                           Map<String, String> cookies, Class<T> responseType) {
        return webClient.get()
                .uri(url)
                .headers(h -> headers.forEach(h::set))
                .cookies(c -> cookies.forEach(c::add))
                .retrieve()
                .bodyToMono(responseType);
    }

    public <T> Mono<T> post(String url, Object body, Map<String, String> headers,
                            Class<T> responseType) {
        return webClient.post()
                .uri(url)
                .headers(h -> headers.forEach(h::set))
                .bodyValue(body)
                .retrieve()
                .bodyToMono(responseType);
    }
}

请求头、Cookie 是 WebClient 的基础操作,如果你有很多相同 header,直接用 defaultHeader 放在构建阶段就好。特别注意 Cookie 在 HTTP 协议里其实是 MultiValueMap,同名 Cookie 可能会有多个值,业务封装时处理好,别只想到 Map<String,String>。

文件上传通常用 multipart/form-data。上传接口大多数场景是拿到本地 MultipartFile 再转发,可以借助 WebClient 的 BodyInserters:

public Mono<String> upload(String url, MultipartFile file) {
    return webClient.post()
        .uri(url)
        .contentType(MediaType.MULTIPART_FORM_DATA)
        .body(BodyInserters.fromMultipartData("file",
                new FilePart("file", file.getOriginalFilename(),
                        new ByteArrayResource(file.getBytes()))))
        .retrieve()
        .bodyToMono(String.class);
}

这里 FilePart 可以直接接管文件内容,避免先读本地文件再额外拷贝次数。如果文件很大,更推荐流式读取后交给 BodyInserters.fromResource 完成,不然 getBytes() 会有一次完整加载入内存的操作。

很多后端接口会返回动态的 NDJSON 或 SSE,对这种响应不要用 bodyToMono(byte[].class),正确姿势是让响应体按无序块流入 Flux:

public Flux<String> streamLines(String url) {
    return webClient.get()
        .uri(url)
        .accept(MediaType.APPLICATION_NDJSON)
        .retrieve()
        .bodyToFlux(String.class)
        .doOnError(e -> log.error("Stream error", e));
}

4. 连接池才是高并发调用的基石

WebClient 默认底层会用 Netty 的 ConnectionProvider,但它给的不是一劳永逸的配置:最大连接数默认 500,连接池耗尽后的等待超时是 45 秒,空闲连接不主动回收。听起来 500 不少了,可如果你的服务调用了多个下游,每个下游都共用同一个默认连接池,某个下游变慢后,池里的连接会被一批慢请求慢慢占据,其他下游即使很健康,也可能等不到连接把请求发出去。这就是“多服务共享连接池会互相拖累”的典型问题。

我的做法很简单:每个核心下游单独配一个 ConnectionProvider 和 WebClient。构建时可以设置下面几个核心参数:

ConnectionProvider provider = ConnectionProvider.builder("product-service")
    .maxConnections(1000)
    .pendingAcquireTimeout(Duration.ofSeconds(10))
    .pendingAcquireMaxCount(5000)
    .maxIdleTime(Duration.ofSeconds(30))
    .maxLifeTime(Duration.ofSeconds(60))
    .evictInBackground(Duration.ofSeconds(5))
    .build();

HttpClient httpClient = HttpClient.create(provider)
    .compress(true)
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);

WebClient webClient = WebClient.builder()
    .baseUrl("http://product-service:8080")
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

这里几个参数值得讨论:

  • maxConnections 不是越大越好。HTTP/1.1 下同一个连接同一时刻只能发一个请求,如果下游支持 HTTP/2,才能在一个连接上多路复用。异步模型下 1000 个连接已经能支撑很高的 QPS,往上调要评估本机和下游的连接数、内存都吃得消。
  • pendingAcquireTimeout 决定连接池耗尽时调用方愿不愿意等。默认 45 秒太久了,正常情况下高并发时下游就算慢一点,几百毫秒内总能释放连接。超过 10 秒还没拿到连接就没必要再等,直接失败让上游快速重试或降级,比耗在下游上强。
  • maxIdleTime 最好配合下游 Nginx / 网关的空闲连接超时时间设置。比如下游网关 60 秒关闭空闲连接,我们这里把客户端空闲回收设在 45 秒或 50 秒,避免拿到已经被服务端关掉的死连接白白浪费一次请求。

ConnectionProvider 是实现了 Disposable 的资源,如果它被声明成 Spring 的 @Bean,记得指定 destroyMethod = "dispose",防止停机时连接还挂着。实践里我喜欢为每个下游建一个带名字的 provider,这样监控日志里还能看出是哪个服务的连接池出了问题。

5. 超时不要只配一个响应超时,三层各管各的

分布式系统里没有超时的调用等于雪崩加速器。这里说三个层次。

第一是 TCP 连接超时。如果目标 IP 不通,或端口已经被防火墙丢弃,客户端不能无限等下去。用 Netty 的 ChannelOption.CONNECT_TIMEOUT_MILLIS 控制,一般几百毫秒到 3 秒之间。连接超时的默认值其实并不理想,很多场景下默认是 30 秒,你肯定不想让一个失败连接占住用户请求那么久。

第二是响应超时,也可以叫请求整体超时。Reactor 里最干净的做法是给 Mono 应用 timeout 操作符:

webClient.get()
    .uri("/api/order/{id}", id)
    .retrieve()
    .bodyToMono(Order.class)
    .timeout(Duration.ofSeconds(3));

这里注意一个容易踩的小坑:因为 WebClient 返回的是响应式流,timeout 计时是从整个 Mono 被订阅开始算的,不是真正发出网络请求那一刻才起表。所以如果你的 Mono 链条里还有前置的耗时操作,需要把 timeout 放在离真正 webClient.get() 最近的地方,或者干脆从进入 DAO 那一刻就开始用 timeout 包住整个调用链。

第三是读取空闲超时。这个在 Reactor Netty 里没有像传统 HttpClient 那样拆成单独的 “Socket read timeout” 配置。我之前查了一下,HTTP/1.1 场景下 Netty 本身并没有直接的读超时属性,你需要自己实现或者加 IdleStateHandler。对绝大多数内部服务调用,连接超时 + 整体超时已经足够兜底,真碰到了长时间不吐数据的服务,多半是服务端逻辑卡死,整体超时就会干掉。

设置值不能一刀切:连接超时建议 1-3 秒,便宜;业务请求的整体超时建议根据接口 P99 延迟留足余量,默认 3 秒到 5 秒都行,但是建议从配置中心读取,而不是写死在代码里。线上经常需要调整,万一有接口被压测逼着改超时,改配置文件比发版快得多。

说到 timeout 超时后,单纯发出 TimeoutException 不代表底层网络连接会被立刻取消。Netty 底层可能还在等待响应而占用连接。如果需要超时后立刻断开这个挂起的请求,可以在 Mono 上补充:

.cancelOnError(TimeoutException.class)

cancelOnError 会在匹配到该异常时主动向底层连接传播取消信号,避免连接一直被悬着。这个细节在连接池很紧张时特别重要。

6. ExchangeFilterFunction 做日志、鉴权、TraceId 透传比拦截器还顺手

WebClient 真正友好的地方是过滤器 ExchangeFilterFunction,相当于一个可以装在客户端上的拦截器,在请求发出前改请求,在响应返回后看状态。生产上很多横切逻辑都可以放在这里。

日志最基础的是打印方法、URL、耗时和响应码:

ExchangeFilterFunction logFilter() {
    return (request, next) -> {
        long start = System.currentTimeMillis();
        return next.exchange(request)
                .doOnNext(response -> log.info("{} {} -> {} {} ms",
                        request.method(), request.url(),
                        response.statusCode(), System.currentTimeMillis() - start));
    };
}

如果想把请求体也打出来,就要小心了:ClientRequest.body 是一个 Publisher,通常没法安全地被消费两次。所以记录请求体的过滤器不能用于大请求,只能把内容缓冲到内存里再用。生产上我很少在过滤器里打请求体,真要完全报文会直接开 Netty wiretap,或走独立的访问日志采集。

鉴权也很常见。比如每个外部调用都要带动态的 OAuth2 token,你可以用 Redis 缓存 token,然后在过滤器里统一加上:

ExchangeFilterFunction authFilter(TokenSupplier supplier) {
    return (request, next) -> supplier.getToken()
            .map(token -> ClientRequest.from(request)
                    .headers(h -> h.setBearerAuth(token))
                    .build())
            .flatMap(next::exchange);
}

注意这里 ClientRequest.from(request) 会把原 request 重新构建一份,已有的 header、cookie 等都会保留,你只要在它的基础上替换 Header 即可。

TraceId 透传过滤器在链路追踪中是必备的。我们内部用 Micrometer Tracing + Brave,通常上下文里已经带了 TraceId,读取后塞进传出的 header 即可:

ExchangeFilterFunction tracingFilter() {
    return (request, next) -> Mono.deferContextual(ctx -> {
        String traceId = ctx.getOrEmpty("traceId").orElse("unknown");
        ClientRequest newRequest = ClientRequest.from(request)
                .header("X-Trace-Id", traceId)
                .build();
        return next.exchange(newRequest);
    });
}

用 Mono.deferContextual 是为了从 Reactor Context 里拿值。注意这个 Context 不是魔法,只有你把 traceId 放在 Mono 的上游并在同一条调用链上订阅,过滤器才能看到它。最方便的是在入口写一个 operator,把当前 TraceId 放到 Reactor Context 里,之后所有调用都会自然拿到。

过滤器的注册顺序是有讲究的。WebClient 会按顺序组成调用链,放在前面的过滤器先执行。一般顺序是:日志在外层(能记录全程耗时和状态)、鉴权在中间、TraceId 尽量靠前但必须放在能拿到 traceId 的 Context 准备好之后。真到了复杂项目里,你可以用一张 List<ExchangeFilterFunction> 控制顺序,别靠人记忆。

7. 重试和退避要讲究,不能无脑 retry

WebClient 的动态对象是 Mono,天然支持 Reactor 的重试操作符。最简单粗暴的是:

Mono<Order> orderResult = webClient.get()
        .uri("/orders/{id}", id)
        .retrieve()
        .bodyToMono(Order.class)
        .retryWhen(Retry.backoff(3, Duration.ofMillis(500))
                .maxBackoff(Duration.ofSeconds(5))
                .jitter(0.5));

如果直接这样用,Retry.backoff 会对所有异常都重试,包括 404、400 这种客户端错误。你重试一百次也是 404,没有任何意义,还浪费下游资源。所以需要一个过滤器,只对真的值得重试的错误重试:

.retryWhen(Retry.backoff(3, Duration.ofMillis(500))
    .filter(throwable -> throwable instanceof IOException
            || throwable instanceof HttpServerErrorException))

这里 HttpServerErrorException 是 5xx 响应抛出的异常。注意 retrieve() 对 4xx/5xx 默认抛的是 WebClientResponseException 的子类,直接拿父类过滤会把 4xx 也包进去,不如用 HttpServerErrorException 或者自定义异常更精确。

另外要记得,重试的前提是请求幂等。GET、PUT、DELETE 这类操作可以重试,POST 就要谨慎。如果你是支付、下单之类的服务,重试前必须保证对端有幂等键,或者你能在失败时通过请求 ID 去查询原始结果,否则重试会造成重复下单,宁可不要自动重试。

再来说熔断。生产上不能只靠 Reactor 的 retry,因为如果下游已经处于熔断状态,重试只会继续压垮它。比较大的系统里,我会把 Resilience4j 的 CircuitBreaker 和 Reactor 重试搭配在一起:外层用 Resilience4j 控制快速失败,内层用 Reactor 重试处理临时性的抖动。

Spring Boot 3 项目中可以这样接入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

配置一个全局的 ReactiveResilience4JCircuitBreakerFactory:

@Bean
ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory() {
    ReactiveResilience4JCircuitBreakerFactory factory = new ReactiveResilience4JCircuitBreakerFactory();
    factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .circuitBreakerConfig(io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.custom()
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(10))
                    .slidingWindowSize(20)
                    .build())
            .timeLimiterConfig(TimeLimiterConfig.custom()
                    .timeoutDuration(Duration.ofSeconds(3))
                    .build())
            .build());
    return factory;
}

然后在调用里直接用 factory.create("orderService").run(mono):

@Autowired
ReactiveResilience4JCircuitBreakerFactory circuitBreakerFactory;

public Mono<Order> getOrder(String id) {
    Mono<Order> source = webClient.get()
            .uri("/orders/{id}", id)
            .retrieve()
            .bodyToMono(Order.class);
    return circuitBreakerFactory.create("orderService").run(source);
}

这里 timeLimiter 和刚才说的 Mono.timeout 有个功能区别:Mono.timeout 超时后,Reactor 只是向订阅者发出异常,如果没有做融断统计,不会把这次超时算进熔断器里;而 Resilience4J 的 TimeLimiter 超时后会被 CircuitBreaker 记录为一次失败,并影响熔断器的滑动窗口。

一般最终模板是:Resilience4J 在最外面包一层,里面再放一个 retryWhen,各种配置从配置中心拉。而且你还要给每个下游服务起好名字,这样监控面板上能直接看到 orderService、productService 各自的失败率、熔断状态。

8. 线程模型、背压和内存安全的几个自觉要求

WebClient 的底层是 Netty,真正跑 I/O 的线程就只有几个 EventLoop。如果下游响应很慢,那几个线程并不会被占死,而是一直处理其他连接的事件。回调默认发生在发起 I/O 的那个 EventLoop 线程,而非“调用方的业务线程”。这个和平时用 RestTemplate 完全不同,写业务时要搞清楚。

有个最常见的坏习惯:在一个 Netty EventLoop 线程上直接调用 block(),因为 event loop 也要等结果,如果这个结果依赖其他 event loop 回调,就很容易死锁或者把整个 Netty 循环卡死。如果项目是传统 Spring MVC,在 Tomcat 线程里用 block() 没问题,但也不能把这个习惯带到 WebFlux 里。如果你需要让某些回调切换线程,比如后续要调用 JDBC 阻塞库,就明确用 publishOn:

webClient.get().uri(...).retrieve().bodyToMono(Order.class)
    .publishOn(Schedulers.boundedElastic())
    .flatMap(this::doBlockingDatabaseCall)

内存安全也有几个隐形坑:

  • 大文件、大响应体不要用 bodyToMono(byte[].class),数据会全部载入内存。要按块流式处理,比如 bodyToFlux(DataBuffer.class) 或者 DataBufferUtils.write。
  • 响应体的内存上限最好明确设一下。HttpClient 里有一些用于聚合的默认内存限制,但更多是 WebClient 解码器自己决定的。你可以通过 codec 配置限制单条消息的最大内存:
WebClient.builder()
    .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
    .build();

这里设置的 maxInMemorySize 主要针对消息解码时能缓冲的最大字节数。如果下游动不动给你返回几百 MB 的 JSON,不管客户端解不解析,上游都得先掂量一下是否需要这么大响应,别把内存炸了。

  • 还有背压的问题。响应式流在订阅后是“按需拉取”,默认情况下 Netty 接收数据和 Reactor 的 request 量联动。如果你的 Flux 订阅者请求量很少,TCP 窗口会自动缩小,从而形成自然背压,不会一次性把响应体灌进内存。这个能力是传统 RestTemplate 不具备的,但有背压不代表没有风险,如果代码里用 .collectList() 之类的操作把整个 Flux 聚合成一个 List,那又等于把所有数据一次性捞回来了,和 bodyToMono(byte[].class) 没有区别。

9. 生产级封装:每个下游一个实例,指标必须能看见

到了生产环境,基本就能看到真实世界里不是干净的 WebClient 直接用,而是每个下游一个封装类、一个独立连接池。下面是我在多个项目里用过的一个结构。

首先给每个下游服务建一个专用的 WebClient Bean:

@Configuration
public class DownstreamWebClientConfig {

    @Bean(destroyMethod = "dispose")
    ConnectionProvider productServiceProvider() {
        return ConnectionProvider.builder("product-service")
                .maxConnections(300)
                .pendingAcquireTimeout(Duration.ofSeconds(10))
                .maxIdleTime(Duration.ofSeconds(45))
                .evictInBackground(Duration.ofSeconds(5))
                .build();
    }

    @Bean
    WebClient productServiceWebClient(ConnectionProvider productServiceProvider) {
        HttpClient httpClient = HttpClient.create(productServiceProvider)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .responseTimeout(Duration.ofSeconds(5));
        return WebClient.builder()
                .baseUrl("http://product-service")
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .filter(new MetricsFilter(...))
                .filter(new TracingFilter(...))
                .build();
    }
}

封装类里要再做一层异常映射。外部服务返回 5xx、连接超时、熔断异常等等,都翻译成我们自己的领域异常,不要让业务层到处去 catch WebClientResponseException、TimeoutException 这些东西。我给团队封装的基类大致长这样:

public abstract class BaseDownstreamClient {
    protected final WebClient webClient;

    protected BaseDownstreamClient(WebClient webClient) {
        this.webClient = webClient;
    }

    protected <T> Mono<T> doGet(String uri, Map<String, String> headers, Class<T> responseType) {
        return webClient.get()
                .uri(uri)
                .headers(h -> headers.forEach(h::add))
                .retrieve()
                .bodyToMono(responseType)
                .onErrorMap(WebClientRequestException.class, e -> new DownstreamUnavailableException("downstream connection failed", e))
                .onErrorMap(HttpServerErrorException.class, e -> new DownstreamServerException(e.getStatusCode(), e));
    }
}

具体每个下游的 Client 再继承它,把各自的 baseUrl、重试、熔断参数传进来,一般 200 行左右就能写完一个规范的调用器。

关于指标监控,我不建议自己随便发明。Micrometer 提供了 ExchangeFilterFunction 的拦截方式,可以给所有下游调用打点。比如定义每个下游的 downstream tag,用 Timer 统计耗时分布,用计数统计 Http 状态比例如下:

public class MetricsFilter implements ExchangeFilterFunction {
    private final MeterRegistry meterRegistry;
    private final String downstreamName;

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        long start = System.nanoTime();
        return next.exchange(request)
                .doOnNext(response -> record(start, request, response.statusCode().value()))
                .doOnError(error -> record(start, request, classifyError(error)));
    }

    private void record(long start, ClientRequest request, int status) {
        Timer timer = Timer.builder("http.client.requests")
                .tag("downstream", downstreamName)
                .tag("method", request.method().name())
                .tag("status", String.valueOf(status))
                .register(meterRegistry);
        timer.record(Duration.ofNanos(System.nanoTime() - start));
    }
}

如果项目已经用 Spring Boot 3 + Micrometer Tracing,更正确的方式是直接用自动配置好的 WebClient.Builder。Spring Boot 会自动给这个 Builder 注入 Observation 过滤器,WebClient 每次调用会自动生成 traceId、spanId,并且上报 http.client.requests 指标。你只要保证:

  • classpath 里有 actuator 和 micrometer-tracing-bridge-brave;
  • 注入的是自动配置的 WebClient.Builder,而不是手动 WebClient.create()。

如果确实要用手动 WebClient.builder(),需要使用 .observationRegistry(observationRegistry) 把 ObservationRegistry 关联进去。不过这个时候你还得考虑给它配一个观察约定,相比直接用自动配置要麻烦得多。我更推荐能自动就不手动。

连接池自身的指标也不能忽视。Reactor Netty 的 Provider 不直接暴露给 Micrometer,但你可以把当前池里的活跃连接数、等待连接数通过过滤器上报成 Gauge。上线初期先在测试环境把各下游的池大小、等待度压一遍,避免到了高并发出现 45 秒的 acquire 超时。

总结

响应式 HTTP 不是简单替换一个类。它真正改变了调用链路的资源模型:若干 EventLoop 线程承载海量 I/O,连接池隔离防止某个下游拖垮全局,超时分层保证该挂的时候快速挂,retry + 熔断确保系统不因临时抖动直接崩掉,而 TraceId/指标则是长期运行时的“眼睛”。

工具都摆在眼前,最需要考虑的还是取舍。什么时候该切 WebClient,什么时候继续使用阻塞,要结合团队对 Reactor 的掌握程度和现有技术栈。功能上 WebClient 几乎什么都有,但从同步思维转过来那一步,需要靠项目约束和 code review 才能避免踩进线程和背压的坑里。


🎁 福利时间

如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。

知识库地址:https://farerboy.com/


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_26629277/article/details/168436584

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--