webclient

依赖 版本

springboot 版本 2.6.1

bash 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

webclient配置类

java 复制代码
package com.huayi.iepms.common.feign.wyy;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
 * WebClient配置类
 *
 * @author zrf
 * @date 2026/09/04 11:00
 */
@Configuration
public class WebClientConfig {

    /**
     * 创建一个 WebClient.Builder 实例,并启用负载均衡
     */
    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }

    /**
     * 创建 WebClient 实例并配置超时和缓冲区
     */
    @Bean
    public WebClient webClient(WebClient.Builder webClientBuilder) {
        // 1. 配置底层 Netty HttpClient 超时参数
        HttpClient httpClient = HttpClient.create()
                // TCP 连接超时时间:10秒
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                // 读写超时时间:300秒(适用于大文件传输)
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(300, TimeUnit.SECONDS))
                        .addHandlerLast(new WriteTimeoutHandler(300, TimeUnit.SECONDS))
                )
                // 整体响应超时(从发请求到接收完):300秒
                .responseTimeout(Duration.ofSeconds(300));

        // 2. 配置内存缓冲区大小(防止大文本/大 JSON 报错)
        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
                .codecs(configurer -> configurer.defaultCodecs()
                        // 设置为 50MB(默认是 256KB),可按需调整
                        .maxInMemorySize(50 * 1024 * 1024))
                .build();

        return webClientBuilder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .exchangeStrategies(exchangeStrategies)
                .build();
    }
}

@LoadBalanced 注解说明

@LoadBalanced 是 Spring Cloud 提供的注解,用于标记 WebClient.BuilderRestTemplate 实例,使其具备客户端负载均衡能力。其核心作用如下:

  • 服务名解析 :启用后,WebClient 在发起请求时,会将 URL 中的服务名(如 http://iepms-file/file/upload)自动解析为实际的服务实例地址,而无需手动拼接 IP 和端口。
  • 负载均衡策略 :当目标服务存在多个实例时,@LoadBalanced 会结合 LoadBalancerClientReactiveLoadBalancer 自动选择一个实例进行调用,默认采用轮询策略,也可通过配置切换为随机、权重等策略。
  • 与注册中心集成 :该注解通常与 Nacos、Eureka 等注册中心配合使用,WebClient 会从注册中心获取服务实例列表,实现服务间的动态发现与调用。

注意@LoadBalanced 仅对 WebClient.Builder 生效,且必须在 @Bean 方法上标注。若直接使用 WebClient.create() 创建实例,则不具备负载均衡能力。

通过 application.yml 配置超时参数

WebClient 基于 Reactor Netty 实现,其连接超时、读取超时等参数可通过 application.yml 进行配置。示例配置如下:

yaml 复制代码
spring:
  codec:
    max-in-memory-size: 50MB   # 响应体最大内存限制,与代码中保持一致

# 自定义 WebClient 超时配置(推荐方式)
webclient:
  connect-timeout: 10000        # 连接超时时间(毫秒),默认 5000
  read-timeout: 300             # 读取超时时间(秒)
  write-timeout: 300            # 写入超时时间(秒)
  response-timeout: 300         # 整体响应超时时间(秒)

若需在代码中动态读取这些配置并应用到 WebClient,可在配置类中注入 @Value 或使用 @ConfigurationProperties

java 复制代码
@Configuration
public class WebClientConfig {

    @Value("${webclient.connect-timeout:10000}")
    private int connectTimeout;

    @Value("${webclient.read-timeout:300}")
    private int readTimeout;

    @Value("${webclient.write-timeout:300}")
    private int writeTimeout;

    @Value("${webclient.response-timeout:300}")
    private int responseTimeout;

    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.SECONDS))
                        .addHandlerLast(new WriteTimeoutHandler(writeTimeout, TimeUnit.SECONDS))
                )
                .responseTimeout(Duration.ofSeconds(responseTimeout));

        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient));
    }
}

说明ChannelOption.CONNECT_TIMEOUT_MILLIS 控制连接超时,responseTimeout 控制整体响应超时,ReadTimeoutHandlerWriteTimeoutHandler 分别控制读写超时。建议将超时参数统一收敛到 application.yml 中,便于后续调整而无需重新编译。

