本课目标:把"用 Feign 做服务间调用"这件事讲透——Feign 是什么、为什么用它、
@FeignClient注解怎么写、参数怎么传、怎么注入调用。这是最基础的常规做法;下一课再讲企业里更进阶的 client-sdk 封装。
一、痛点:服务拆开后,它们怎么互相调用?
微服务把系统拆成一堆独立服务后,一个绕不开的现实问题是:服务之间经常要互相协作。
比如"下单"时,订单服务需要去问商品服务:"这个商品还有库存吗?"
在单体应用里,这就是一次普通的方法调用。但现在两个服务跑在不同的进程、不同的机器上,订单服务只能通过 HTTP 网络请求去调商品服务。
如果手写这个调用,你得做一堆事:
// 传统做法:手写 HTTP(伪代码),很啰嗦
String url = "http://product-service:8087/api/product/123";
HttpResponse resp = httpClient.get(url); // 自己拼URL、发请求
String json = resp.getBody(); // 拿到JSON字符串
ProductDTO product = objectMapper.readValue(json, ProductDTO.class); // 自己反序列化
每调一个接口都要:拼 URL、发请求、处理响应、反序列化、处理异常…… 又啰嗦又容易错。有没有办法像调本地方法一样调远程服务?有,这就是 Feign。
二、Feign 是什么
Feign 是一个"声明式"的 HTTP 客户端:你只要写一个接口、加几个注解,声明"我要调哪个服务的哪个接口",Feign 就在底层帮你把它变成一次 HTTP 请求。你调接口方法,就像调本地方法一样,完全不用自己写网络代码。
一句话:Feign = 让"调别的服务"看起来像"调本地方法"的翻译官。
📌 名词澄清:我们用的是 Spring Cloud 的 OpenFeign。"OpenFeign"是这个库的名字;真正写代码时用的注解是
@FeignClient(声明客户端)和@EnableFeignClients(启用)。
三、三步接入 Feign
第一步:加依赖
在要发起调用的服务(消费方,比如订单服务)的 pom.xml 里加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:启动类开启 Feign
@SpringBootApplication
@EnableFeignClients // 启用 Feign:扫描并创建所有 @FeignClient 接口的代理
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
@EnableFeignClients会扫描@FeignClient注解的接口,**自动为每个接口生成一个实现(动态代理)**并放进 Spring 容器,你直接注入就能用。
第三步:写一个 @FeignClient 接口
这是核心。声明"我要调商品服务的哪些接口":
@FeignClient(
name = "product-service", // 服务名(标识,也用于服务发现)
url = "http://localhost:8087", // 目标地址(基础版写死;接Nacos可省略)
path = "/api/product" // 该客户端所有接口的统一前缀
)
public interface ProductClient {
/** 查询商品详情 —— 对应商品服务的 GET /api/product/{id} */
@GetMapping("/{id}")
ProductDTO getProduct(@PathVariable("id") Long id);
/** 扣减库存 —— 对应商品服务的 POST /api/product/deduct-stock */
@PostMapping("/deduct-stock")
Boolean deductStock(@RequestBody DeductStockReq req);
}
就这么简单——没有一行网络代码,全靠注解声明。
四、@FeignClient 接口怎么写(详解)
4.1 @FeignClient 的几个关键属性
| 属性 | 作用 | 例子 |
|---|---|---|
name | 服务名(必填)。多个 client 调同一服务时还要配 contextId 区分 | "product-service" |
url | 目标地址。基础版写死;接入 Nacos 后可不写,用服务名自动发现 | "http://localhost:8087" |
path | 该接口下所有方法的统一路径前缀 | "/api/product" |
4.2 方法 = 提供方的一个接口
写 Feign 接口的诀窍:照着"提供方的 Controller"抄。 提供方接口长什么样,Feign 方法就声明成什么样——路径、方法、参数一一对应:
4.3 参数怎么传
Feign 用 Spring MVC 的注解来描述参数怎么放进 HTTP 请求:
| 注解 | 参数放在哪 | 用于 |
|---|---|---|
@PathVariable | URL 路径里 | /product/{id} 这种 |
@RequestParam | URL 问号后的查询参数 | ?name=xxx&page=1 |
@RequestBody | 请求体(JSON) | POST/PUT 提交对象 |
@RequestHeader | 请求头 | 传 token、租户ID 等 |
// 各种参数写法示例
@GetMapping("/{id}")
ProductDTO getProduct(@PathVariable("id") Long id); // 路径参数
@GetMapping("/search")
List<ProductDTO> search(@RequestParam("name") String name, // 查询参数
@RequestParam("page") Integer page);
@PostMapping("/create")
ProductDTO create(@RequestBody CreateProductReq req); // 请求体
@GetMapping("/mine")
ProductDTO mine(@RequestHeader("X-User-Id") Long userId); // 请求头
⚠️ 新手坑:Feign 接口里的
@GetMapping/@RequestParam等注解,必须显式写出名字(如@PathVariable("id")、@RequestParam("name")),不能省,否则运行时可能报参数名解析失败。
五、像调本地方法一样使用
把 Feign 接口当成一个普通 Bean 注入,直接调用:
@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductClient productClient; // 直接注入 Feign 接口
public void createOrder(Long productId, int qty) {
// 像调本地方法一样调商品服务!
ProductDTO product = productClient.getProduct(productId);
if (product.getStock() < qty) {
throw new BusinessException("库存不足");
}
productClient.deductStock(new DeductStockReq(productId, qty));
// ... 继续下单逻辑
}
}
看,
productClient.getProduct(productId)读起来和调本地方法一模一样,但底层是一次跨服务的 HTTP 调用。这就是声明式调用的优雅之处。
六、调用全过程
七、Feign 怎么找到目标服务?——服务发现 + 负载均衡
先回到第三步那个写死的地址:url = "http://localhost:8087"。本地 demo 没问题,但生产环境根本行不通:
- 商品服务为了扛流量,往往部署了好几台(集群),到底该调哪一台?
- 服务器重启、扩容、容器漂移后 IP 会变,写死的地址立刻失效。
所以生产里 @FeignClient 不写 url,只写服务名:
@FeignClient(name = "product-service", path = "/api/product") // 注意:没有 url 了
public interface ProductClient { ... }
那 Feign 凭一个"服务名"怎么找到真实 IP?靠 注册中心 + 负载均衡 两件事配合。
7.1 注册中心:服务的"通讯录"
每个服务启动时,把自己的 IP + 端口 登记到注册中心(如 Nacos);Feign 调用时,拿服务名去注册中心查出这个服务当前所有可用实例的地址列表。
这就是上一课《微服务架构》里讲过的"服务注册与发现"——注册中心就像一个实时更新的通讯录。
7.2 负载均衡:从多个实例里挑一个
查到的往往是一串地址(商品服务有 3 台),到底发给哪台?这一步"从实例列表里挑一个"就是负载均衡,由 Spring Cloud LoadBalancer 完成(默认轮询)。
完整链路:Feign 拿服务名 → LoadBalancer 去 Nacos 查实例列表 → 挑一个实例 → 真正发 HTTP 请求。 整个过程业务代码完全无感,依然是 productClient.getProduct(123) 一行。
7.3 历史演进:Ribbon → Spring Cloud LoadBalancer
早期 Spring Cloud 的负载均衡用的是 Netflix Ribbon。后来 Ribbon 停止维护,Spring Cloud 用官方自研的 Spring Cloud LoadBalancer 取而代之。新项目一律用 Spring Cloud LoadBalancer,Ribbon 只在老项目里还能见到。
接入时依赖大致如下(了解即可,完整接入是注册中心课程的内容):
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
本节记住一句:
@FeignClient写服务名而非 IP,靠"注册中心查地址 + LoadBalancer 挑实例"完成寻址和负载均衡——这才是生产里 Feign 的真实工作方式。
八、几个实用配置(够用就好)
# application.yml —— Feign 常用配置
feign:
client:
config:
default: # default = 对所有 Feign 客户端生效
connect-timeout: 2000 # 连接超时(ms)
read-timeout: 5000 # 读取超时(ms)
# 也可针对单个客户端名(如 product-service)单独配
logging:
level:
com.xiangmu.order.client: DEBUG # 把 Feign 接口包设为 DEBUG 可打印请求/响应,方便排错
📌 提示:示例里的
url是本地调试写法。生产中改用"服务名 + 服务发现 + 负载均衡"(见第七节),更完整的 Nacos 接入是后面注册中心课程的内容。
九、常规做法的局限(引出下一课)
到这里,"用 Feign 做服务间调用"你已经会了。但在多服务、多团队的真实企业项目里,这种"各调各的"做法会暴露几个问题:
| 问题 | 说明 |
|---|---|
| 重复定义 | 每个调商品服务的人,都自己写一遍 @FeignClient 接口和 DTO |
| DTO 容易对不上 | 各自定义返回对象,商品服务改了字段,调用方未必同步 |
| 响应解包重复 | 真实返回往往是 ApiResponse<T>,每个调用方都要自己解一层 |
| 降级容错重复 | 商品服务挂了怎么办?每个调用方都得自己写 try-catch / 兜底 |
这些重复和不一致,正是下一课要解决的——把"怎么调我"由服务提供方统一封装成一个
client-sdk,别人依赖即用。那才是企业里的最佳实践。
十、小结
六句话带走:
- Feign 是声明式 HTTP 客户端:写接口 + 注解,像调本地方法一样调远程服务。
- 三步接入:加 openfeign 依赖 → 启动类
@EnableFeignClients→ 写@FeignClient接口。 - 接口照着提供方 Controller 抄:路径、方法、参数一一对应,参数名要显式写。
- 注入即用:把 Feign 接口当普通 Bean 注入,直接调方法。
- 生产里靠服务名寻址:
@FeignClient写服务名,靠注册中心查实例 + Spring Cloud LoadBalancer(早期 Ribbon)做负载均衡。 - 常规做法有重复和不一致问题 → 下一课用
client-sdk封装解决。