OpenFeign 服务调用
声明式 HTTP 客户端、负载均衡。
OpenFeign 服务调用
这一课你将能做到
- 在 user-service 里声明一个 Feign 接口,像调本地方法一样调用 problem-service 的 HTTP API
- 配置全局与单个 Client 的超时,并通过 RequestInterceptor 把登录态透传给下游
- 说清 Feign 背后的动态代理流程,以及它和注册中心、负载均衡的配合
为什么要学
服务拆开之后必然互相调用:user-service 的「学习面板」要展示用户信息 + 在学题目,就得调 problem-service。用 RestTemplate/RestClient 手写:URL 自己拼、负载均衡自己做、超时要手动包、接口变了编译期毫无提示——全是样板代码。
OpenFeign 是声明式 HTTP 客户端:定义一个 Java 接口加注解,框架在运行时生成实现,帮你完成「按服务名找实例 → 负载均衡选一个 → 发 HTTP → 反序列化成对象」整套动作。代码里只剩接口和 DTO,可读性和可测试性都上一个台阶。
先建立图像
user-service 里调用:
problemClient.getProblem(1L) ← 看起来是本地方法
│
▼
Feign 动态代理(运行时生成的接口实现)
│ ① 解析 @GetMapping("/problems/{id}") 拼出请求模板
│ ② LoadBalancer 按服务名 problem-service 问 Eureka 要实例列表,轮询选一个
│ ③ RequestInterceptor 补 Header(如 Authorization)
│ ④ 发出 HTTP GET http://10.0.0.7:8082/problems/1
▼
problem-service 返回 JSON → 反序列化成 ProblemDTO 返回给调用方
关键词:声明式接口 + 动态代理 + 服务名寻址 + 负载均衡。
跟着做
步骤 1:给 user-service 加依赖
pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
步骤 2:定义 DTO 与 Feign 接口
DTO(两端共享或各自定义一份,字段名对齐即可):
package com.example.user.dto;
public record ProblemDTO(Long id, String title, String difficulty) {}
Feign 接口:
package com.example.user.client;
import com.example.user.dto.ProblemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@FeignClient(name = "problem-service") // name = 注册中心里的服务名
public interface ProblemClient {
@GetMapping("/problems/{id}")
ProblemDTO getProblem(@PathVariable("id") Long id);
@GetMapping("/problems")
List<ProblemDTO> listByDifficulty(@RequestParam("difficulty") String difficulty);
@PostMapping("/problems/{id}/favorite")
void favorite(@PathVariable("id") Long id, @RequestBody FavoriteRequest req);
record FavoriteRequest(Long userId, boolean favorite) {}
}
注意两个易错点:@PathVariable 和 @RequestParam 在 Feign 接口里必须显式写参数名(接口编译后默认不保留参数名)。
步骤 3:启用并注入使用
启动类加 @EnableFeignClients:
package com.example.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.user.client")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
在业务代码里直接注入(Feign 会为接口生成代理 Bean):
package com.example.user.service;
import com.example.user.client.ProblemClient;
import com.example.user.dto.ProblemDTO;
import org.springframework.stereotype.Service;
@Service
public class DashboardService {
private final ProblemClient problemClient;
public DashboardService(ProblemClient problemClient) {
this.problemClient = problemClient;
}
/** 学习面板:用户侧信息 + 跨服务取题目详情 */
public String dashboard(Long userId, Long problemId) {
ProblemDTO problem = problemClient.getProblem(problemId);
return "用户 " + userId + " 正在做:" + problem.title() + "(" + problem.difficulty() + ")";
}
}
package com.example.user.controller;
import com.example.user.service.DashboardService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DashboardController {
private final DashboardService dashboardService;
public DashboardController(DashboardService dashboardService) {
this.dashboardService = dashboardService;
}
@GetMapping("/dashboard/{userId}/problem/{problemId}")
public String dashboard(@PathVariable Long userId, @PathVariable Long problemId) {
return dashboardService.dashboard(userId, problemId);
}
}
依次启动 eureka → problem-service → user-service,访问 http://localhost:8081/dashboard/1001/problem/1——user-service 已经通过服务名调到了 problem-service,代码里没有任何 URL。
步骤 4:配置超时(必做)
spring:
cloud:
openfeign:
client:
config:
default: # 全局默认
connectTimeout: 2000
readTimeout: 5000
problem-service: # 针对 problem-service 覆盖
connectTimeout: 2000
readTimeout: 8000
loggerLevel: basic
不配超时等于裸奔:下游一慢,调用方线程被成批占住,故障瞬间向上传染。
步骤 5:RequestInterceptor 透传登录态
package com.example.user.config;
import feign.RequestInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Configuration
public class FeignConfig {
/** 把进来的 Authorization 原样带给下游,保持登录态跨服务传递 */
@Bean
public RequestInterceptor authInterceptor() {
return template -> {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) {
HttpServletRequest request = attrs.getRequest();
String token = request.getHeader("Authorization");
if (token != null) {
template.header("Authorization", token);
}
}
};
}
}
步骤 6:看 Feign 的调用日志
logging:
level:
com.example.user.client.ProblemClient: DEBUG
配 loggerLevel: basic(或 full)后,DEBUG 日志会打印请求 URL、状态码、耗时,排查「调没调到、调了多久」最直观。
讲清楚要点
@FeignClient(name) 的值就是注册中心里的服务名:Feign 把它交给 LoadBalancer 查实例列表;没有注册中心时也可写 url = "http://..." 直连(仅测试用)。
- 动态代理是核心原理:启动时 Feign 为接口生成代理对象并注册为 Bean;调用方法时代理把注解元数据翻译成 HTTP 请求模板,编码器序列化参数,发出请求后解码器反序列化返回值。
- 负载均衡在客户端:Spring Cloud LoadBalancer 默认轮询(Round Robin);Eureka 给列表,LoadBalancer 挑一个——「发现」与「均衡」分层解耦。
- 超时两层都要懂:connectTimeout(建连)+ readTimeout(等响应),可在
default 全局、单个服务名下、@FeignClient(configuration=...) 三层覆盖,粒度越小优先级越高。
- 错误传播:下游非 2xx 会抛
FeignException,状态码和响应体都在异常里;配合 ErrorDecoder 可把下游错误翻译成本服务更语义化的异常。
- Feign 不自带容错:超时了就是异常,重试/熔断/降级要靠 Resilience4j(下一课),两者是组合关系不是替代关系。
你可能会懵的地方
-
Feign 和 RestTemplate/RestClient 什么时候选谁?
跨服务高频业务调用选 Feign(接口化、可 Mock、统一超时);偶尔调一次外部第三方 API 用 RestClient 更轻。同一家公司内部服务间几乎都用 Feign,风格统一比灵活重要。
-
Feign 能不能传文件、自定义 Header?
都能。文件用 @PostMapping + MultipartFile 参数(需要 feign-form 相关支持或用 consumes 配置);自定义 Header 直接在方法上加 @RequestHeader("X-Foo") String foo,批量透传用 RequestInterceptor。
-
为什么 GET 请求带 @RequestBody 是坏味道?
HTTP 语义上 GET 应无请求体,部分代理/网关会丢弃它导致诡异 bug。查询条件用 @RequestParam 或改成 POST 查询接口。
-
接口两边都要定义,会不会重复劳动?
会,这是分布式的固有成本。进阶做法是把「API 接口 + DTO」抽成独立 api 模块,服务提供方实现它、消费方引依赖生成 Feign 代理——契约单一来源,编译期就能发现不兼容。
-
Feign 的 fallback 是什么?为什么现在配了没生效?
fallback 是调用失败时的兜底实现,但它依赖熔断器(Resilience4j)介入才会触发,还要开 spring.cloud.openfeign.circuitbreaker.enabled=true——这正是下一课的主题。
错法对照
| 错法 | 问题 | 正解 |
|---|
@FeignClient(url="http://localhost:8082") 用于生产 | 绕过注册中心与负载均衡 | 生产用 name="problem-service",url 仅限本地调试 |
@PathVariable Long id 不写参数名 | 模板里 {id} 找不到实参,启动/调用报错 | @PathVariable("id") Long id,@RequestParam 同理 |
忘加 @EnableFeignClients | 注入 ProblemClient时报找不到 Bean | 启动类开启并指定 basePackages |
| 不配 readTimeout | 默认偏长,下游慢时线程被拖垮 | 全局 2s/5s 起步,慢接口单独覆盖 |
| 在循环里逐条 Feign 调用 100 次 | 100 次网络往返,RT 爆炸 | 下游提供批量接口,一次拉回 |
卡点急救
- 报
No Feign Client for loadBalancing defined 或找不到负载均衡:缺 spring-cloud-starter-loadbalancer,或 name 拼写与注册中心的服务名(大小写不敏感但中划线要对)不一致。
- 调用报 404:Feign 接口路径与目标服务实际路径不一致(别忘了目标服务前面有没有 context-path);开 DEBUG 日志看实际请求 URL 逐字符对比。
- 启动报
Invalid HTTP method: PATCH 之类:Feign 默认用 HttpURLConnection 不支持部分方法,换底层客户端为 OkHttp(feign.okhttp.enabled=true + 依赖)或 Apache HttpClient5。
面试常问
-
OpenFeign 的工作原理?(必背)
①启动时 @EnableFeignClients 扫描 @FeignClient 接口 → ②FeignContext 为每个 Client 创建独立的组件容器 → ③JDK 动态代理生成接口实现并注册为 Bean → ④调用时解析注解生成 RequestTemplate → ⑤LoadBalancer 按服务名取实例列表并选择一个(默认轮询)→ ⑥编码器序列化请求体,发 HTTP → ⑦解码器把响应反序列化为方法返回类型 → ⑦异常交给 ErrorDecoder。若类路径有熔断器且开关打开,整个调用还会被 CircuitBreaker 包裹。
-
Feign、RestTemplate、WebClient 对比?
RestTemplate:Servlet 栈命令式模板类,样板代码多,官方已建议迁移;WebClient:响应式非阻塞,适合 WebFlux 与高并发流式;Feign:声明式接口 + 与注册中心/负载均衡/熔断生态无缝集成,是服务间调用的事实标准。选型口诀:服务间 Feign,外部 API RestClient/WebClient。
-
服务间调用的超时和重试怎么设计?
超时必须显式配置(连接 1-2s、读按下游 P99 定);重试只对幂等 GET 且次数 ≤2,配合指数退避;写操作不盲目重试,用幂等键。三者与熔断组合:重试解决瞬时抖动,熔断防持续故障,超时是两者的安全底线。
自测三问
@FeignClient(name = "problem-service") 发起调用时,name 在哪几步被用到?
- Feign 的超时可以在哪几层配置?优先级如何?
- 为什么 Feign 接口里
@PathVariable 必须写参数名?
小结一句话
OpenFeign 用「接口 + 注解」声明远程调用,动态代理在运行时完成服务名寻址、负载均衡、HTTP 编解码,让跨服务调用长得像本地方法。
延伸