ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务通信实战:REST Template与OpenFeign选型、配置与踩坑指南

微服务通信实战:REST Template与OpenFeign选型、配置与踩坑指南 做微服务如果只有一个绕不开的话题那一定是服务间通信。把单体拆成多个 SpringBoot 应用之后原本一行代码就能完成的内部方法调用变成了一次跨越进程、跨越网络的远程调用。这篇是微服务系列的第十二篇我把这两条最常用的路——REST Template 和 OpenFeign——从选型到实战一路讲透顺便聊聊我这些年在这两个方案上踩过的坑。如果你是刚接触微服务的同学或者团队正在讨论服务间调用到底用哪套方案这篇文章可以直接给你一个现成的结论和一套能跑通的代码。我会尽量不堆官话把那些“网上教程不会写但实际一定会遇到”的细节都抖出来。1. 为什么微服务通信需要先把方案想清楚1.1 服务拆分后调用方式发生了根本变化先看一个最常见的业务场景订单服务在下单时需要查用户信息。在单体架构里这就是一次userService.getById()的本地方法调用不涉及网络、不涉及序列化、也不涉及服务地址。但一旦把用户模块拆成独立的用户微服务订单服务就必须通过网络去拿数据。微服务通信的主流方案大致有三类基于 HTTP 的 REST 调用、基于自定义协议的 RPC 调用比如 Dubbo、gRPC、以及通过消息队列做的异步调用。RPC 性能好但通常对技术栈有要求比如 Dubbo 偏向 Java 生态gRPC 虽然跨语言但引入了 IDL 和额外运行时。消息队列则适合“发出去就不管”的异步场景不适合要求实时返回结果的同步查询。所以对大多数团队来说最轻量、最容易上手的方案就是 HTTP REST。REST Template 和 OpenFeign本质上都是 Spring 生态下对 HTTP 调用的封装只是封装层次和用法完全不同。1.2 REST Template 与 OpenFeign 的本质差异REST Template 是 Spring 很早就提供的同步阻塞式 HTTP 客户端它像是一个“万能工具箱”GET、POST、PUT、DELETE或者更复杂的exchange泛型请求全部可以用代码手动拼接。你要自己写 URL、自己塞参数、自己处理响应体。OpenFeign 则是声明式客户端它的思路走的是另一个极端你只需要定义一个接口在接口方法上标注GetMapping、PostMapping这类注解OpenFeign 会在启动时自动生成代理实现类。调用接口方法就是在发起 HTTP 请求。我用一张表把关键差异列出来对比维度REST TemplateOpenFeign代码风格过程式调用细节自己写声明式定义接口即可URL 维护业务代码里拼接分散在各处集中在 Feign 接口里负载均衡支持需手动整合 LoadBalancer原生支持配置即可可读性一般代码多时容易乱高接口即契约上手成本低几行代码就能跑稍高要理解代理机制适合场景第三方 API、非微服务体系微服务内部调用从表里能明显看出来OpenFeign 更适合用在体系内的服务间调用REST Template 更适合用在需要灵活拼接请求的外部场景。但这不意味着两者只能二选一后面我会专门讲怎么让它们共存。1.3 选型时真正要考虑的因素我在实际项目里的判断标准有三条。第一调用方和被调用方是否都在微服务体系内。如果都在优先 OpenFeign因为你可以直接把注册中心里的服务名写到FeignClient(namexxx)里OpenFeign 会自动结合负载均衡器找到可用实例不用关心 IP 和端口。REST Template 要手动接入注册中心才能做到同样的事麻烦不少。第二接口数量。如果只有一两个调用点用 REST Template 没什么问题。但如果一个服务要调用十几个下游服务的几十个接口REST Template 的代码会迅速膨胀每个调用点都要重复写 URL、请求头、超时时间。这时候 OpenFeign 的“接口即契约”优势就非常明显。第三要不要做降级。OpenFeign 原生支持 fallback 降级策略在依赖服务故障时可以快速返回兜底结果。REST Template 要实现同样的效果得自己在调用处包一层 try-catch 或写 AOP明显更费劲。2. REST Template 实战从 Bean 配置到拦截器定制2.1 最小可用配置为什么不要到处 new RestTemplate()在 SpringBoot 项目里一个最小可用的 REST Template 只需要三步引入spring-boot-starter-web依赖、声明一个 Bean、然后在需要的地方Autowired注入。注意这里强烈建议把 RestTemplate 声明成 Spring 单例 Bean而不是在每个类里new RestTemplate()。每次 new 都会重新创建底层连接资源在高并发下很容易造成连接数膨胀和资源浪费而且后续想统一加拦截器、换超时配置都没法下手。Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }这个配置在项目初期够用了但它底层用的是 JDK 自带的HttpURLConnection没有连接池性能一般。如果调用量上来了我一般会换成 Apache HttpClient 或者 OKHttp 作为底层实现这样能复用连接、减少握手开销。2.2 换成带连接池的底层客户端先加上依赖dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId /dependency然后创建一个带连接池的RestTemplateBean public RestTemplate restTemplate() { // 连接池管理器 PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 每个路由的最大连接数 RequestConfig requestConfig RequestConfig.custom() .setConnectTimeout(5000) // 连接超时 5 秒 .setSocketTimeout(10000) // 读取超时 10 秒 .build(); CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); return new RestTemplate(factory); }这里有两个参数需要解释一下。setMaxTotal(200)是整个连接池的上限setDefaultMaxPerRoute(50)是单个目标服务地址可以占用的最大连接数。如果目标服务多MaxTotal可以调大如果集中在少数几个服务上MaxPerRoute更重要。我之前碰到过一个案例MaxTotal 设了 500但 MaxPerRoute 只有 20结果压测时所有流量都打向同一个服务连接池里的连接全堵在等待上超时一堆调大 MaxPerRoute 才解决。2.3 常用 API 与 URL 拼接的正确姿势REST Template 最常用的方法有这几个getForObject、postForObject、exchange。前两个适合简单场景exchange适合需要自定义请求头或要拿到完整响应信息的场景。一个最容易踩坑的地方是 URL 拼接。很多人图省事直接用字符串拼接String url http://user-service/api/v1/users/ userId ?source source;这种写法如果source里带了、空格、中文等特殊字符轻则请求参数解析错误严重时直接报 400。正确做法是用UriComponentsBuilderURI uri UriComponentsBuilder .fromUriString(http://user-service/api/v1/users/{id}) .queryParam(source, source) .encode() .buildAndExpand(userId) .toUri(); UserDTO user restTemplate.getForObject(uri, UserDTO.class);这里有两个细节值得注意。第一路径参数{id}和queryParam分离代码更清晰第二.encode()会帮我们把参数做 URL 编码避免特殊字符问题。这个方法我用了很多年从未翻车。再看一个携带请求头的 POST 示例HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(X-Trace-Id, traceId); String requestBody {\name\:\zhangsan\}; HttpEntityString entity new HttpEntity(requestBody, headers); ResponseEntityResultDTO response restTemplate.exchange( http://user-service/api/v1/users, HttpMethod.POST, entity, ResultDTO.class );2.4 统一拦截器不要再每个调用点手工塞请求头当项目里 REST Template 的调用点多起来以后最让人头疼的就是每个调用点都要重复设置 token、traceId。如果调用方不统一处理就会出现两个问题一是代码冗余每个调用处都要 copy 一段设置 headers 的代码二是遗漏新同事接手后很容易在某一个调用点忘了塞 token导致下游鉴权失败。好的解决办法是自定义一个ClientHttpRequestInterceptor统一在拦截器里加公共请求头public class HeaderInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers request.getHeaders(); headers.set(X-Trace-Id, TraceIdHolder.get()); headers.set(X-Forward-User, UserContext.get()); return execution.execute(request, body); } }然后在配置里把它加到 RestTemplate 上Bean public RestTemplate restTemplate() { RestTemplate restTemplate new RestTemplate(factory()); restTemplate.getInterceptors().add(new HeaderInterceptor()); return restTemplate; }注意拦截器里不要做业务重试逻辑。我曾经在拦截器里加过一段“失败了自动重试”的代码结果下游接口偶发超时拦截器把同一个请求重发了三次用户下单接口被重复调用造成了重复支付。重试应该有独立的策略和幂等控制不能放在底层拦截器里。3. OpenFeign 实战声明式调用的优雅之道3.1 起步依赖与启用配置先用 Spring Cloud 的依赖管理引入spring-cloud-starter-openfeign。注意版本对齐我遇到过最典型的坑是 Spring Boot 版本和 Spring Cloud 版本不匹配启动时报ClassNotFoundException或者注解扫描不到。以 Spring Boot 3.2.x 为例使用 Spring Cloud 2023.0.x 是兼容的。依赖如下dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency然后在启动类上加EnableFeignClientsSpringBootApplication EnableFeignClients(basePackages com.example.order.client) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }basePackages指定扫描的 client 包路径这个包就是存放所有 Feign 接口的地方。如果项目里没有这个包扫描就会失效而且不会报错只是在注入 Feign 接口时抛异常。3.2 声明式接口写法与注解使用细节下面是一个标准 OpenFeign 接口示例FeignClient(name user-service, path /api/v1/users) public interface UserClient { GetMapping(/{id}) UserDTO getUserById(PathVariable(id) Long id); PostMapping UserDTO createUser(RequestBody UserCreateRequest request); }FeignClient注解有几个属性需要理解。name是服务名OpenFeign 会拿它去注册中心找服务实例然后配合负载均衡组件发起请求。path是统一前缀如果被调用方接口都有/api/v1/users这段路径写在注解上比每个方法都写一遍要干净。这里有一个非常容易踩的坑Feign 接口的PathVariable(id)必须显式指定名称不能像 SpringMVC Controller 那样省略。因为 Feign 在生成代理时需要明确知道路径参数的名字。我见过有人写成PathVariable Long id启动不报错但运行时 URL 里永远没有 id 值接口直接 404。RequestBody和RequestParam的使用方法和 Controller 类似但要记住一个原则Feign 接口是轻量契约不要在里面定义复杂的继承结构或泛型嵌套否则序列化和反序列化时问题会非常难排查。3.3 超时、日志与拦截器配置OpenFeign 默认连接超时是 10 秒读取超时是 60 秒。这个默认值在生产环境偏长一旦下游服务出问题调用线程会长时间挂住拖垮线程池。所以项目里我基本上会立刻改掉默认值feign: client: config: default: connectTimeout: 3000 readTimeout: 8000如果想对某个服务单独配置把default换成服务名即可feign: client: config: user-service: connectTimeout: 2000 readTimeout: 5000OpenFeign 的日志默认是关闭的因为它要避免打印每个请求的信息拖慢性能。调试时我会单独把某个 client 的日志级别打开logging: level: com.example.order.client.UserClient: DEBUG然后在 Feign 配置里指定日志级别Configuration public class FeignConfig { Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }FULL会打印请求头、请求体、响应体和响应头信息最全但生产环境不建议开因为有些响应体包含敏感数据。我一般只在联调环境开FULL生产环境保持默认NONE。和 RestTemplate 的拦截器类似OpenFeign 也有自己的RequestInterceptor可以用来统一加 token 或 traceIdpublic class FeignAuthInterceptor implements RequestInterceptor { Override public void apply(RequestTemplate template) { template.header(X-Trace-Id, TraceIdHolder.get()); template.header(Authorization, Bearer TokenManager.getAccessToken()); } }这里有个我和团队踩过的坑token 不要写成静态常量。如果下游服务开启鉴权而你在拦截器里写死一个 token等它过期后所有 Feign 调用会突然集体 401。正确的做法是每次从统一的 TokenManager 里动态获取并且考虑 token 刷新机制。3.4 负载均衡、熔断与降级OpenFeign 本身不具备负载均衡能力它是通过在 HTTP 请求前选择合适的服务地址来实现的。Spring Cloud 从 2020 年开始把默认负载均衡组件从 Ribbon 换成了 Spring Cloud LoadBalancer。所以要给 OpenFeign 加负载均衡能力必须引入dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency没有这个依赖OpenFeign 的name属性无法解析成具体实例启动时也许正常一旦调用就会报No instances available for user-service之类的错误。再来说降级。Fallback 是 OpenFeign 非常实用的容错手段。下面是一个简单示例FeignClient(name user-service, fallback UserClientFallback.class) public interface UserClient { GetMapping(/{id}) UserDTO getUserById(PathVariable(id) Long id); } Component public class UserClientFallback implements UserClient { Override public UserDTO getUserById(Long id) { // 返回兜底缓存或默认对象 return new UserDTO(id, unknown); } }注意Fallback 生效有一个前提必须启用 Spring Cloud CircuitBreaker 或引入 Sentinel 等熔断依赖。如果没有配套依赖Fallback 配置了不生效而服务故障时会直接抛异常表现和没配一样。很多教程只写接口和实现类把这一点略过了导致不少人照抄后发现降级没有起作用。Fallback 类的实现原则是“快速失败、有兜底逻辑”不要在 fallback 里做耗时操作比如查数据库。降级本身就是服务不可用时的最后保障越快返回越好。4. 双方案共存怎么混用和切换最顺手4.1 什么场景下需要两者共存我先说结论不代表用了 OpenFeign 就必须把所有 HTTP 调用都改成 Feign也不代表用了 REST Template 就完全不能用 OpenFeign。它们在项目里完全可以各管一摊。以我参与过的电商项目为例订单服务内部对其他微服务一律用 OpenFeign因为它要享受注册中心的服务发现、负载均衡和降级能力。但同样的订单服务需要调用第三方物流平台的查询接口时就不太适合用 OpenFeign 了原因有三点一是第三方接口的 URL 可能是不固定的由配置中心动态下发OpenFeign 的name属性写死服务名后不好动态调整二是第三方接口的鉴权方式特殊可能需要动态拼接签名封装成拦截器反而复杂三是这类接口通常没有熔断降级的需求用 REST Template 处理更直接。这个场景是很多文章没有讲透的REST Template 更适合“非微服务体系内的对外 HTTP 调用”OpenFeign 更适合“微服务体系内的对内调用”。两者天然互补。4.2 通过接口抽象实现无缝切换如果你的项目处于重构期要慢慢把 REST Template 调用迁移到 OpenFeign或者反过来我建议给这两套调用方式做一层抽象。定义一个客户端接口public interface UserApiClient { UserDTO getUserById(Long id); }然后分别实现Component ConditionalOnProperty(name client.user.strategy, havingValue feign) public class FeignUserApiClient implements UserApiClient { private final UserClient userClient; public FeignUserApiClient(UserClient userClient) { this.userClient userClient; } Override public UserDTO getUserById(Long id) { return userClient.getUserById(id); } } Component ConditionalOnProperty(name client.user.strategy, havingValue rest) public class RestUserApiClient implements UserApiClient { private final RestTemplate restTemplate; public RestUserApiClient(RestTemplate restTemplate) { this.restTemplate restTemplate; } Override public UserDTO getUserById(Long id) { return restTemplate.getForObject(http://user-service/api/v1/users/{id}, UserDTO.class, id); } }然后在配置里选择一个实现client: user: strategy: feign # 或 rest这样做的价值在于业务代码只依赖UserApiClient接口具体用 Feign 还是 RestTemplate 是配置决定的。我在重构老项目时经常用这个方案先在老服务上用rest策略验证完毕后再切成feign线上无感。4.3 混用时的注意事项混用方案时有三件事要特别注意。第一请求轨迹 ID 必须统一。如果一部分服务间调用走 OpenFeign、一部分走 REST Template那公共请求头traceId必须同时加到 Feign 拦截器和 RestTemplate 拦截器里否则排查问题时日志链路会断很难定位一次请求到底经过了哪些服务。第二连接资源要分开管理。REST Template 的连接池和 OpenFeign 底层使用的连接池是两套体系配置时要分别评估不要觉得“总连接数已经够大了”就大意。尤其是混用阶段总连接数上限要重新估算。第三不要同时在两种方案里实现同一套复杂逻辑比如统一加解密。统一加解密应该下沉到网关或公共过滤器而不是在客户端层各做各的。我在某个项目里见过 Feign 拦截器和 RestTemplate 拦截器各自加了一层加密逻辑结果下游收到的字段被重复加密排查了很久才发现是两端实现不一致。5. 常见问题与排查技巧实录5.1 连接超时与读取超时到底谁在报错超时是最常见的问题但很多人分不清报错来自哪一层。connect timed out表示建立 TCP 连接阶段失败原因通常是网络不通、端口未开放、防火墙拦截read timed out表示连接建立成功但服务端在指定时间内没有返回数据原因通常是服务端处理太慢或者数据库查询卡住了。我的排查顺序是这样的先用telnet或curl直接请求目标服务地址判断网络层是否通如果网络通再看服务端日志确认是被调用方接口本身的耗时超过了调用方设置的 readTimeout还是调用方设置的超时时间本身太短。很多团队把 connectTimeout 和 readTimeout 都设成一样的值这是不合理的。connectTimeout 是网络级参数一般在 2 到 5 秒readTimeout 是业务级参数要根据下游接口的实际响应时间来定可能 5 秒、10 秒甚至更长。5.2 序列化问题LocalDateTime 和泛型微服务之间传 JSON最常踩的序列化坑有两个。第一个是LocalDateTime字段反序列化失败。默认的 Jackson 不认 Java 8 时间类型会报类似InvalidDefinitionException: Java 8 date/time type not supported的错误。解决方法是引入jackson-datatype-jsr310依赖并在 ObjectMapper 里注册JavaTimeModule。Spring Boot 如果使用spring-boot-starter-json一般会自动配置好但如果你手动 new 过 ObjectMapper就很容易把它丢掉。第二个坑是泛型返回类型丢失。直接调用restTemplate.getForObject(url, ResultDTO.class)没问题但如果是ResultDTOListUserDTO这种嵌套泛型getForObject 无法正确反序列化 JSON 里的data字段。正确做法是用ParameterizedTypeReferenceResponseEntityResultDTOListUserDTO response restTemplate.exchange( url, HttpMethod.GET, null, new ParameterizedTypeReferenceResultDTOListUserDTO() {} );Feign 接口里如果定义这种泛型返回值同样要注意不过 Feign 在这块一般表现更好因为它底层用的是自定义的Decoder对泛型的处理比 RestTemplate 默认机制要强。5.3 OpenFeign 负载均衡不生效怎么办OpenFeign 的负载均衡没生效最常见的表现是配置了服务名但接口总是请求到一个特定实例其他实例完全没有流量或者直接报找不到实例。排查步骤我建议按顺序来。第一确认是否引入了spring-cloud-starter-loadbalancer。第二确认FeignClient的name是否和注册中心的服务名完全一致大小写也要一致。第三确认调用方和被调用方是否注册到同一个注册中心命名空间比如 Nacos 的 namespace、group 配置不一致也会导致服务列表为空。第四检查被调用方服务是否真的注册成功了可以直接去注册中心控制台看服务列表。有一种比较隐蔽的情况是OpenFeign 的url属性被设置了。一旦显式配置了urlOpenFeign 会直接使用这个地址不经过注册中心也就没有负载均衡。很多人在本地调试时为了绕开注册中心给FeignClient加过url然后忘了删上了生产环境之后负载均衡就“神秘失效”。遇到这种情况先全局搜一下url 。5.4 版本冲突老问题Spring Boot 2.4 之前OpenFeign 默认集成了 Ribbon引入spring-cloud-starter-openfeign后负载均衡开箱即用。但从 Spring Cloud 2020.0 版本开始Ribbon 进入维护模式Spring Cloud 官方把默认负载均衡组件换成了 Spring Cloud LoadBalancer。如果大家在网上搜到老教程照抄之后发现没有负载均衡原因就在这。Spring Boot 升级到 3.x 之后社区线程模型也发生了大变化OpenFeign 本身需要依赖新版本的 Feign core。用 Spring Boot 3.x 配一个很老的spring-cloud-starter-feign启动时大概率会报类加载错误或方法签名不匹配。这种问题的根源不是代码逻辑而是 Spring Cloud 版本和 Spring Boot 版本没有对齐最直接的办法是去 Spring Cloud 官方文档查版本对应关系表而不是一个个去猜。5.5 排查小工具与速查表最后分享一个我常用的排查思路遇到服务间调用异常先看清楚异常栈是客户端封装层报错还是下游接口返回错误状态码。如果是客户端封装层报错优先检查超时、序列化、负载均衡如果是下游返回错误优先看下游服务的日志。问题现象可能原因处理建议connect timed out网络不通、端口未开放、防火墙拦截telnet 测试端口检查安全组规则read timed out下游处理慢、连接池耗尽加大 readTimeout排查下游性能No instances available未引入负载均衡依赖或服务未注册检查依赖和服务注册状态反序列化失败LocalDateTime 未适配、泛型嵌套注册 JavaTimeModule用 ParameterizedTypeReference所有请求打到同一实例FeignClient配了 url 属性移除 url改用服务名fallback 不触发未引入熔断依赖引入 spring-cloud-starter-circuitbreaker 或 Sentinel我自己在验收一个微服务通信方案时有一套固定动作先跑通普通接口再验证超时配置是否生效然后关闭一个服务实例看负载均衡是否把流量切到其他实例最后开启熔断降级人为停掉下游服务看 fallback 是否返回兜底数据。这一套流程走下来通信方案基本就稳了。回到实践如果你从零开始搭一个微服务项目我的建议是服务间调用直接用 OpenFeign因为它的接口契约和维护体验是 REST Template 替代不了的。REST Template 留作集成外部系统、处理特殊 HTTP 请求时的专业工具。两个都会而不是二选一才是微服务开发者的正确姿势。
返回列表