做微服务的人几乎都绕不开一个问题服务拆了A服务怎么调B服务在 Spring Cloud 的体系里这个问题的解法有好几种RestTemplate 就是其中最直接、也最适合练手的一种。它本身是 Spring 框架老牌的 HTTP 客户端几乎零学习成本加上LoadBalanced注解之后又能配合注册中心实现服务发现和负载均衡。在真正上手 Feign 之前先把 RestTemplate 的原理啃透对你理解整个微服务调用链路会非常有帮助。这篇文章我打算从方案选型讲到实战细节再穿插几个我在真实项目里踩过的坑希望对正在学 Spring Cloud 的朋友以及那些用了很久 Feign 却不太清楚底层机制的人都有点参考价值。1. 为什么服务间调用从 RestTemplate 讲起1.1 拆了服务通信成本就来了微服务架构下过去单体应用里的一个方法调用变成了跨进程的远程调用。这个转变带来的问题比想象中多得多至少摆在你面前有四个绕不开的难题服务地址怎么找、多个实例怎么选、超时了怎么办、服务挂了怎么反馈。早期没有注册中心的时候服务提供者会把地址写死在配置文件里消费者也是通过配置文件读取。这种方式在小规模下能跑但实例数量一多就崩了。运维要手动维护一份 IP 清单某台机器挂了还要人工改配置再重启整个过程非常痛苦。Spring Cloud 给出的答案是注册中心就是服务地址的“电话本”。服务提供者启动时把自己的地址告诉注册中心消费者每次调用前向注册中心查询地址这就是服务发现。而 RestTemplate 就是那个拿起电话本打电话的动作。讲到这顺便说一句HTTP 协议不是什么高性能方案比起 RPC 框架在传输效率和二进制协议上有差距但它最大的优点就是通用不绑定语言、不绑定平台、调试方便拿个 curl 就能看到完整报文。很多团队内部服务调用也走 HTTP一来省去引入额外框架的成本二来排障的时候能直接抓包看数据。RestTemplate 在 Spring Cloud 里能成为入门首选靠的也是这种“通用”。1.2 RestTemplate、Feign、WebClient 怎么选不少新手会直接问我现在都推荐用 Feign还有人在讲 WebClient 异步为什么还要学 RestTemplate我的看法很固执你如果刚接触微服务一定要先从 RestTemplate 做起。原因很实际——Feign 本质上是对 HTTP 调用的抽象封装底层最终还是走相同的请求执行链路你不理解 RestTemplate 是怎么拼 URL、怎么序列化、怎么处理错误的直接上 Feign 很容易遭遇“抽象泄漏”。比如 Feign 明明配了超时时间但拦截器、解码器配置不对最后还是踩坑而且你连排障方向都没有。工具调用方式学习成本适用场景RestTemplate手动拼接 URL用模板方法低小规模系统、学习阶段、需要精细控制 HTTP 细节Feign声明式接口加注解像调本地方法中业务服务间广泛调用、团队规范统一的场景WebClient函数式、响应式非阻塞高高并发、需要异步化的场景RestTemplate 的另一个优势是灵活性高。Feign 把请求方式封装到接口注解里你能干预的入口是固定的RestTemplate 则不同URL、Header、请求体、拦截器、错误处理器都是裸露的你可以在它上面做很多定制化改造。比如说你想统一往请求头里塞一个签名参数或者所有请求都自动带上 traceId用 RestTemplate 的拦截器就能非常优雅地解决不需要给每个接口单独写逻辑。1.3 RestTemplate 不是什么“玩具”很多人觉得 RestTemplate 是老古董其实它在 Spring 官方体系中长期都是主推的 Rest 客户端。它的设计思路是“模板方法”把 HTTP 请求里不变的部分也就是建立连接、发送请求、处理响应消息转换统统封装好把变化的部分比如 URL、参数、返回类型留给调用者去填。这和你平时用的 JdbcTemplate 是一个道理。在使用 RestTemplate 调用接口时底层实际是通过HttpMessageConverter把 Java 对象转成 JSON 或 XML 字节流反过来再把响应的字节流转成 Java 对象。默认情况下Jackson 负责 JSON 的序列化和反序列化这就解释了为什么你传一个对象进去对方收到的是一个标准的 JSON 字符串对方返回 JSON 时你传一个类进去它又能给你转成具体的对象。把 RestTemplate 当成一个“带着工具包的对象”来理解后面遇到很多诡异问题比如返回类型拿到的是 LinkedHashMap、日期格式不对、泛型擦除就都能找到依据了。2. 搭一个能跑的调用链路2.1 注册中心的选择和搭建做服务间调用前提是能按服务名找到地址。现在主流的注册中心无非是 Nacos 和 Eureka。Eureka 很老牌Spring Cloud 早期版本大量使用但由于后来进入维护模式新项目我更建议选 Nacos。Nacos 集注册中心和配置中心于一身控制台一看就能知道哪些服务在线比 Eureka 的运维体验好不少。为了演示方便就用 Nacos 为例。启动 Nacos 最简单的方式是下载安装包解压后在 bin 目录执行启停脚本单机模式不需要额外依赖启动成功后访问控制台确认服务正常。接下来在服务提供者和消费者的工程里加上对应依赖注意版本要和你用的 Spring Cloud 版本匹配。我建议用 Spring Boot 2.7.x 配合 Spring Cloud 2021.0.x或者 Spring Boot 3.x 配合 Spring Cloud 2022.x具体版本对照表在 Spring 官网能查到不要自己乱配。版本错的症状往往很隐蔽比如注解扫描不到、配置不生效查起来很头疼。2.2 服务提供者让别人能调到你先写服务提供者。核心工作就两步暴露一个 HTTP 接口、把自己的地址注册到注册中心。典型的控制类接口长这样RestController RequestMapping(/user) public class UserController { GetMapping(/{id}) public Result getUser(PathVariable Long id) { return Result.ok(new User(id, name- id)); } PostMapping(/save) public Result save(RequestBody User user) { return Result.ok(user); } }注册到 Nacos 只需要在 application.yml 里配置注册中心地址和应用名spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 server: port: 8081这里的spring.application.name非常重要消费者调用你的时候用的就是它不能随便改也不能和别的服务重名。这个配置是我见过被忽略最多的很多团队一个服务启动不起来查半天发现是应用名带了下划线或者大小写不一致注册中心里显示的名字和代码里调用的名字对不上。2.3 消费者注入 RestTemplate消费者这边的准备工作更简单创建一个配置类把 RestTemplate 声明成 BeanConfiguration public class RestConfig { Bean LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }不要在 Controller 里自己new RestTemplate()这是很多新手的第一个坑。自己 new 出来的对象不会被 Spring 容器管理也就不会具备负载均衡、拦截器这些能力。你后面辛苦配好的超时时间、拦截器、错误处理器全部失效而且问题非常隐蔽。2.4 直连调用与服务名调用的差异先看不走注册中心时怎么调。假设提供者地址固定消费者可以直接写死String url http://localhost:8081/user/1; Result result restTemplate.getForObject(url, Result.class);这种写法在小单体时代没问题但一旦 user-service 扩容到三台实例地址就变成三个你必须在代码里做选择负载均衡逻辑就得自己写非常痛苦。机器上下线、端口调整这类事也会让你疲于奔命。注册中心加LoadBalanced之后调用代码变成String url http://user-service/user/1; Result result restTemplate.getForObject(url, Result.class);注意到区别了吗域名部分从 IP 和端口变成了服务名。RestTemplate 根据应用名从注册中心拉实例列表再按负载均衡策略选一个实际地址发起请求。这就是 RestTemplate 在微服务场景下最核心的用法。你写的 URL 越“虚拟”系统扩展的弹性就越大。3. LoadBalanced 的原理与负载均衡策略3.1 一个注解为什么有这么大的魔力先说结论LoadBalanced本身不干活它只是一个标记。真正起作用的是客户端负载均衡器。Spring Cloud 启动时会扫描所有加了LoadBalanced的 RestTemplate Bean给它额外配置一个拦截器。这个拦截器拦截请求拿到 URL 里的服务名向注册中心查询实例列表然后通过负载均衡算法选出一个实例把 URL 改写成真实的http://ip:port/...再继续执行后续请求。你可以把整个过程类比成外卖平台你下单时填的是“某餐厅”这个门店品牌平台根据这个品牌下的多个分店以及各分店的当前排队情况帮你选一个具体门店去送单。你支付时并不关心送单地址送单地址是平台帮你决定的。RestTemplate 借用了服务实例选择器做了一个类似的中转。在 Spring Cloud 旧版本里这个客户端负载均衡器底层用的是 Ribbon新版本的 Spring Cloud 已经将其替换为 Spring Cloud LoadBalancer。虽然类名变了但整体的“拦截器加服务发现加选择器”架构没有变理解这一层很多配置项就能对上号了。3.2 负载均衡策略怎么换默认的负载均衡策略是轮询也就是轮流把请求分给各个实例。实际项目中可能会遇到权重不均衡、需要优先本机房实例等需求。在 Spring Cloud LoadBalancer 下可以通过配置切换策略。比如在 application.yml 里spring: cloud: loadbalancer: ribbon: enabled: false上面这个是关闭旧版 Ribbon 的开关。然后定义自己的负载均衡配置类替换默认策略为随机或其他方式。实现层面上要么定制ServiceInstanceListSupplier要么自定义负载均衡器实现。我以前在一个项目里把默认策略换成了“优先选择响应时间最短的实例”。做法是在配置类里注册一个 Supplier给它包一层健康检查筛选逻辑再通过缓存 Supplier 做结果缓存。这里有个提醒负载均衡策略的调整一定要先压测不要想当然。某些策略对网络抖动特别敏感换完之后反而更差了尤其是那种基于实时统计的策略在流量波动大的场景下很不稳定。3.3 新手容易翻车的三个地方第一LoadBalanced只能作用于被 Spring 管理的 RestTemplate Bean。如果你在代码里又 new 了一个那它肯定不会有负载均衡能力日志里会出现“UnknownHostException: xxx-service”。第二多个 RestTemplate 的场景。有的项目既需要直连第三方接口又需要内部服务调用于是创建了两个 RestTemplate。一个加了LoadBalanced另一个没加这没问题但使用时一定要分清楚。别把内部服务名 URL 用直连的 RestTemplate 去调那会直接抛 UnknownHostException也别把外网 IP 地址塞给加了负载均衡的 RestTemplate它会尝试从注册中心解析这个“服务名”结果是解析不到。第三注册中心里没有实例。也就是服务名能解析但实例列表是空的这时候会报 “No instances available for user-service”。这个问题很常见尤其是服务提供者刚启动还没完成注册或者注册中心里服务已经被下线后面我会专门讲排查顺序。4. 实际编码中的细节与参数控制4.1 GET、POST、参数传递怎么写最省心RestTemplate 提供了一组模板方法最常用的有getForObject、getForEntity、postForObject、postForEntity、exchange。我的建议是临时调试用getForObject和postForObject就行一旦代码进入正式业务链路统一用exchange方法因为它能完整控制请求头、请求体、响应状态和泛型类型扩展性最好以后加需求不用改调用方式。GET 带参数最简单的做法是用UriComponentsBuilder构造带查询参数的 URLString url http://user-service/user/query; URI uri UriComponentsBuilder.fromUriString(url) .queryParam(city, demo-city) .queryParam(page, 1) .build() .toUri(); Result result restTemplate.getForObject(uri, Result.class);POST 传对象直接传 Java 对象即可RestTemplate 会自动序列化成 JSONUser user new User(); user.setName(demo-user); Result result restTemplate.postForObject(http://user-service/user/save, user, Result.class);但要注意如果对方接口声明的是RequestBody User user那你传一个对象过去就是 JSON如果对方是用表单接收参数的那就得用MultiValueMap并设置请求头为application/x-www-form-urlencoded。这两种写法很容易混。我见过很多人 post 对象给一个表单接口对方所有字段都是 null排查半天才发现是 Content-Type 不对报文里明明是一坨 JSON对方却按表单去解析。4.2 超时时间默认值只会让你怀疑人生RestTemplate 默认使用简单请求工厂生产环境我强烈建议换成 Apache HttpClient 的实现后者的连接池、超时控制和性能都要好一截。超时配置不能只设一个至少要分两个维度连接超时 connectTimeout 和读取超时 readTimeout。简单理解连接超时是建立 TCP 连接等了多久读取超时是连接建立后对方多久没返回数据就放弃。还有一个 socketTimeout大多数场景下跟随读取超时配置即可。配置示例Bean LoadBalanced public RestTemplate restTemplate() { HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(5000); return new RestTemplate(factory); }这里的经验是connectTimeout 别超过 3 秒readTimeout 根据业务量级来定普通的查询接口 5 秒左右比较稳妥。设太长会导致线程堆积高并发下把 Tomcat 线程池耗尽设太短会导致偶尔的慢调用被误杀。你不能拍脑袋定一个数可以先压测统计接口耗时分布看 p99 是多少再往上加一点余量。4.3 自定义 Header、统一拦截器与链路信息透传微服务中经常要往请求头里塞东西比如认证 token、traceId。如果用getForObject它没有入参给你设置 header这时候就要用exchange。但更推荐用拦截器的方案定义ClientHttpRequestInterceptor给所有通过该 RestTemplate 发出去的请求统一加 header。public class CustomHeaderInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().set(X-Trace-Id, TraceIdUtil.get()); request.getHeaders().set(Authorization, Bearer tokenProvider.getToken()); return execution.execute(request, body); } }然后在配置类里restTemplate.getInterceptors().add(new CustomHeaderInterceptor());这样做的好处是业务代码里不需要处理任何 header 逻辑新增一个调用方服务也不会漏加公共请求头。做链路追踪的朋友应该知道这个有多重要微服务一旦拆开没有 traceId你根本没法把一次用户请求的日志串起来排障全靠翻文件碰运气。4.4 错误响应别等到解析时才懵RestTemplate 默认的行为是如果响应状态码是 4xx 或 5xx它会在内部错误处理逻辑里抛出HttpClientErrorException或HttpServerErrorException。如果你没有捕获代码会直接抛异常中断。有些人会写这样的代码ResponseEntityResult response restTemplate.getForEntity(url, Result.class); if (response.getStatusCode().is2xxSuccessful()) { // ... }如果服务端返回 500这段代码根本走不到 if 判断异常在getForEntity内部就抛出来了。所以要么 try-catch 捕获异常拿错误信息要么自定义ResponseErrorHandler把错误响应体也解析出来再抛出统一业务异常。我的习惯是定义一个全局配置统一错误处理器不放过错误响应体里的提示消息这样排查问题时能直接看到对方服务返回的具体提示而不是只有一行笼统的 “500 Internal Server Error”。5. 常见问题与排查技巧实录5.1 “No instances available”的排查顺序这是我在各种技术交流群里被问得最多的一个问题。报错原文类似java.lang.IllegalStateException: No instances available for user-service原因其实很单一你调用的服务名在注册中心里找不到存活的实例。排查顺序建议固定为到注册中心控制台确认 user-service 是否存在以及在线实例数量。如果是 Nacos在“服务管理”的“服务列表”里查看。如果服务列表有实例但消费者还是报找不到实例检查消费者配置的注册中心地址对不对是不是连到了另一套环境的注册中心。环境隔离问题在微服务里非常常见dev 环境调用到 prod 注册中心或者反过来都是事故现场。检查服务提供者的心跳配置。服务实际还在跑但注册中心认为它下线了多半是心跳发送间隔和过期时间配得不合理。Nacos 默认的心跳行为偶尔会和你自定义的配置冲突需要仔细看控制台的过期时间提示。最后检查你的调用代码服务名拼写是否和spring.application.name完全一致大小写、下划线都必须一模一样。服务名带横线或下划线时特别容易写错。5.2 动不动就 Read Timeout到底是谁慢读超时出现的时候不要第一时间怀疑网络。网络闪断不是没有但大部分情况是服务端真的慢。我在一个项目里遇到过A 服务调用 B 服务查询列表B 服务又去查数据库数据库里有慢 SQL导致接口耗时经常超过 3 秒而 A 的读取超时只给了 2 秒于是高峰期集中报错业务方以为是 A 服务挂了。排查方法先看 B 服务的接口日志记录单次请求耗时再用压测工具模拟调用看耗时分布。如果确认是服务端慢那就优化慢 SQL 或做异步化调大读取超时只是兜底手段不是治病良方。另外如果你在 RestTemplate 层加了重试逻辑一定要谨慎。重试会放大服务端压力尤其在服务端本来就慢的情况下重试等于火上浇油。而且重试前要确认这个请求是幂等的非幂等接口重试可能导致重复下单、重复扣款。这块要单独写清楚哪些接口能重试哪些绝对不能。5.3 返回结果反序列化失败一个非常典型的坑是你定义的返回类型是ResultUser但拿到的却是一个LinkedHashMap。原因要归到 Java 泛型擦除。getForObject(url, Result.class)这句代码里Result.class是一个Class对象它只知道自己本身是 Result 类型并不知道 Result 内部的泛型参数是什么。响应转换时Jackson 反序列化外部 Result 很容易但内部泛型没有具体类型信息只能转成LinkedHashMap。你后面如果强转成User就会抛ClassCastException或者干脆编译期看不出来、运行期才炸。解决办法是ParameterizedTypeReferenceParameterizedTypeReferenceResultUser typeRef new ParameterizedTypeReferenceResultUser() {}; ResponseEntityResultUser response restTemplate.exchange(url, HttpMethod.GET, null, typeRef);用这种写法Jackson 就能拿到完整泛型信息反序列化出真正的 User 对象。这个坑在 Feign 里同样存在属于 Spring 生态里序列化机制的通病值得专门记一笔。5.4 负载均衡没生效怎么办负载均衡没生效的最典型场景是你成功调用了服务但请求每次都打到同一台实例另外几台完全空闲。查问题按这个顺序来。第一步确认 RestTemplate 是否加了LoadBalanced。没加这个注解的话URL 写服务名直接报错而不是负载不均衡。既然能调通那大概率是加了。第二步看策略。默认轮询在实例都健康的情况下不会出现持续命中一台除非实例列表里实际只有一台。去注册中心控制台确认实例数量别只看配置文件里的服务端列表。第三步考虑本地缓存问题。Spring Cloud LoadBalancer 默认会缓存实例列表如果实例刚上线或刚下线缓存没有及时刷新就会出现转发不到新实例的情况。可以把缓存刷新间隔调小或者在发布流程里增加一个“预热”动作先把流量放一小部分进来观察没问题再全量。还有一个隐藏坑在区域优先策略上。某些配置下它会优先选择同区域的实例跨区域实例不到万不得已不会被选到。这本来是好设计但如果你部署架构没有按区域划分或者区域的元数据字段写得不一致实例一样会被“排挤”到无人问津。5.5 快速排查速查表现象可能原因动作UnknownHostException: service-name没加 LoadBalanced / 服务名拼错检查注解和名称No instances available服务未注册 / 注册中心地址不对 / 实例下线查看控制台实例列表Connection refused提供者没启动 / 端口不对 / 实例刚启动未完成注册确认启动状态Read timeout服务端慢 / 数据库慢 SQL / 线程池打满查服务端耗时拿到 LinkedHashMapJava 泛型擦除用 ParameterizedTypeReferenceContent-Type 不匹配接收方是表单却传 JSON检查实际报文格式所有请求打到一台实例实例列表缓存 / 区域策略 / 实例确实只有一台查看实例列表和策略配置6. 我的一些实践心得做这个专栏的时候我复盘了自己最初学 Spring Cloud 的过程发现很多人卡在 RestTemplate 这一关不是因为代码不会写而是不理解“服务名解析”这个中间环节。一旦想通了LoadBalanced只是在帮你完成“服务名到真实地址”的映射很多问题就迎刃而解。比如为什么 URL 要写服务名而不是 IP为什么不同的 RestTemplate Bean 会表现出完全不同的行为为什么负载均衡的生效条件这么苛刻这些疑问都有了统一的答案。我个人的建议是用 RestTemplate 做第一版服务间调用一定要手动写一个完整的小例子把服务提供者、消费者、注册中心整个链路跑通再逐步加拦截器、超时、错误处理。等你对这个过程有了肌肉记忆再去用 Feign 感受“声明式调用的爽”也不迟。别一上来就从 Feign 学起遇到问题时你会完全摸不着头脑。最后再分享一个小技巧调试 RestTemplate 的时候不要一上来就打断点建议先开 debug 日志观察一下实际请求的 URL 最终被改写成什么样子。很多时候 URL 改写这一层已经暴露了问题比如实例 IP 选错了、端口没拼对。你也可以用接口调试工具直接调服务提供者先确认提供者本身没问题再回头怀疑 RestTemplate 的配置这样一层层剥开排查效率会高很多。大家如果在用 RestTemplate 的过程中遇到什么奇怪的报错欢迎带着 Spring Cloud 版本、注册中心版本和服务端代码片段来交流我后面也会在专栏里对比着讲讲 Feign 的玩法。