1. 为什么到了现在还在用 RestTemplate 做远程调用
先交代一下背景。项目里经常要做服务之间的数据互通,比如订单服务要查用户服务的用户信息,库存服务要从商品服务同步价格,这些场景本质都是“一个服务通过 HTTP 协议去请求另一个服务的接口”。在 Java 生态里,这件事最传统的做法就是 Spring 提供的 RestTemplate。
我知道现在一提到 HTTP 客户端,很多人的第一反应是 WebClient、Feign、OkHttp 这些新东西。Spring 官方也确实在 Spring 5 之后把 WebClient 列为推荐的响应式客户端,但现实是:大量存量项目、金融政企系统、内部中台服务,代码仓库里跑的还是 RestTemplate。原因不复杂——它简单、同步、足够成熟,团队里随便一个后端开发拿到就能写,不需要理解响应式编程的背压和订阅模型。对于内部接口调用这种“请求-响应”模型,RestTemplate 依然是成本最低的解决方案。
这篇文章我不会只给一段“怎么用”的 Demo,而是把我在实际项目中用 RestTemplate 调接口踩过的坑、总结出的套路、以及一些源码层面的理解都写出来。内容定位是:已经会 Spring Boot 基础、但想在实际项目中把远程调用写得更稳的读者。你看完之后,至少能解决这些实际问题:超时为什么经常不生效、POST 请求中文为什么乱码、接口返回的 JSON 怎么转换成泛型对象、第三方接口返回非 200 时怎么拿到错误详情、连接池怎么配才不会把端口耗尽。
我先说一个关键认知:RestTemplate 本身只是一个门面,真正干活的是它内部的 ClientHttpRequestFactory。很多人配了超时时间发现不生效,就是因为只设置了 RestTemplate 的默认属性,却忽略了底层工厂。这个点后面我会详细拆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RestTemplate 的实例化与底层工厂选型
2.1 最基础的 Bean 定义
在 Spring Boot 项目里,最常见写法是直接在配置类里 new 一个 RestTemplate 丢给容器管理:
java复制@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
这段代码能跑,但千万别直接用到生产环境。为什么?因为 new RestTemplate() 默认用的是 SimpleClientHttpRequestFactory,底层是 JDK 自带的 HttpURLConnection。这个实现有两个非常明显的缺陷:
- 没有连接池概念,每次请求都要经历 TCP 三次握手和四次挥手,高并发下性能很难看。
- 对连接超时和读取超时的控制能力很弱,容易出现接口卡住、线程池被占满的情况。
我在一个老项目里见过一台上线三个月没重启的机器,跑着默认配置的 RestTemplate,最终状态是端口被 TIME_WAIT 状态的连接占满,新建请求直接报 Cannot assign requested address。所以说,实例化这一步省不得。
2.2 生产推荐:HttpClient 或 OkHttp 作为底层实现
比较务实的做法,是引入 Apache HttpClient 或 OkHttp 作为 RestTemplate 的底层客户端。我自己的偏好是用 Apache HttpClient,主要原因是它在传统企业项目里口碑稳、配置项丰富、排查问题资料多。
引入依赖:
xml复制<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
然后自定义一个工厂:
java复制@Bean
public RestTemplate restTemplate() {
// 连接池管理,最大连接数 200,单路由最大 50
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(50);
// 连接超时与读取超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(3000)
.setSocketTimeout(10000)
.setConnectionRequestTimeout(3000)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.evictIdleConnections(30, TimeUnit.SECONDS)
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
这里解释几个我踩过坑之后才真正重视的参数:
setConnectTimeout(3000) 表示与服务器建立 TCP 连接的最大等待时间。如果目标 IP 不可达,或者防火墙把端口丢了,这个参数能避免线程傻等几十秒。setSocketTimeout(10000) 表示从服务器读取数据的超时时间,也就是“连上了但迟迟没返回数据”的最大容忍时间。setConnectionRequestTimeout(3000) 是从连接池获取连接的超时时间,如果连接池里没有空闲连接且池子已满,这个参数会兜底,避免线程无限阻塞。
evictExpiredConnections 和 evictIdleConnections 是我强烈建议加上的。默认情况下,连接池里的连接如果长时间不用,服务端可能已经把它关了,但客户端不知道,等下一次请求拿到这条死连接,就会报 Connection reset。定期清理空闲连接能明显减少这类偶发报错。
2.3 为什么超时配置容易“看着配了,实际没用”
这是 RestTemplate 使用中最常见的迷思。很多人只在代码里这样写:
java复制restTemplate.getForObject(url, String.class);
然后以为超时只管 RestTemplate 本身。实际上,如果你用的是 JDK 默认的 SimpleClientHttpRequestFactory,超时配置要这样设置才生效:
java复制SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(10000);
RestTemplate restTemplate = new RestTemplate(factory);
而当你换成 HttpClient 的工厂,超时控制权就转交给了 RequestConfig。如果你仍然调用 factory.setConnectTimeout(),会发现毫无效果。这一点很多人会忽略。我在代码评审时经常看到同事两种配置混着写,表面看起来没问题,但埋下了隐患。
另一个容易踩的点:socketTimeout 不等于“接口必须在 10 秒内返回完整响应”。它是“两次数据包读取之间”的最大间隔。如果接口是流式返回、边生成边写,每个分包间隔都很短,整体响应可能超过 10 秒也不会超时。理解这个语义,能帮你更精准地定位线上偶发超时问题。
3. 必会的核心 API 与参数传递细节
3.1 GET 请求的两种传参姿势
GET 请求最常用的方法是 getForObject 和 getForEntity。两者的区别我用一句话概括:getForObject 直接返回响应体转换后的对象,getForEntity 返回包含状态码、响应头、响应体的完整 ResponseEntity。如果你需要拿到接口返回的 HTTP 状态码或响应头,就必须用 getForEntity。
参数拼接有两种写法。第一种是最传统的 String.format 风格:
java复制String url = "http://user-service/api/user/{id}";
UserDTO user = restTemplate.getForObject(url, UserDTO.class, 1001L);
花括号 {id} 是 URI 模板占位符,RestTemplate 会把可变参数按顺序填入。这种方式推荐的原因只有一个:它能自动处理 URL 中的特殊字符编码,避免手拼字符串时出现非法字符。
第二种是直接拼完整的 query string:
java复制String url = "http://user-service/api/user?userId=" + userId;
UserDTO user = restTemplate.getForObject(url, UserDTO.class);
简单直接,但隐患在参数值里。如果 userId 是中文或者包含 &、?、空格这些字符,直接拼上去极大概率请求失败,或者服务端解析出错误的值。我见过一个真实事故:上游参数值是手机号带 + 号前缀,直接拼接后 + 被解析成了空格,导致业务数据匹配不上。所以只要参数值是动态的,优先用 URI 模板 + 可变参数,或者用 UriComponentsBuilder 手动编码:
java复制UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl)
.queryParam("key", value)
.queryParam("name", "张三");
String url = builder.encode(StandardCharsets.UTF_8).toUriString();
3.2 POST 请求的三种请求体格式
POST 调用最常见的问题不是不会调,而是请求体格式和服务端期望对不上。我用排除法总结一下。
第一种:提交 JSON。大多数后端接口现在都是这种风格,使用 HttpHeaders 指定 Content-Type 为 application/json;charset=UTF-8,请求体一般是对象或 Map。代码长这样:
java复制HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Request-Id", UUID.randomUUID().toString());
Map<String, Object> body = new HashMap<>();
body.put("orderNo", "20250101001");
body.put("amount", 99.5);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<ResultDTO> response =
restTemplate.postForEntity(url, requestEntity, ResultDTO.class);
我反复强调 charset=UTF-8 不是多余动作。某些老旧的中间件或定制版 Tomcat 会默认按 ISO-8859-1 解析请求体,导致中文变成乱码。客户端这边明确了 UTF-8,能规避一大部分这类问题。
第二种:提交表单。服务端接口是 application/x-www-form-urlencoded 风格,比如 OAuth2 的 token 接口、微信支付回调验签接口。这时候请求体不能直接放 Map,要用 LinkedMultiValueMap:
java复制HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("app_id", "xxx");
body.add("app_secret", "yyy");
HttpEntity<LinkedMultiValueMap<String, String>> requestEntity =
new HttpEntity<>(body, headers);
JSONObject result = restTemplate.postForObject(url, requestEntity, JSONObject.class);
注意这里不能用普通 HashMap,否则 Spring 无法把 Map 的键值对编码成 key=value&key2=value2 这种格式。LinkedMultiValueMap 存在的意义就是支持一个 key 对应多个 value,且保持有序。
第三种:提交二进制或文件。比如调用某些供应商的图片上传接口,请求体是 multipart/form-data。需要用到 FileSystemResource 或 ByteArrayResource,并借助 FormHttpMessageConverter。这块使用频率相对低,我这里不展开代码,但提醒一点:RestTemplate 默认的 FormHttpMessageConverter 对 ByteArrayResource 的支持需要额外设置文件名,否则上传后服务端拿不到原始文件名,很多对接文档不会写这一点。
3.3 响应体转换:String、实体、泛型各有坑
如果接口返回的是纯文本或 JSON 字符串,最简单的方法就是直接把响应类型设成 String.class:
java复制String json = restTemplate.getForObject(url, String.class);
拿到字符串之后,再交给 Jackson 或 Fastjson 手动解析。这种写法在接口结果动态性很强、无法确定统一结构时非常实用。
如果接口返回结构相对固定,直接指定目标实体类更优雅:
java复制UserDTO user = restTemplate.getForObject(url, UserDTO.class);
但这里有一个很多人一开始会踩的坑:接口返回的最外层往往不是业务数据本身,而是一个统一包装结构,比如:
json复制{
"code": 0,
"message": "success",
"data": {
"userId": 1001,
"userName": "张三"
}
}
如果你直接把 UserDTO.class 传进去,期望拿到 data 字段,那一定会得到一个“JSON 解析失败”的报错,因为 Jackson 试图把整个包装结构映射到 UserDTO,发现字段对不上。正确做法是先定义一个统一返回体 Result<T>,泛型再用 ParameterizedTypeReference 处理,这部分我放到后面“泛型反序列化”小节详细讲。
4. 错误处理:别再用 try-catch 兜底全局异常
4.1 RestTemplate 默认的错误响应行为
RestTemplate 这一层有个很容易被忽略的设计:只要 HTTP 状态码是 4xx 或 5xx,它默认抛 HttpClientErrorException 或 HttpServerErrorException,最终统一表现为 RestClientException。也就是说,你在代码里写了 restTemplate.getForObject(...),服务端返回 500,你的业务代码会直接抛异常,根本拿不到响应体内容。
很多新手在这个阶段会写这样的代码:
java复制try {
ResultDTO result = restTemplate.postForObject(url, request, ResultDTO.class);
// 业务处理
} catch (Exception e) {
log.error("调用失败", e);
// 降级处理
}
这个写法不能说错,但信息量太少了。一旦线上出现批量失败,日志里只能看到一堆“服务器返回 500”,接口到底返回了什么错误信息、是参数问题还是服务端内部异常,一概不知。
4.2 自定义 ResponseErrorHandler 拿到错误详情
更稳妥的做法是自定义一个 ResponseErrorHandler,在异常抛出之前先读取响应体内容,把服务端真正返回的错误信息记录下来:
java复制public class CustomResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
HttpStatus status = HttpStatus.resolve(response.getRawStatusCode());
return status != null && status.isError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
String body = StreamUtils.copyToString(
response.getBody(), StandardCharsets.UTF_8);
int statusCode = response.getRawStatusCode();
throw new CustomRuntimeException(
String.format("远程调用返回异常, status=%d, body=%s", statusCode, body));
}
}
然后设置到 RestTemplate:
java复制restTemplate.setErrorHandler(new CustomResponseErrorHandler());
这样异常信息里就带上了具体状态码和响应体,排查问题效率高很多。还有一个实用技巧:如果你的接口虽然返回 200,但业务里 code 字段非 0 表示失败,这种情况 ResponseErrorHandler 管不了,你需要在拿到 ResultDTO 之后手动判断 code。我习惯封装一个私有方法统一处理:
java复制private <T> T execute(Supplier<T> supplier) {
ResultDTO<T> result = supplier.get();
if (result == null || !result.isSuccess()) {
throw new BizException(result == null ? "未知错误" : result.getMessage());
}
return result.getData();
}
4.3 超时与重试:区分“幂等”和“非幂等”
超时异常的根因是什么?按我的经验,大部分不是网络故障,而是服务端处理慢、线程池排队、或者数据库锁等待。这类问题的排查重点应该在服务端日志和监控指标上,而不是一味的加重试。
但如果确实要加重试,必须区分接口是否幂等。查询、删除这类操作,重复执行结果一致,放心重试。下单、转账、支付回调这类写操作,重试可能会导致重复扣款或重复下单,需要额外引入幂等键(比如请求头里的 X-Request-Id)或者干脆不重试。
我推荐做一个简单但实用的重试封装:基于 Spring Retry 或手动循环,重试 2 次,间隔 200ms,只在超时异常或特定状态码时触发:
java复制int retryTimes = 0;
boolean success = false;
String result = null;
while (retryTimes < 3 && !success) {
try {
result = restTemplate.postForObject(url, request, String.class);
success = true;
} catch (ResourceAccessException e) {
retryTimes++;
if (retryTimes >= 3) {
throw e;
}
Thread.sleep(200L * retryTimes);
}
}
ResourceAccessException 是 RestTemplate 对 I/O 异常、超时异常的包装类别。捕获它而不是捕获 RestClientException,精度更高,不会把业务状态码错误也吞进去重试。
5. 进阶实践:泛型反序列化、拦截器与 HTTPS 坑
5.1 泛型擦除问题:ParameterizedTypeReference 的正确用法
前面提到统一返回体 Result<T> 的解析问题。如果你直接写:
java复制Result<UserDTO> result = restTemplate.getForObject(url, Result.class);
那得到的结果里,data 字段通常会被解析成一个 LinkedHashMap,而不是 UserDTO。发生这个现象的原因是 Java 泛型擦除:运行时的字节码里找不到 Result<UserDTO> 中的 UserDTO 类型信息,Jackson 只能按照 Object 处理,最终落到 Map 结构。
解决办法是使用 ParameterizedTypeReference:
java复制ResponseEntity<Result<UserDTO>> response = restTemplate.exchange(
url, HttpMethod.GET, null,
new ParameterizedTypeReference<Result<UserDTO>>() {});
Result<UserDTO> result = response.getBody();
原理并不玄妙:ParameterizedTypeReference 在创建匿名内部类实例时,通过反射捕获了父类的泛型参数类型,把这个类型信息传递给了 Jackson 的 TypeFactory,从而让反序列化能按完整类型进行。所以写这个匿名内部类的时候,{} 不能省略,省略了就等于没传递类型信息。
5.2 拦截器:统一加请求头、日志、签名
ClientHttpRequestInterceptor 是 RestTemplate 非常实用的扩展点。你可以把它想象成一个“中间人”,在请求发出前和响应返回后各有一个切入机会。
最典型的应用是统一打印请求日志,方便排查问题。我项目中就有一个:
java复制public class LoggingInterceptor implements ClientHttpRequestInterceptor {
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
START_TIME.set(System.currentTimeMillis());
log.info("请求方法: {}, 请求地址: {}, 请求体: {}",
request.getMethod(), request.getURI(), new String(body, StandardCharsets.UTF_8));
ClientHttpResponse response = execution.execute(request, body);
long cost = System.currentTimeMillis() - START_TIME.get();
log.info("响应状态: {}, 耗时: {}ms", response.getStatusCode().value(), cost);
return response;
}
}
设置方式:
java复制RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor()));
这个拦截器有几个需要注意的细节。第一,execution.execute(request, body) 执行完之后,如果你尝试在这里读取 response.getBody() 来记录响应内容,等于把响应流消费了一遍,后续业务代码再读就是空的。一定要复制流或者使用其他的包装方式,否则会坑到后面的人。所以我上面只记录了状态码,避免踩这个坑。第二,拦截器里抛出的异常会继续向外传播,所以日志里要带上足够上下文,至少要有请求地址和唯一追踪号。
另一个场景是统一签名。如果对接的外部平台要求每次请求带签名(比如按时间戳 + 密钥 + 请求体生成 MD5),用拦截器可以在不改业务代码的前提下统一处理,新接口接入时非常省事。
5.3 HTTPS 证书信任问题
内网环境经常遇到自签名 HTTPS 证书的情况。RestTemplate 默认是严格校验证书链的,直接请求大概率报 PKIX path building failed。很多人的第一反应是写一个信任所有证书的 HostnameVerifier,把校验整个关掉。这个做法在低安全等级的内网临时调试时可以接受,但生产环境我强烈不建议。
更合理的方案有两类:
- 如果服务方提供了证书文件,把证书导入本地信任库,启动参数里指定信任库,保持 JVM 原有的严谨校验。
- 如果实在没有证书文件、又必须绕过校验,至少用代码单独构建一个信任所有证书的
HttpClient,并且只用于对应场景,不要全局替换。
为什么强调“只用于对应场景”?因为全局信任所有证书等于给攻击者开了大门。在企业内部服务互调中,一旦某个节点被拿下,所有走这个客户端的请求都能被中间人截获和篡改,风险极大。
5.4 连接池监控与限额评估
连接池的参数不是随便拍脑袋定的。setMaxTotal(200) 这个数字,应当根据上游接口的 QPS 和单次请求耗时来推算。一个粗略的估算公式是:
code复制在线连接数 ≈ QPS × 平均响应时间(秒)
比如接口 QPS 是 50,平均响应时间 0.5 秒,那么大约需要 25 条连接。为了方便理解,你可以把连接池想象成餐厅的座位数:同一时刻只能容纳这么多人就餐,人多了就得在门口排队(connectionRequestTimeout)。座位设置太多浪费资源,设置太少则排队时间过长。
我看过很多团队直接把连接池 maxTotal 改成几百上千,其实没必要。连接数的上限还受目标服务端最大连接数限制,两边是水桶原理。我曾经在一次压测中发现,把连接池从 200 调到 50,性能没有明显下降,反而因为减少了线程竞争和上下文切换,接口 P99 延迟略有下降。所以正确流程是:先估算,再压测,根据监控数据调整。
6. 我把 RestTemplate 大规模接入生产后踩过的坑
6.1 中文乱码问题:不是所有环境都默认 UTF-8
这个问题说起来简单,但线上踩一次就很难忘。现象是:请求头里明明设置了 Content-Type: application/json;charset=UTF-8,但服务端收到的中文依然乱码。
排查链路是这样的:先看服务端日志,乱码字符如果是 ??? 或者类似 æµè¯ 的表现,说明是编码转换问题,大概率出在链路某一环没有按 UTF-8 处理。RestTemplate 默认的 StringHttpMessageConverter 在 Spring Boot 2.x 之后默认编码是 UTF-8,但在 Spring Boot 1.x 或某些定制版本中默认是 ISO-8859-1。这意味着即使你设置了请求头,转换器可能在序列化时用了错误的编码把字符串转成了字节流。
稳妥做法是显式声明全局替换转换器:
java复制RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().removeIf(converter -> converter instanceof StringHttpMessageConverter);
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
这段代码的意思是:把默认的 StringHttpMessageConverter 删掉,换成显式指定 UTF-8 的实例,并放在列表首位,让它在消息转换时优先被匹配。凡是遇到 String 类型的请求体或响应体,都会走这个转换器。
6.2 URL 中特殊字符导致的签名失败
有一次对接外部平台的支付接口,客户端生成的签名串包含 + 和 /,直接拼到 URL 里。外部平台验签一直不通过,排查了大半天,最后发现是 URL 编码问题:+ 在 query string 中会被解析成空格,服务端拿到的参数值和签名时的不一致,验签自然失败。
解决办法是在拼接 URL 之前对参数值做 URLEncoder.encode(value, StandardCharsets.UTF_8.toString()),但要注意:URLEncoder 会把空格编码成 +,这本身又会引入上面那个问题,所以还需要把 + 替换成 %20。更省心的做法是用 Spring 自带的 UriComponentsBuilder,它的 encode() 方法会自动处理这些特殊字符规则。
6.3 响应体过大导致的内存问题
有些查询接口一次性返回几万条数据,如果把响应体直接塞进 String 再解析,内存消耗会非常可观。RestTemplate 没有内置流式解析的能力,默认行为是把整个响应体读入内存。面对大响应时,有两个思路:
- 尽量让上游接口支持分页或增量查询,从源头控制数据量。
- 如果上游接口不支持分页,考虑用
RestTemplate.execute()结合ResponseExtractor边读边处理,避免整个文件一次性载入内存。
后者代码复杂度会上升,但碰到强需求时值得。
6.4 线程池与异常日志的配合
RestTemplate 调用通常发生在 Tomcat 的业务线程中。如果服务端响应很慢,业务线程会被占用很久,并发一高,Tomcat 线程池也会被打满。所以在日志中记录耗时的意义不仅是排查接口问题,更重要的是帮你判断“到底是谁慢”。我通常会在调用结束后打印精确到毫秒的耗时,如果某个接口耗时超过了设定阈值,就会自动打 WARN 日志并附带上请求参数摘要,这样监控系统能第一时间发现异常趋势。
6.5 服务降级与熔断的取舍
最后聊一个设计层面的问题。远程调用最大的不确定性是“你控制不了对方的稳定性”。团队在引入 RestTemplate 做核心链路的远程调用后,必须想清楚一个问题:对方接口挂了,我的系统怎么办?
这里的取舍我认为分三层:
- 第一层:接口可降级。比如查用户信息失败时,可以降级成一个空对象或缓存中的旧数据,保证主流程不中断。
- 第二层:接口可缓存。热点数据查询失败时,走本地缓存兜底,同时异步刷新缓存。
- 第三层:快速失败。与其让线程全部阻塞在超时等待上,不如在监控指标达到阈值时直接放行降级逻辑。Spring Cloud 体系中可以用 Sentinel 或 Resilience4j 实现,但底层调用依然是 RestTemplate 的话,超时参数和连接池配置依然是最基础的保障。
我个人在项目里更倾向于把超时设短一点(连接 3 秒、读取 5 到 10 秒),再配合一两次精准重试,而不是把超时时间设得很长来“等”对方返回。远程调用本来就是不可靠的,设计目标应该是“快速失败、及时降级”,而不是“无限等待、死扛到底”。
写在最后的经验之谈
RestTemplate 本身不是一个复杂的技术,但把它用好、用稳,背后需要的思考远比“调一个接口”多:底层连接池怎么选、超时参数怎么设、错误怎么处理、泛型解析怎么做、踩过的编码坑怎么绕,这些经验如果没有人提醒,基本都要靠线上故障换回来。这篇文章里写的都是我在真实项目中验证过的方案。如果你正在接手一个老项目,发现里面有几十处 new RestTemplate() 的散装写法,建议先统一收敛到一个配置类中,再逐步按上面的思路改造,而不是一次性全量替换,以免回归测试范围失控。
最后分享一个我的操作习惯:每接入一个新上游接口,我会先写一个包含“打印请求参数、响应状态码、响应体、耗时”的测试用例,跑通后再写业务代码。看似多花了几分钟,但后面排查问题省下的时间远超这几倍。远程调用这事,出问题是常态,不出问题才是运气,做好准备总比出事儿再慌要强。
