远程调用
Feign 是声明式 HTTP 客户端。它的目标是让远程 HTTP 调用像调用本地接口一样自然,开发者只需要定义接口和注解,Feign 负责生成代理对象、拼接请求、发送 HTTP、解析响应。
阅读入口:从一个接口方法追到网络包
按“接口契约 → 动态代理 → RequestTemplate → 服务发现与负载均衡 → HTTP Client → Decoder/异常”阅读。基础用法只解决如何声明接口,生产问题集中在内部原理与生产治理。
核心原理:接口代理到 HTTP 响应的转换链
启动时 FeignClientsRegistrar 注册客户端工厂,调用时代理根据 MethodMetadata 创建 RequestTemplate,再交给负载均衡客户端选择实例和底层 HTTP Client 执行;响应最后经过 Decoder 或 ErrorDecoder 转成对象或异常。
flowchart TD
A["调用StockClient方法"] --> B["JDK动态代理/MethodHandler"]
B --> C["Contract解析注解"]
C --> D["Encoder构造RequestTemplate"]
D --> E["Interceptor注入Trace/Token"]
E --> F["Discovery + LoadBalancer"]
F --> G["HTTP Client发送"]
G --> H["Decoder或ErrorDecoder"]本页负责零基础概念、常用配置和基础商业用法。需要继续追踪FeignClientsRegistrar、FeignClientFactoryBean、命名子容器、ReflectiveFeign、SynchronousMethodHandler、LoadBalancer桥接、连接池、三层重试和生产Runbook,请继续阅读:
为什么需要 Feign
不用 Feign 时,服务间调用通常要手写 URL、参数、请求头、序列化和反序列化逻辑。代码容易重复,也不利于统一处理异常、日志和超时。
使用 Feign 后,调用方只关注“我要调用哪个服务的哪个接口”。
flowchart TD
A[业务代码调用接口方法] --> B[Feign 代理对象]
B --> C[根据注解构造 HTTP 请求]
C --> D[通过服务名找到实例]
D --> E[发送请求]
E --> F[解析响应为 Java 对象]基本使用思路
@FeignClient(name = "stock-service")
public interface StockClient {
@GetMapping("/stock/{skuId}")
StockDTO getStock(@PathVariable("skuId") Long skuId);
}业务代码里直接注入 StockClient,调用 getStock 方法即可。
Feign 做了什么
| 能力 | 说明 |
|---|---|
| 接口代理 | 根据接口创建动态代理 |
| 请求构造 | 根据 Spring MVC 注解生成 HTTP 请求 |
| 服务发现 | 通过服务名找到服务实例 |
| 负载均衡 | 配合 Ribbon 或 LoadBalancer 选择实例 |
| 编解码 | 请求对象序列化,响应内容反序列化 |
| 异常处理 | 可配置 ErrorDecoder、Fallback |
Feign、Nacos、LoadBalancer和HTTP Client怎么分工
用一句话说清:
Feign负责把Java接口调用变成HTTP请求;Nacos负责告诉调用方某个服务有哪些实例;LoadBalancer负责从这些实例里选一个;真正把请求发出去的是底层HTTP Client。
例如:
@FeignClient(name = "stock-service")
public interface StockClient {
@GetMapping("/stock/{skuId}")
StockDTO getStock(@PathVariable("skuId") Long skuId);
}业务代码调用:
StockDTO stock = stockClient.getStock(1001L);它并不是在JVM里直接执行库存服务的方法,而是大致经历:
stockClient.getStock(1001L)
-> Feign代理生成 GET http://stock-service/stock/1001
-> LoadBalancer按 stock-service 找候选实例
-> Nacos Discovery提供 stock-service 的实例快照
-> LoadBalancer选中 10.10.2.17:8080
-> 重建URL为 http://10.10.2.17:8080/stock/1001
-> HTTP Client真正发送请求flowchart TD
A["业务代码调用Feign接口"] --> B["Feign代理生成逻辑请求"]
B --> C["URL主机名是服务名"]
C --> D["LoadBalancer提取serviceId"]
D --> E["DiscoveryClient读取实例列表"]
E --> F["Nacos Client返回本地实例快照"]
F --> G["LoadBalancer选择实例"]
G --> H["替换为真实IP和端口"]
H --> I["HTTP Client发送请求"]这里最容易混淆的是Nacos的位置。Nacos像“服务通讯录”,不是“业务请求中转站”。真实业务请求不是:
订单服务 -> Nacos -> 库存服务而是:
订单服务 -> 库存服务实例Nacos只在控制面提供实例数据:
订单服务 -> Nacos:stock-service有哪些实例?
Nacos -> 订单服务:这些IP:Port可用
订单服务 -> 某个库存服务实例:发送真实HTTP请求生产环境中,调用方通常使用Nacos Client本地缓存的实例列表,并通过订阅推送或定时刷新感知变化,不会每次Feign调用都同步请求一次Nacos Server。否则注册中心会被业务QPS打成瓶颈。
Feign 调用的底层流程
Feign 写起来像本地接口,但执行时一定会走网络。完整链路可以这样理解:
flowchart TD
A["业务代码调用 StockClient.getStock"] --> B["Feign 动态代理拦截方法"]
B --> C["Contract 解析注解<br/>GetMapping / PathVariable"]
C --> D["Encoder 编码请求参数"]
D --> E["RequestInterceptor 添加请求头<br/>traceId / token"]
E --> F["LoadBalancer 根据服务名选实例"]
F --> G["HTTP Client 发送请求<br/>OkHttp / Apache HC / JDK"]
G --> H["按成功、HTTP错误或网络失败分类"]
H --> I["成功由Decoder解码<br/>错误由ErrorDecoder或异常表达"]
I --> J["熔断启用时可进入fallback<br/>否则返回结果或向上抛错"]图中把互斥结果纵向展示以适配窄屏;一次调用只会进入成功解码、HTTP错误或网络失败中的对应路径。是否进入fallback还取决于CircuitBreaker是否真正装配,完整分支见下方角色表和深层原理页。
这里有几个角色:
| 角色 | 作用 |
|---|---|
Contract | 解析接口上的注解,知道 URL、方法、参数怎么拼 |
Encoder | 把 Java 对象编码成请求体 |
Decoder | 把响应体解析成 Java 对象 |
RequestInterceptor | 统一加请求头,比如 token、traceId、租户 ID |
Client | 真正发 HTTP 请求 |
ErrorDecoder | 把非 2xx 响应转换成业务异常或系统异常 |
LoadBalancer | 根据服务名选择具体实例 |
所以 Feign 的本质不是 RPC 魔法,而是“接口代理 + HTTP 请求模板 + 编解码 + 负载均衡 + 错误处理”。
Feign 在启动阶段做了什么
Feign 不是等你第一次调用接口时才临时分析注解。Spring Boot 启动时,OpenFeign 会扫描 @FeignClient 接口,并把这些接口注册成 Spring 容器里的代理 Bean。
flowchart TD
A["启动类启用 @EnableFeignClients"] --> B["扫描 @FeignClient 接口"]
B --> C["为每个接口创建 FeignClientFactoryBean"]
C --> D["解析服务名、路径、配置类"]
D --> E["构建 Contract、Encoder、Decoder、Client"]
E --> F["创建 JDK 动态代理对象"]
F --> G["代理对象放入 Spring 容器"]所以你注入的 StockClient 并不是自己写的实现类:
@Service
public class OrderService {
private final StockClient stockClient;
public OrderService(StockClient stockClient) {
this.stockClient = stockClient;
}
}这里的 stockClient 本质是代理对象。调用 stockClient.getStock(1001L) 时,代理对象会把“方法调用”转换成“HTTP 请求”。
如果启动阶段没有正确创建代理,常见问题包括:
| 问题 | 表现 | 原因 |
|---|---|---|
| 没有启用 Feign 扫描 | 注入 StockClient 失败 | 缺少 @EnableFeignClients 或扫描包不对 |
@FeignClient 名称写错 | 调用时报无实例 | 服务名和注册中心不一致 |
| 接口注解不规范 | 启动失败或请求路径错误 | @PathVariable 没写 name,参数绑定不清楚 |
| 配置类放错位置 | 所有 Feign 都受影响 | 局部配置被全局扫描 |
为什么 Feign 默认用 JDK 动态代理
Feign Client 通常是接口:
@FeignClient(name = "stock-service")
public interface StockClient {
@GetMapping("/stock/{skuId}")
StockDTO getStock(@PathVariable("skuId") Long skuId);
}接口没有普通实现类,JDK 动态代理正适合“为接口生成代理对象”。代理对象的核心逻辑不是执行业务,而是进入 Feign 的 InvocationHandler:
调用接口方法
-> 进入代理对象
-> 根据 Method 找到 MethodHandler
-> 根据注解和参数创建 RequestTemplate
-> 发起 HTTP 请求
-> 解码响应这也是为什么 Feign 方法不要写成本地工具方法思维。它看起来是 Java 方法,实际跨进程、跨网络、跨线程池、跨数据库。
@FeignClient 每个属性到底影响什么
@FeignClient(
name = "stock-service",
contextId = "stockClient",
path = "/api",
configuration = StockFeignConfig.class,
fallbackFactory = StockClientFallbackFactory.class
)
public interface StockClient {
@GetMapping("/stock/{skuId}")
StockDTO getStock(@PathVariable("skuId") Long skuId);
}| 属性 | 含义 | 写错会怎样 |
|---|---|---|
name / value | 目标服务名,用于服务发现和负载均衡 | 注册中心找不到实例,报 No instances available |
contextId | 当前 Feign Client 在 Spring 中的唯一上下文 ID | 多个同名 Client 时 Bean 冲突 |
path | 所有方法公共前缀 | 路径重复或漏前缀导致 404 |
configuration | 当前 Client 的局部配置 | 配错会影响编解码、超时、日志、拦截器 |
fallback | 固定降级实现 | 拿不到原始异常原因 |
fallbackFactory | 能拿到异常原因的降级工厂 | 写错容易把真正异常吞掉 |
生产项目里更推荐用 fallbackFactory,因为它能拿到失败原因。你可以区分是超时、404、500、熔断还是无实例。
RequestTemplate 是怎么拼出来的
Feign 解析注解后,会先生成一个请求模板。模板不是最终请求,因为方法参数还没填进去;真正调用时,Feign 会把参数填进模板。
@PostMapping("/orders/{orderId}/stock")
Result<Void> lockStock(@PathVariable("orderId") Long orderId,
@RequestParam("skuId") Long skuId,
@RequestBody LockStockCommand command);调用:
stockClient.lockStock(9001L, 1001L, new LockStockCommand(2));会被转换成:
POST /orders/9001/stock?skuId=1001
Content-Type: application/json
{"count":2}流程如下:
flowchart TD
A["接口方法和注解"] --> B["Contract 解析 HTTP Method 和 Path"]
B --> C["MethodMetadata 保存参数位置"]
C --> D["调用时填充 PathVariable 和 RequestParam"]
D --> E["Encoder 序列化 RequestBody"]
E --> F["RequestInterceptor 添加 Header"]
F --> G["生成最终 HTTP Request"]常见错误和后果:
| 错误写法 | 后果 |
|---|---|
@PathVariable 不指定名称 | 某些编译参数缺失时无法绑定 |
| GET 请求传复杂对象但下游不支持 | 参数丢失或 400 |
POST 忘记 @RequestBody | 下游收不到 JSON body |
| Feign 接口路径和 Controller 路径不一致 | 404 |
| 日期、枚举格式两边不一致 | 反序列化失败或业务含义错误 |
Encoder 和 Decoder 原理
Encoder 负责把 Java 对象变成 HTTP 请求体,Decoder 负责把 HTTP 响应体变回 Java 对象。
flowchart TD
A["Java 请求对象"] --> B["Encoder"]
B --> C["JSON / Form / Multipart 请求体"]
C --> D["HTTP Client 发送"]
D --> E["HTTP 响应体"]
E --> F["Decoder"]
F --> G["Java 响应对象"]例如请求对象:
public class LockStockCommand {
private Integer count;
public LockStockCommand(Integer count) {
this.count = count;
}
public Integer getCount() {
return count;
}
}会被编码成:
{
"count": 2
}如果下游所有接口都返回 Result<T>,Feign 方法返回值也要和协议一致:
Result<StockDTO> getStock(Long skuId);否则下游真实返回:
{
"code": 0,
"data": {
"skuId": 1001,
"count": 20
}
}而你却让 Feign 直接解码成 StockDTO,就会出现字段为空、反序列化失败或业务语义错乱。统一响应包装要在 SDK 层统一处理,不能让每个业务方各自猜。
HTTP Client、连接池和线程为什么重要
Feign 自己不是 TCP 客户端,真正发请求的是底层 HTTP Client,例如 Apache HttpClient、OkHttp 或 JDK Client。
flowchart TD
A["Feign Request"] --> B["从连接池获取连接"]
B --> C["有空闲连接则复用<br/>没有则建立新连接"]
C --> D["应用连接池等待与Connect Timeout"]
D --> E["发送请求并等待响应"]
E --> F["应用Read Timeout"]
F --> G["读取完成后归还或关闭连接"]如果连接池配置不合理,会出现非常隐蔽的问题:
| 问题 | 表现 | 原因 |
|---|---|---|
| 连接池太小 | 上游线程排队等连接 | 并发超过连接池容量 |
| 读取超时太长 | 线程长时间阻塞 | 下游慢但上游不释放资源 |
| 连接未正确释放 | 后续请求越来越慢 | 响应体没读完或客户端配置错误 |
| 长连接遇到旧实例 | 服务下线后仍打到旧连接 | 连接池存量连接未关闭 |
生产建议:
spring:
cloud:
openfeign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
client:
config:
default:
connectTimeout: 1000
readTimeout: 3000
loggerLevel: basic不同 Spring Cloud 版本配置项可能略有差异,项目里要以实际版本为准。原则是:不要只配 Feign 超时,也要关注 HTTP Client 连接池容量、每路由连接数、连接存活时间和指标监控。
为什么 Feign 不是本地调用
本地方法调用一般只受 CPU 和内存影响;Feign 调用会额外受这些因素影响:
- DNS 或注册中心实例列表。
- 负载均衡选择。
- 网络延迟。
- 下游线程池和连接池。
- HTTP 连接建立和复用。
- 序列化和反序列化。
- 超时、重试、熔断、降级。
如果把 Feign 当成本地调用,就很容易写出这种代码:
for (Long recordId : recordIds) {
RecordDetail detail = recordClient.getDetail(recordId);
handle(detail);
}这会造成 N 次远程调用。商业系统里应优先改成批量接口:
List<RecordDetail> details = recordClient.getDetails(recordIds);
for (RecordDetail detail : details) {
handle(detail);
}医疗采集平台里,如果一批 1000 条数据逐条 Feign 调字典服务,延迟会被放大 1000 次,还可能拖垮字典服务。应设计批量查询、本地缓存或异步预热。
超时和重试
远程调用必须设置超时。没有超时的调用会长时间占用线程,最终拖垮上游服务。
flowchart TD
A["Feign发起请求"] --> B["按成功或超时分类"]
B --> C["成功返回结果<br/>超时形成调用异常"]
C --> D["熔断启用时可fallback<br/>未启用则异常向上抛出"]成功与超时、执行fallback与向上抛错都是互斥结果;图中纵向合并用于窄屏阅读。Fallback是否可用取决于CircuitBreaker是否装配,不能只看注解属性。
重试要谨慎开启。查询接口可以适当重试,创建订单、扣库存、支付这类写操作必须考虑幂等,否则可能造成重复写入。
连接超时和读取超时
| 超时 | 含义 | 典型问题 |
|---|---|---|
| 连接超时 | 建立连接最多等多久 | 下游实例不可达、网络不通 |
| 读取超时 | 请求发出后等响应最多多久 | 下游处理慢、数据库慢 |
配置示例:
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 5000
dictionary-service:
connectTimeout: 1000
readTimeout: 3000具体配置前缀会随 Spring Cloud 版本有差异,落地时以项目版本文档为准,但原则不变:所有远程调用都必须有明确超时。
为什么重试可能造成雪崩
下游已经很慢时,如果上游还不断重试,会把流量放大。
flowchart TD
A["下游变慢"] --> B["上游请求超时"]
B --> C["自动重试"]
C --> D["同一业务请求变成多次下游请求"]
D --> E["下游压力更大"]
E --> F["更多请求继续超时并形成雪崩"]因此重试要满足:
- 只对幂等接口开启。
- 设置最大次数。
- 使用退避策略。
- 配合熔断和限流。
- 写操作必须有业务唯一键或幂等表兜底。
重试放大效应怎么算
假设入口 QPS 是 1000,订单服务调用库存服务,Feign 配了最多重试 2 次,也就是一次业务请求最多打 3 次库存服务。
下游最大请求量 = 原始请求量 * (1 + 重试次数)
= 1000 * (1 + 2)
= 3000 QPS如果订单服务还会调用优惠券服务、积分服务、用户服务,每个服务都重试,链路上的放大效应会更明显。下游越慢,上游越重试,越容易把“局部慢”放大成“系统雪崩”。
flowchart TD
A["入口请求增加"] --> B["订单服务调用库存"]
B --> C["库存超时"]
C --> D["订单服务重试"]
D --> E["库存请求量放大"]
E --> F["库存更慢"]
F --> C生产上可以按接口类型决定是否允许自动重试:
| 接口类型 | 是否建议自动重试 | 原因 |
|---|---|---|
| 查询字典、查询配置 | 可以少量重试 | 天然幂等,失败影响较小 |
| 查询订单状态 | 可以少量重试 | 查询不会改变状态 |
| 创建订单 | 不建议盲目重试 | 可能重复创建 |
| 扣库存 | 只能在幂等保护下重试 | 可能重复扣减 |
| 支付扣款 | 极其谨慎 | 超时后可能已经扣款成功 |
| 发送短信 | 谨慎 | 可能重复通知用户 |
Fallback 降级
Feign 可以结合 Hystrix 实现 fallback。下游不可用时,返回默认值或友好错误,避免异常继续扩散。
降级不是“假装成功”。例如库存服务不可用时,不能默认扣库存成功,而应该返回“系统繁忙”或进入补偿流程。
FallbackFactory 比 Fallback 更适合排查
简单 fallback:
@Component
public class StockClientFallback implements StockClient {
@Override
public Result<StockDTO> getStock(Long skuId) {
return Result.fail("库存服务不可用");
}
}这种写法的问题是:业务知道降级了,但不知道为什么降级。是无实例、连接超时、读取超时、下游 500,还是熔断打开?
更推荐:
@Component
public class StockClientFallbackFactory implements FallbackFactory<StockClient> {
@Override
public StockClient create(Throwable cause) {
return skuId -> {
log.warn("调用库存服务失败, skuId={}, cause={}", skuId, cause.toString());
return Result.fail("库存服务暂不可用,请稍后重试");
};
}
}降级要遵守两个原则:
- 不能把失败伪装成成功。
- 不能吞掉关键异常信息。
例如扣库存失败时,不能返回“扣库存成功”。正确做法通常是返回失败、记录异常、触发补偿或让订单进入待确认状态。
商业场景:订单创建调用库存服务
下面是一个更接近真实项目的 Feign 设计。目标不是“能调通”,而是让调用具备超时、幂等、错误语义和排查能力。
1. 定义幂等命令
public class LockStockCommand {
private String requestNo;
private Long orderId;
private Long skuId;
private Integer count;
public String getRequestNo() {
return requestNo;
}
public Long getOrderId() {
return orderId;
}
public Long getSkuId() {
return skuId;
}
public Integer getCount() {
return count;
}
}requestNo 是幂等号。即使上游因为网络抖动重试,下游也可以根据 requestNo 判断这是不是同一次业务请求。
2. 定义 Feign Client
@FeignClient(
name = "stock-service",
path = "/api/stocks",
fallbackFactory = StockClientFallbackFactory.class
)
public interface StockClient {
@PostMapping("/lock")
Result<Void> lock(@RequestBody LockStockCommand command);
@GetMapping("/lock-records/{requestNo}")
Result<LockRecordDTO> queryLockRecord(@PathVariable("requestNo") String requestNo);
}为什么要有 queryLockRecord?因为远程写操作超时时,调用方无法确定下游到底有没有执行成功。此时应该查询状态,而不是直接重试或直接判失败。
3. 调用方处理超时不确定性
public void createOrder(CreateOrderCommand command) {
String requestNo = "LOCK-" + command.getOrderId();
LockStockCommand lockCommand = buildLockCommand(command, requestNo);
try {
Result<Void> result = stockClient.lock(lockCommand);
if (!result.isSuccess()) {
throw new BusinessException("库存锁定失败: " + result.getMessage());
}
} catch (RetryableException ex) {
Result<LockRecordDTO> record = stockClient.queryLockRecord(requestNo);
if (record.isSuccess() && record.getData().isLocked()) {
return;
}
throw new BusinessException("库存状态未知,请稍后重试");
}
}这段代码体现了远程写操作的核心原则:
| 原则 | 为什么 |
|---|---|
| 写操作带幂等号 | 防止重试造成重复扣减 |
| 超时后查状态 | 因为超时不代表下游失败 |
| 不把降级当成功 | 防止订单和库存状态不一致 |
| 失败要可补偿 | 可以进入异常表或人工处理 |
Feign 线上排查路径
遇到“Feign 调用失败”,不要一上来就改超时。按下面路径定位:
flowchart TD
A["Feign 调用失败"] --> B["保存 traceId、methodKey和第一处cause"]
B --> C["判断代理、编码、选址或网络阶段"]
C --> D["核对最终实例和HTTP状态"]
D --> E["核对连接池、下游Trace和资源指标"]
E --> F["核对重试、熔断与fallback"]
F --> G["按根因恢复并用真实请求验证"]具体异常与检查项不是并行乱查,而是按证据分层。No instances、Connect Timeout、Read Timeout、404、500和熔断的第一检查点见紧随其后的表格;更完整的命令与恢复边界见Feign生产Runbook。
排查证据建议:
| 证据 | 说明 |
|---|---|
| traceId | 串起调用方、网关、下游日志 |
| methodKey | 明确哪个 Feign 方法失败 |
| serviceId 和 instanceId | 确认请求打到哪台实例 |
| connect/read timeout 配置 | 判断是否等待过长或过短 |
| 下游 P95/P99 | 看是不是长尾请求 |
| 下游线程池和连接池 | 判断是否资源耗尽 |
| HTTP Client 连接池指标 | 判断是否等连接 |
| 熔断器状态 | 判断是否被快速失败 |
统一请求头和错误处理 Demo
透传 traceId
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignTraceConfig {
@Bean
public RequestInterceptor traceInterceptor() {
return template -> {
String traceId = TraceContext.getTraceId();
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
};
}
}这样跨服务排查时,Gateway、采集服务、字典服务、数据库日志能用同一个 traceId 串起来。
统一解析错误响应
import feign.Response;
import feign.codec.ErrorDecoder;
public class BizErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404) {
return new DownstreamNotFoundException("下游资源不存在: " + methodKey);
}
if (response.status() >= 500) {
return new DownstreamUnavailableException("下游服务异常: " + methodKey);
}
return new RuntimeException("Feign 调用失败, status=" + response.status());
}
}不要让下游 HTML 错误页、Nginx 默认错误页直接冒到业务层。业务层应收到可识别的异常或统一错误码。
商业落地清单
| 检查项 | 为什么 |
|---|---|
| 是否设置超时 | 防止线程无限等待 |
| 是否批量接口 | 防止循环远程调用 |
| 写接口是否幂等 | 防止重试导致重复写 |
| 是否透传 traceId | 方便跨服务排查 |
| 是否统一 ErrorDecoder | 避免错误语义混乱 |
| 是否有熔断降级 | 防止下游故障扩散 |
| 是否限制并发 | 防止调用方线程池被耗尽 |
| 是否记录耗时 | 发现慢下游和长尾请求 |
开发建议
- 每个下游服务单独定义 Client 接口,避免一个接口塞太多方法。
- 给所有远程调用设置连接超时和读取超时。
- 写操作慎用自动重试,必须保证幂等。
- 统一处理错误码和异常,不要让下游 HTML 错误页直接返回给业务。
- 调用链路中打印 traceId,方便排查跨服务问题。
Feign 通常会配合 Eureka 或 Nacos 获取服务地址,配合 Ribbon/LoadBalancer 做负载均衡,配合 Hystrix 或 Sentinel 做熔断降级。想把这些组件串成一次完整请求,可以继续看 服务调用链路。
面试标准回答已独立整理到OpenFeign独立面试题,完整源码推导和线上取证统一放在OpenFeign内部原理与生产治理,避免知识点与背诵答案混在一起。
