资讯动态

webclient

发布时间:2026/9/8 17:05:13 来源:尧图企业网站定制
依赖 版本springboot 版本 2.6.1dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-webflux/artifactId/dependencywebclient配置类packagecom.huayi.iepms.common.feign.wyy;importio.netty.channel.ChannelOption;importio.netty.handler.timeout.ReadTimeoutHandler;importio.netty.handler.timeout.WriteTimeoutHandler;importorg.springframework.cloud.client.loadbalancer.LoadBalanced;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.client.reactive.ReactorClientHttpConnector;importorg.springframework.web.reactive.function.client.ExchangeStrategies;importorg.springframework.web.reactive.function.client.WebClient;importreactor.netty.http.client.HttpClient;importjava.time.Duration;importjava.util.concurrent.TimeUnit;/** * WebClient配置类 * * author zrf * date 2026/09/04 11:00 */ConfigurationpublicclassWebClientConfig{/** * 创建一个 WebClient.Builder 实例并启用负载均衡 */BeanLoadBalancedpublicWebClient.BuilderwebClientBuilder(){returnWebClient.builder();}/** * 创建 WebClient 实例并配置超时和缓冲区 */BeanpublicWebClientwebClient(WebClient.BuilderwebClientBuilder){// 1. 配置底层 Netty HttpClient 超时参数HttpClienthttpClientHttpClient.create()// TCP 连接超时时间10秒.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,10000)// 读写超时时间300秒适用于大文件传输.doOnConnected(conn-conn.addHandlerLast(newReadTimeoutHandler(300,TimeUnit.SECONDS)).addHandlerLast(newWriteTimeoutHandler(300,TimeUnit.SECONDS)))// 整体响应超时从发请求到接收完300秒.responseTimeout(Duration.ofSeconds(300));// 2. 配置内存缓冲区大小防止大文本/大 JSON 报错ExchangeStrategiesexchangeStrategiesExchangeStrategies.builder().codecs(configurer-configurer.defaultCodecs()// 设置为 50MB默认是 256KB可按需调整.maxInMemorySize(50*1024*1024)).build();returnwebClientBuilder.clientConnector(newReactorClientHttpConnector(httpClient)).exchangeStrategies(exchangeStrategies).build();}}LoadBalanced 注解说明LoadBalanced是 Spring Cloud 提供的注解用于标记WebClient.Builder或RestTemplate实例使其具备客户端负载均衡能力。其核心作用如下服务名解析启用后WebClient在发起请求时会将 URL 中的服务名如http://iepms-file/file/upload自动解析为实际的服务实例地址而无需手动拼接 IP 和端口。负载均衡策略当目标服务存在多个实例时LoadBalanced会结合LoadBalancerClient或ReactiveLoadBalancer自动选择一个实例进行调用默认采用轮询策略也可通过配置切换为随机、权重等策略。与注册中心集成该注解通常与 Nacos、Eureka 等注册中心配合使用WebClient会从注册中心获取服务实例列表实现服务间的动态发现与调用。注意LoadBalanced仅对WebClient.Builder生效且必须在Bean方法上标注。若直接使用WebClient.create()创建实例则不具备负载均衡能力。通过 application.yml 配置超时参数WebClient 基于 Reactor Netty 实现其连接超时、读取超时等参数可通过application.yml进行配置。示例配置如下spring:codec:max-in-memory-size:50MB# 响应体最大内存限制与代码中保持一致# 自定义 WebClient 超时配置推荐方式webclient:connect-timeout:10000# 连接超时时间毫秒默认 5000read-timeout:300# 读取超时时间秒write-timeout:300# 写入超时时间秒response-timeout:300# 整体响应超时时间秒若需在代码中动态读取这些配置并应用到WebClient可在配置类中注入Value或使用ConfigurationPropertiesConfigurationpublicclassWebClientConfig{Value(${webclient.connect-timeout:10000})privateintconnectTimeout;Value(${webclient.read-timeout:300})privateintreadTimeout;Value(${webclient.write-timeout:300})privateintwriteTimeout;Value(${webclient.response-timeout:300})privateintresponseTimeout;BeanLoadBalancedpublicWebClient.BuilderwebClientBuilder(){HttpClienthttpClientHttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS,connectTimeout).doOnConnected(conn-conn.addHandlerLast(newReadTimeoutHandler(readTimeout,TimeUnit.SECONDS)).addHandlerLast(newWriteTimeoutHandler(writeTimeout,TimeUnit.SECONDS))).responseTimeout(Duration.ofSeconds(responseTimeout));returnWebClient.builder().clientConnector(newReactorClientHttpConnector(httpClient));}}说明ChannelOption.CONNECT_TIMEOUT_MILLIS控制连接超时responseTimeout控制整体响应超时ReadTimeoutHandler和WriteTimeoutHandler分别控制读写超时。建议将超时参数统一收敛到application.yml中便于后续调整而无需重新编译。webclient工具类package com.huayi.iepms.common.feign.wyy;importcom.huayi.satoken.utils.LoginUtils;importorg.springframework.core.ParameterizedTypeReference;importorg.springframework.core.io.buffer.DataBuffer;importorg.springframework.core.io.buffer.DataBufferUtils;importorg.springframework.http.HttpHeaders;importorg.springframework.http.MediaType;importorg.springframework.http.client.MultipartBodyBuilder;importorg.springframework.stereotype.Component;importorg.springframework.util.Assert;importorg.springframework.util.LinkedMultiValueMap;importorg.springframework.util.MultiValueMap;importorg.springframework.util.StringUtils;importorg.springframework.web.multipart.MultipartFile;importorg.springframework.web.reactive.function.BodyInserters;importorg.springframework.web.reactive.function.client.WebClient;importreactor.core.publisher.Flux;importreactor.core.publisher.Mono;importjavax.annotation.Resource;importjavax.servlet.http.HttpServletResponse;importjava.io.OutputStream;importjava.net.URLEncoder;importjava.nio.charset.StandardCharsets;importjava.util.Collections;importjava.util.Map;importjava.util.function.Consumer;/** * 通用 WebClient HTTP 工具类 * * author zrf * date2026/09/04 */ Component public class WebClientUtil{Resource private WebClient webClient;//1. 基础 GET 请求/** * 同步 GET 请求返回对象/Map/List等 */ publicTT get(String url, MapString, ObjectqueryParams, ClassTresponseType){returnget(url, queryParams, null, responseType);}/** * 同步 GET 请求支持泛型如 ResultListUser */ publicTT get(String url, MapString, ObjectqueryParams, MapString, Stringheaders, ParameterizedTypeReferenceTtypeRef){returnbuildGetRequest(url, queryParams, headers).retrieve().bodyToMono(typeRef).block();}publicTT get(String url, MapString, ObjectqueryParams, MapString, Stringheaders, ClassTresponseType){returnbuildGetRequest(url, queryParams, headers).retrieve().bodyToMono(responseType).block();}//2. 基础 POST 请求/** * 同步 POST JSON 请求 */ publicTT postJson(String url, Object body, ClassTresponseType){returnpostJson(url, body, null, responseType);}publicTT postJson(String url, Object body, MapString, Stringheaders, ClassTresponseType){returnwebClient.post().uri(url).headers(applyHeaders(headers)).contentType(MediaType.APPLICATION_JSON).bodyValue(body!null ? body:Collections.emptyMap()).retrieve().bodyToMono(responseType).block();}/** * 同步 POST Form 表单请求 */ publicTT postForm(String url, MapString, StringformData, ClassTresponseType){MultiValueMapString, StringparamMapnew LinkedMultiValueMap();if(formData!null){paramMap.setAll(formData);}returnwebClient.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 附加的普通参数表单 */ publicTT uploadFiles(String url, MapString, MultipartFilefileMap, MapString, ObjectparamMap, ClassTresponseType){Assert.hasText(url,URL不能为空);Assert.notEmpty(fileMap,上传文件不能为空);MultipartBodyBuilder buildernew MultipartBodyBuilder();// 填充文件 fileMap.forEach((key,file)-{ if(file!null!file.isEmpty()){String originalFilenamefile.getOriginalFilename();String contentTypefile.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);}returnwebClient.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, MultipartFilefile){returnuploadFiles(url, Collections.singletonMap(file,file), null, String.class);}/** * 浏览器下载文件直接写入 HttpServletResponse */ public void downloadToResponse(String url, String fileName, HttpServletResponse response, MapString, Stringheaders){Assert.hasText(url,下载地址不能为空);Assert.hasText(fileName,文件名称不能为空);Assert.notNull(response,HttpServletResponse 不能为 null);try{response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);String encodedFileNameURLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace(,%20);response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format(attachment; filename\%s\; filename*UTF-8%s, encodedFileName, encodedFileName));FluxDataBufferdataBufferFluxdownloadStream(url, headers);OutputStream outputStreamresponse.getOutputStream();//手动消费 buffer避开 DataBufferUtils.write()dataBufferFlux .doOnNext(buffer -{try{int readablebuffer.readableByteCount();if(readable0){byte[]bytesnew 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 FluxDataBufferdownloadStream(String url, MapString, Stringheaders){returnwebClient.get().uri(url).headers(applyHeaders(headers)).retrieve().bodyToFlux(DataBuffer.class);}//4. 辅助私有方法private WebClient.RequestHeadersSpec?buildGetRequest(String url, MapString, ObjectqueryParams, MapString, Stringheaders){Assert.hasText(url,URL不能为空);returnwebClient.get().uri(uriBuilder -{uriBuilder.path(url);if(queryParams!null){queryParams.forEach(uriBuilder::queryParam);}returnuriBuilder.build();}).headers(applyHeaders(headers));}private ConsumerHttpHeadersapplyHeaders(MapString, StringcustomHeaders){returnhttpHeaders -{if(customHeaders!null){customHeaders.forEach(httpHeaders::set);}// 自动补全全局系统 Token String tokenLoginUtils.getToken();if(StringUtils.hasText(token)){httpHeaders.set(hy-token, token);}};}}工具使用例子//1. GET 请求带 Query 参数并解析为对象 UserDTO userwebClientUtil.get(http://service-b/user/info, Map.of(userId,123), UserDTO.class);//2. GET 请求解析复杂的 ListDTOListUserDTOlistwebClientUtil.get(http://service-b/user/list, null, null, new ParameterizedTypeReferenceListUserDTO(){});//3. POST 发送 JSON ResultVO reswebClientUtil.postJson(http://service-b/user/create, createReq, ResultVO.class);//4. 上传文件 String resultwebClientUtil.upload(http://service-b/file/upload, multipartFile);//5. 浏览器下载文件 webClientUtil.downloadToResponse(http://service-b/file/download?id1,账单.pdf, response, null);

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价