webclient工具类

bash 复制代码
package com.huayi.iepms.common.feign.wyy;

import com.huayi.satoken.utils.LoginUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;

/**
 * 通用 WebClient HTTP 工具类
 *
 * @author zrf
 * @date 2026/09/04
 */
@Component
public class WebClientUtil {

    @Resource
    private WebClient webClient;

    // ==================== 1. 基础 GET 请求 ====================

    /**
     * 同步 GET 请求(返回对象/Map/List等)
     */
    public <T> T get(String url, Map<String, Object> queryParams, Class<T> responseType) {
        return get(url, queryParams, null, responseType);
    }

    /**
     * 同步 GET 请求(支持泛型,如 Result<List<User>>)
     */
    public <T> T get(String url, Map<String, Object> queryParams, Map<String, String> headers, ParameterizedTypeReference<T> typeRef) {
        return buildGetRequest(url, queryParams, headers)
                .retrieve()
                .bodyToMono(typeRef)
                .block();
    }

    public <T> T get(String url, Map<String, Object> queryParams, Map<String, String> headers, Class<T> responseType) {
        return buildGetRequest(url, queryParams, headers)
                .retrieve()
                .bodyToMono(responseType)
                .block();
    }

    // ==================== 2. 基础 POST 请求 ====================

    /**
     * 同步 POST JSON 请求
     */
    public <T> T postJson(String url, Object body, Class<T> responseType) {
        return postJson(url, body, null, responseType);
    }

    public <T> T postJson(String url, Object body, Map<String, String> headers, Class<T> responseType) {
        return webClient.post()
                .uri(url)
                .headers(applyHeaders(headers))
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(body != null ? body : Collections.emptyMap())
                .retrieve()
                .bodyToMono(responseType)
                .block();
    }

    /**
     * 同步 POST Form 表单请求
     */
    public <T> T postForm(String url, Map<String, String> formData, Class<T> responseType) {
        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
        if (formData != null) {
            paramMap.setAll(formData);
        }
        return webClient.post()
                .uri(url)
                .headers(applyHeaders(null))
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .body(BodyInserters.fromFormData(paramMap))
                .retrieve()
                .bodyToMono(responseType)
                .block();
    }

    // ==================== 3. 文件上传与下载 ====================

    /**
     * 单/多文件上传
     *
     * @param url       上传接口
     * @param fileMap   文件表单键值对,例如 ("file", multipartFile)
     * @param paramMap  附加的普通参数表单
     */
    public <T> T uploadFiles(String url, Map<String, MultipartFile> fileMap, Map<String, Object> paramMap, Class<T> responseType) {
        Assert.hasText(url, "URL不能为空");
        Assert.notEmpty(fileMap, "上传文件不能为空");

        MultipartBodyBuilder builder = new MultipartBodyBuilder();

        // 填充文件
        fileMap.forEach((key, file) -> {
            if (file != null && !file.isEmpty()) {
                String originalFilename = file.getOriginalFilename();
                String contentType = file.getContentType();
                builder.part(key, file.getResource())
                        .filename(StringUtils.hasText(originalFilename) ? originalFilename : "unknown")
                        .contentType(MediaType.parseMediaType(
                                StringUtils.hasText(contentType) ? contentType : MediaType.APPLICATION_OCTET_STREAM_VALUE
                        ));
            }
        });

        // 填充普通字段
        if (paramMap != null) {
            paramMap.forEach(builder::part);
        }

        return webClient.post()
                .uri(url)
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .headers(applyHeaders(null))
                .body(BodyInserters.fromMultipartData(builder.build()))
                .retrieve()
                .bodyToMono(responseType)
                .block();
    }

    /**
     * 快捷单文件上传
     */
    public String upload(String url, MultipartFile file) {
        return uploadFiles(url, Collections.singletonMap("file", file), null, String.class);
    }

