资讯动态

Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理

发布时间:2026/9/26 18:24:20 来源:尧图企业网站定制
Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理调外部接口的代码,大多数人是从RestTemplate起步的。写了几年之后会发现两个问题:一是RestTemplate在 Spring 6 里已经进入维护模式,官方文档明确写「未来可能被弃用」,只修 bug 不加新功能;二是想换成响应式的WebClient,又得把整个技术栈往 Reactor 上靠,一个同步的 CRUD 服务为此引入spring-boot-starter-webflux并不划算。Spring Boot 3.2 给了第三条路:RestClient。同步、阻塞、链式 API,底层默认用 JDK 的HttpClient,但可以换成 Apache HttpClient 或 OkHttp 拿到连接池能力。这篇把集成过程里真正会踩的坑过一遍:超时为什么设了不生效、连接池怎么配、错误处理别再用try/catch包一切。一、最小可用:三行拿到一个可用的客户端先引依赖,同步调用只需要 web starter:dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency不注册任何 Bean,直接构建也能跑:RestClientclientRestClient.create();Stringbodyclient.get().uri(https://api.example.com/users/{id},42).retrieve().body(String.class);uri(String, Object...)的变参是模板占位符,不是简单拼接。这一点比RestTemplate的url id强:参数会被 URI 编码,id里带/、空格、中文都不会把 URL 拆坏。手写拼接的写法遇到name 张三/李四直接就是一个非法 URL。但RestClient.create()每次调用都新建一套底层资源,生产环境不能这么用。要么自己建 Bean,要么交给自动配置。二、交给自动配置:RestClient.Builder 已经准备好了只要类路径上有 web starter,Spring Boot 就会提供一个预配置的RestClient.BuilderBean。自定义时注入这个 Builder 而不是RestClient.create(),因为它已经带上了自动配置好的ClientHttpRequestFactory、HttpMessageConverters,以及可观测性所需的ObservationRegistry。ConfigurationpublicclassHttpClientConfig{BeanRestClientuserClient(RestClient.Builderbuilder,Value(${user.api.base-url})StringbaseUrl){returnbuilder.baseUrl(baseUrl)// 后面 uri() 只写相对路径.defaultHeader(HttpHeaders.ACCEPT,MediaType.APPLICATION_JSON_VALUE).build();}}baseUrl是这类客户端最容易漏的一点。配了它之后,uri(/users/{id}, id)拼出来就是完整地址;不配就得在每个调用点重复写域名,某天域名切测试环境时漏改一处就出事。三、超时:默认是「永不超时」,这才是线上事故的源头RestClient默认用的SimpleClientHttpRequestFactory来自java.net.HttpURLConnection,connectTimeout和readTimeout都是 0,含义是无限等待。对端网络抖动、负载均衡把请求黑洞掉的时候,业务线程就一直挂在那儿,线程池被慢慢占满,最终整个服务一起躺下。所以超时不是「优化项」,是必须配的:BeanRestClientuserClient(RestClient.Builderbuilder,Value(${user.api.base-url})StringbaseUrl){ClientHttpRequestFactorySettingssettingsClientHttpRequestFactorySettings.defaults().withConnectTimeout(Duration.ofSeconds(2))// 建连超时,短一点.withReadTimeout(Duration.ofSeconds(5));// 等响应超时,给业务余量ClientHttpRequestFactoryfactoryClientHttpRequestFactoryBuilder.detect().build(settings);returnbuilder.baseUrl(baseUrl).requestFactory(factory).build();}RestClient没有setConnectTimeout这样的方法,超时只能配置在 request factory 上。这是从RestTemplate迁移过来最容易卡住的地方:找不到设置入口,很多人就干脆不设了。两个超时值的量级要分开思考:connectTimeout反映的是「网络能不能通」,和业务逻辑无关,2 秒足够;readTimeout反映的是「对端处理要多久」,要按对方 P99 留余量,同时必须小于本服务自己的上游超时,否则本服务已经对外报错了,这个线程还堵在等对方响应。四、连接池:不换 factory 就没有池,每次调用都重新握手上面detect()在只有 web starter 的情况下,探测到的仍然是SimpleClientHttpRequestFactory,它没有连接池——每次请求都走一次 TCP 三次握手 TLS 协商。内网调用一次可能就几十毫秒,占了接口总耗时的可观比例。要池子,引 Apache HttpClient 并显式选用:dependencygroupIdorg.apache.httpcomponents.client5/groupIdartifactIdhttpclient5/artifactId/dependencyBeanRestClientuserClient(RestClient.Builderbuilder){PoolingHttpClientConnectionManagercmnewPoolingHttpClientConnectionManager();cm.setMaxTotal(200);// 全部连接的池子总容量cm.setDefaultMaxPerRoute(50);// 单个目标 host 的上限RequestConfigrcRequestConfig.custom().setConnectTimeout(Timeout.ofSeconds(2)).setResponseTimeout(Timeout.ofSeconds(5)).setConnectionRequestTimeout(Timeout.ofSeconds(1))// 从池里借连接的等待上限.build();HttpClienthttpClientHttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(rc).build();returnbuilder.baseUrl(https://api.example.com).requestFactory(newHttpComponentsClientHttpRequestFactory(httpClient)).build();}三个超时里,connectionRequestTimeout最容易被忽略,但它在池化场景下很关键:池子满了、连接借不到时会阻塞,不设上限同样会把线程吊死。它的含义是「借不到连接我可以等多久」,设成 1 秒,快速失败比慢慢排队更符合服务端的期待。setDefaultMaxPerRoute也要按实际并发推。假设服务 QPS 100、单次调用耗时 50ms,按利特尔法则平均需要100 × 0.05 5个并发连接;给到 50 已经有三倍以上的突发空间。设小了会在池内排队,connectionRequestTimeout先炸;设大了只是白占内存,不会更慢。五、错误处理:用状态码断言,别把异常包成万能 catchretrieve()返回的是ResponseSpec。它的默认行为是:4xx / 5xx 抛RestClientResponseException的子类(HttpClientErrorException、HttpServerErrorException)。想按状态码做不同处理,用onStatus:ServicepublicclassUserService{privatefinalRestClientclient;publicUserService(RestClientuserClient){this.clientuserClient;}publicOptionalUserfindById(longid){returnclient.get().uri(/users/{id},id).retrieve().onStatus(HttpStatusCode::is4xxClientError,(req,res)-{if(res.getStatusCode().value()404){thrownewUserNotFoundException(id);// 业务异常,交给上层}thrownewRemoteCallException(调用失败: res.getStatusCode());}).onStatus(HttpStatusCode::is5xxServerError,(req,res)-thrownewRemoteCallException(对方服务异常: res.getStatusCode())).body(User.class);}}这里有个反直觉的点:onStatus是消费者,不是过滤器。它不返回值,只能靠抛异常来表达「这次调用算失败」。所以如果你想在 5xx 时降级返回一个默认对象,不能在onStatus里返回,得换一种写法——直接在exchange里拿原始响应自己判断:Useruserclient.get().uri(/users/{id},id).exchange((req,res)-{if(res.getStatusCode().isError()){returnUser.UNKNOWN;// 降级,不抛}returnres.bodyTo(User.class);});exchange还能读到响应头,比如限流场景要拿Retry-After:.exchange((req,res)-{if(res.getStatusCode().value()429){StringretryAfterres.getHeaders().getFirst(Retry-After);thrownewRateLimitedException(retryAfter);}returnres.bodyTo(Order.class);});需要提醒的是exchange给了你 raw 响应,相应的类型转换也要自己负责,日常能不用就不用,retrieveonStatus已经覆盖九成场景。六、重试:幂等接口才重试,而且要区分哪些错该重调外部接口总有偶发失败。Spring 6 提供了RestClient.Builder#requestInterceptor,但要按响应状态重试,还得靠重试框架。最省事的是 Spring Retry:ConfigurationEnableRetrypublicclassRetryConfig{BeanRestClientuserClient(RestClient.Builderbuilder){returnbuilder.baseUrl(https://api.example.com).requestFactory(/* 前面配好的池化 factory */).build();}}Retryable(retryFor{RemoteCallException.class},noRetryFor{UserNotFoundException.class},// 明确的业务错误不重试maxAttempts3,backoffBackoff(delay200,multiplier2,maxDelay2000))publicUserload(longid){returnuserClient.get().uri(/users/{id},id).retrieve().body(User.class);}三条硬规则:只重试幂等操作。GET、PUT、DELETE 可以,POST 创建订单、扣款这类重试可能造成重复提交。确实需要重试就得带幂等键。4xx 基本不重试。参数错了、没权限,重试一万次还是错,只会拖长响应时间。重试要有上限和退避。固定间隔重试在对端过载时会变成放大攻击,退避加抖动(jitter)才是正确姿势——multiplier 2已经是指数退避,生产上可以再叠加随机抖动避免一堆请求同时重发。七、两个容易忽略的细节响应体只能读一次。retrieve()的响应流读完即关。调试时如果想打日志又想把 body 反序列化成对象,别在onStatus里读 body 再指望后面还能用,RestClientResponseException已经把响应体缓存进异常了,直接e.getResponseBodyAsString()取。日志别打全量 body。外部接口的响应里可能带手机号、身份信息。用拦截器记录方法、URL(去掉 query 里的敏感参数)、状态码、耗时即可:.requestInterceptor((request,body,execution)-{longstartSystem.nanoTime();try{returnexecution.execute(request,body);}finally{longcostMs(System.nanoTime()-start)/1_000_000;log.info(outbound {} {} took {}ms,request.getMethod(),request.getURI().getPath(),costMs);}})小结RestClient是RestTemplate的同步替代品,链式 API、默认 JDKHttpClient,不需要把项目改成响应式。自定义客户端时注入RestClient.Builder,复用自动配置的消息转换器和可观测性设施,不要用RestClient.create()。超时默认是无限等待,必须显式配,且只能配在ClientHttpRequestFactory上;readTimeout要小于本服务的上游超时。不换 factory 就没有连接池。引入httpclient5用PoolingHttpClientConnectionManager,三个超时(connect / response / connectionRequest)一个都别少。错误处理优先用onStatus按状态码分流;要降级不抛异常就走exchange。重试只对幂等接口、只对可恢复错误,配指数退避加抖动,noRetryFor明确排除业务异常。一句话记忆点:RestClient 的默认值全是「生产不可用」的默认值——超时无限、连接不池化,搬上生产前必须把 request factory 这一段补完。

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

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

免费获取报价 →
↑