    /**
     * 浏览器下载文件(直接写入 HttpServletResponse)
     */
       public void downloadToResponse(String url, String fileName, HttpServletResponse response, Map<String, String> headers) {
        Assert.hasText(url, "下载地址不能为空");
        Assert.hasText(fileName, "文件名称不能为空");
        Assert.notNull(response, "HttpServletResponse 不能为 null");

        try {
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                    String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", encodedFileName, encodedFileName));

            Flux<DataBuffer> dataBufferFlux = downloadStream(url, headers);
            OutputStream outputStream = response.getOutputStream();

            // ========== 手动消费 buffer,避开 DataBufferUtils.write() ==========
            dataBufferFlux
                    .doOnNext(buffer -> {
                        try {
                            int readable = buffer.readableByteCount();
                            if (readable > 0) {
                                byte[] bytes = new byte[readable];
                                buffer.read(bytes);
                                outputStream.write(bytes);
                            }
                        } catch (IOException e) {
                            throw new RuntimeException("写入响应流失败", e);
                        } finally {
                            // 必须释放,否则内存泄漏
                            DataBufferUtils.release(buffer);
                        }
                    })
                    .blockLast();
            // ========================================================================

            outputStream.flush();

        } catch (Exception e) {
            throw new RuntimeException("文件下载失败,url: " + url + ", fileName: " + fileName, e);
        }
    }

    /**
     * 获取文件流(响应式非阻塞,适用于服务间数据转发)
     */
    public Flux<DataBuffer> downloadStream(String url, Map<String, String> headers) {
        return webClient.get()
                .uri(url)
                .headers(applyHeaders(headers))
                .retrieve()
                .bodyToFlux(DataBuffer.class);
    }

    // ==================== 4. 辅助私有方法 ====================

    private WebClient.RequestHeadersSpec<?> buildGetRequest(String url, Map<String, Object> queryParams, Map<String, String> headers) {
        Assert.hasText(url, "URL不能为空");
        return webClient.get()
                .uri(uriBuilder -> {
                    uriBuilder.path(url);
                    if (queryParams != null) {
                        queryParams.forEach(uriBuilder::queryParam);
                    }
                    return uriBuilder.build();
                })
                .headers(applyHeaders(headers));
    }

    private Consumer<HttpHeaders> applyHeaders(Map<String, String> customHeaders) {
        return httpHeaders -> {
            if (customHeaders != null) {
                customHeaders.forEach(httpHeaders::set);
            }
            // 自动补全全局系统 Token
            String token = LoginUtils.getToken();
            if (StringUtils.hasText(token)) {
                httpHeaders.set("hy-token", token);
            }
        };
    }
}

工具使用例子

bash 复制代码
// 1. GET 请求带 Query 参数并解析为对象
UserDTO user = webClientUtil.get("http://service-b/user/info", Map.of("userId", 123), UserDTO.class);

// 2. GET 请求解析复杂的 List<DTO>
List<UserDTO> list = webClientUtil.get("http://service-b/user/list", null, null, new ParameterizedTypeReference<List<UserDTO>>() {});

// 3. POST 发送 JSON
ResultVO res = webClientUtil.postJson("http://service-b/user/create", createReq, ResultVO.class);

// 4. 上传文件
String result = webClientUtil.upload("http://service-b/file/upload", multipartFile);

// 5. 浏览器下载文件
webClientUtil.downloadToResponse("http://service-b/file/download?id=1", "账单.pdf", response, null);
相关推荐
神仙别闹2 小时前
基于 C++ 实现两个有序链表序列的交集
java·c++·链表
swordbob2 小时前
ReentrantLock 与 AQS 完整学习手册
java·开发语言
m0_587383003 小时前
全民健身解决方案软件开发实战:从架构设计到落地指南
java·spring boot·spring·架构·需求分析
白山编程大哥3 小时前
Java OutputStreamWriter 详解:从字符到字节的桥梁
java·开发语言·python
一技安身4 小时前
【信创】Docker‑Compose V2 两种离线部署(独立模式、插件模式)简易教程
java·docker·eureka
七夜zippoe5 小时前
为什么 2026 年每个 Java 团队都该懂 AI Agent
java·开发语言·人工智能
时凌云.5 小时前
【2026最新】JDK 下载安装与环境配置全教程(Windows/Mac/Linux 三平台,零基础友好)
java·linux·macos
Despacito10065 小时前
Java后端性能探查工具速查表
java·开发语言
蓝桉柒75 小时前
下载idea,用idea输入一个程序
java·ide·intellij-idea