概述
上一篇用 RestTemplate + @LoadBalanced 打通了 order-service 到 user-service 的远程调用,能用,但写法笨重。本文用 Feign 把远程调用改造成"调本地方法"的体验,四步完成改造,并顺手把底层原理和常见的启动坑讲清楚。
纲要
- RestTemplate 的问题
- URL 硬编码、字符串拼接易错
- 复杂参数难以维护
- 编程体验不统一(写的是 HTTP 请求,不是业务方法)
- Feign 是什么
- 声明式 HTTP 客户端,类比声明式事务
- 一个 HTTP 请求的五要素:服务名称、请求方式、请求路径、请求参数、返回值类型
- 四步改造 order-service
- 引入
spring-cloud-starter-openfeign依赖 - 启动类加
@EnableFeignClients - 编写
UserClient接口(@FeignClient+ SpringMVC 注解) OrderService注入接口直接调用
- 引入
- 改造前后对比:RestTemplate 写法 vs Feign 写法
- 底层原理:动态代理 + 集成 Ribbon 负载均衡
- 实战避坑:注解扫描不到、参数注解缺失、服务名大小写、返回类型不一致
RestTemplate 到底差在哪
先看改造前的代码,这是 order-service 里查询订单时远程查询用户的逻辑:
java
// 2.利用RestTemplate发起http请求,查询用户
// 2.1.url路径:服务名写死在字符串里,参数靠手工拼接
String url = "http://userservice/user/" + order.getUserId();
// 2.2.发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
这段代码已经是基于 Ribbon 做过优化的版本了------URL 里写的是服务名 userservice 而不是 IP + 端口,负载均衡已经生效。但它依然有三个硬伤:
| 问题 | 具体表现 | 后果 |
|---|---|---|
| 可读性差 | 一段代码里混着 URL、请求方式、参数拼接、返回类型转换 | 没接触过远程调用的人第一眼看不懂 |
| 参数拼接易错 | 路径参数靠 + 手工拼接 |
参数一多就乱,拼错路径只会在运行时报 404 |
| 编程体验不统一 | 业务代码里到处写的是"怎么发请求",而不是"要做什么" | 正常写业务都是调方法,这里突然冒出一个 URL 字符串 |
参数复杂时问题会被放大。回想一下在浏览器里访问 Nacos 控制台、或者用百度搜索时地址栏里那一长串参数------七八个参数拼在 Java 字符串里维护,将来参数一变,改代码就是灾难。
Feign:把发请求的五个信息"声明"出来
Feign 是一个声明式的 HTTP 客户端。
"声明式"这个概念在 Spring 声明式事务里已经见过:早期手动开事务、提交事务、回滚,后来只需要告诉 Spring 规则,剩下的事框架做。Feign 同理------你把发 HTTP 请求所需要的信息声明出来,请求本身由 Feign 帮你发。
发一个 HTTP 请求,恰好需要五个信息:
- 服务名称(发给谁)
- 请求方式(GET / POST)
- 请求路径
- 请求参数
- 返回值类型
Feign 的做法是:定义一个接口,把这五个信息全部用注解声明在接口上,运行时由 Feign 生成实现并发请求。声明完之后,业务代码里只剩下"调接口的方法"这一件事。
四步改造 order-service
改造只动 order-service 这个消费方,user-service 作为提供方一行不改。改造后的 order-service 结构:
tree
order-service
├── pom.xml # 第一步:在这里加 feign 依赖
└── src/main/java/cn/itcast/order
├── OrderApplication.java # 第二步:加 @EnableFeignClients
├── client
│ └── UserClient.java # 第三步:Feign 客户端接口
├── controller
│ └── OrderController.java
├── mapper
│ └── OrderMapper.java
├── pojo
│ └── Order.java
└── service
└── OrderService.java # 第四步:注入 UserClient 调用
引入依赖
在 order-service 的 pom.xml 中添加 openfeign 起步依赖。artifactId 是 spring-cloud-starter-openfeign,注意别写成老版本的 spring-cloud-starter-feign:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
starter 意味着自动装配,Feign 运行所需的各种组件由 Spring Boot 帮我们配好。
启动类加 @EnableFeignClients
@EnableFeignClients 是 Feign 功能的总开关,不加它,接口声明得再规范也不会生效。这一步改的就是启动类这一个注解:
java
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients // 开启Feign功能,默认扫描启动类所在包及其子包中的@FeignClient接口
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate并注入Spring容器
* 改用Feign后这个Bean可以删掉,这里暂时保留用于对比
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@EnableFeignClients 默认扫描启动类所在包及子包。UserClient 放在 cn.itcast.order.client,在扫描范围内,什么都不用配。如果客户端接口放在别的包,就要显式指定 basePackages 或 clients 属性------这是后面避坑清单里的第一名。
编写 UserClient 接口
新建一个接口,封装所有对 userservice 服务的远程调用:
java
package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
仔细看这个接口,全是 SpringMVC 的注解,没有任何新东西------这正是 Feign 降低学习成本的设计,它默认采用 SpringMVC 的注解来声明调用信息。五个要素对应关系如下:
| 声明位置 | 代码 | 对应要素 |
|---|---|---|
类上 @FeignClient("userservice") |
"userservice" |
服务名称(注册中心里的服务名,不是 IP 地址) |
方法上 @GetMapping("/user/{id}") |
@GetMapping |
请求方式 GET |
| 同上 | "/user/{id}" |
请求路径,{id} 是路径占位符 |
方法参数 @PathVariable("id") Long id |
Long id |
请求参数 |
| 方法返回值 | User |
返回值类型 |
写这个接口时对着提供方的 UserController 抄即可,两边的方法签名和注解必须保持一致。user-service 里的接口长这样:
java
package cn.itcast.user.web;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
类上的 @RequestMapping("/user") 加方法上的 @GetMapping("/{id}"),合并起来就是 /user/{id}------这正是 UserClient 里声明的路径。
有一个关键认知:@FeignClient 的 value 是服务名,对应 nacos/eureka 注册中心里注册的服务名。Feign 拿到服务名后自己去注册中心拉实例列表,你永远不需要在代码里写 IP 和端口。
OrderService 注入 UserClient 调用
最后一步,把原来 RestTemplate 的代码整段删掉,注入 UserClient,直接调方法:
java
package cn.itcast.order.service;
import cn.itcast.order.client.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用,查用户
User user = userClient.findById(order.getUserId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
启动 order-service,浏览器访问 http://localhost:8080/order/101 并多刷新几次,每次都返回完整订单数据。观察 user-service 的 8081、8082 两个实例的日志,会发现两个实例都被访问到了------Feign 不仅完成了远程调用,负载均衡也在生效。
改造前后对比
同一个"查用户"动作,两种写法放在一起看:
改造前(RestTemplate):
java
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
改造后(Feign):
java
User user = userClient.findById(order.getUserId());
| 维度 | RestTemplate | Feign |
|---|---|---|
| 调用风格 | 拼 URL 字符串发请求 | 调接口方法 |
| URL 维护 | 硬编码在业务代码里 | 声明在客户端接口上,集中管理 |
| 参数处理 | 手工字符串拼接,多个参数极易出错 | 方法参数 + 注解,几个参数写几个形参 |
| 负载均衡 | 需要 @LoadBalanced 手动开启 |
内部集成 Ribbon,自动生效 |
| 可读性 | 不看注释不知道在干什么 | 不说明都以为是本地方法调用 |
| 复杂 URL | 参数七八个时基本没法维护 | 方法列表里加形参即可 |
将来遇到参数非常多的接口,Feign 的应对方式很朴素:方法列表里多加几个参数,每个参数配好 @RequestParam 或 @PathVariable,维护成本恒定。
Feign 底层是怎么工作的
你写的只是一个接口,没有实现类,那调用方法时发生了什么?答案是动态代理 :Feign 在启动时为每个 @FeignClient 接口生成代理对象注入容器,调用方法时,代理对象把注解里声明的信息组装成一个 HTTP 请求发出去。
user-service 注册中心(Nacos) Ribbon负载均衡 UserClient(动态代理对象) OrderService user-service 注册中心(Nacos) Ribbon负载均衡 UserClient(动态代理对象) OrderService #mermaid-svg-gTPLmEFp5u1xPl9f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gTPLmEFp5u1xPl9f .error-icon{fill:#552222;}#mermaid-svg-gTPLmEFp5u1xPl9f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gTPLmEFp5u1xPl9f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gTPLmEFp5u1xPl9f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gTPLmEFp5u1xPl9f .marker.cross{stroke:#333333;}#mermaid-svg-gTPLmEFp5u1xPl9f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gTPLmEFp5u1xPl9f p{margin:0;}#mermaid-svg-gTPLmEFp5u1xPl9f .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gTPLmEFp5u1xPl9f text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gTPLmEFp5u1xPl9f .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gTPLmEFp5u1xPl9f .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gTPLmEFp5u1xPl9f #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gTPLmEFp5u1xPl9f .sequenceNumber{fill:white;}#mermaid-svg-gTPLmEFp5u1xPl9f #sequencenumber{fill:#333;}#mermaid-svg-gTPLmEFp5u1xPl9f #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gTPLmEFp5u1xPl9f .messageText{fill:#333;stroke:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gTPLmEFp5u1xPl9f .labelText,#mermaid-svg-gTPLmEFp5u1xPl9f .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .loopText,#mermaid-svg-gTPLmEFp5u1xPl9f .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gTPLmEFp5u1xPl9f .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gTPLmEFp5u1xPl9f .noteText,#mermaid-svg-gTPLmEFp5u1xPl9f .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gTPLmEFp5u1xPl9f .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gTPLmEFp5u1xPl9f .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gTPLmEFp5u1xPl9f .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gTPLmEFp5u1xPl9f .actorPopupMenu{position:absolute;}#mermaid-svg-gTPLmEFp5u1xPl9f .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gTPLmEFp5u1xPl9f .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gTPLmEFp5u1xPl9f .actor-man circle,#mermaid-svg-gTPLmEFp5u1xPl9f line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gTPLmEFp5u1xPl9f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} findById(101L) 看似调用本地方法 解析@FeignClient/@GetMapping注解 组装请求: GET /user/101 请求目标: 服务名 userservice 拉取 userservice 实例列表 8081, 8082 按负载均衡策略选出一个实例 HTTP GET http://192.168.x.x:8082/user/101 返回 JSON 响应体 反序列化为 User 对象
整个过程可以概括成一条链路:
#mermaid-svg-aHdoYp42H1y9N8Ih{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aHdoYp42H1y9N8Ih .error-icon{fill:#552222;}#mermaid-svg-aHdoYp42H1y9N8Ih .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aHdoYp42H1y9N8Ih .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aHdoYp42H1y9N8Ih .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aHdoYp42H1y9N8Ih .marker.cross{stroke:#333333;}#mermaid-svg-aHdoYp42H1y9N8Ih svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aHdoYp42H1y9N8Ih p{margin:0;}#mermaid-svg-aHdoYp42H1y9N8Ih .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster-label text{fill:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster-label span{color:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster-label span p{background-color:transparent;}#mermaid-svg-aHdoYp42H1y9N8Ih .label text,#mermaid-svg-aHdoYp42H1y9N8Ih span{fill:#333;color:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih .node rect,#mermaid-svg-aHdoYp42H1y9N8Ih .node circle,#mermaid-svg-aHdoYp42H1y9N8Ih .node ellipse,#mermaid-svg-aHdoYp42H1y9N8Ih .node polygon,#mermaid-svg-aHdoYp42H1y9N8Ih .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aHdoYp42H1y9N8Ih .rough-node .label text,#mermaid-svg-aHdoYp42H1y9N8Ih .node .label text,#mermaid-svg-aHdoYp42H1y9N8Ih .image-shape .label,#mermaid-svg-aHdoYp42H1y9N8Ih .icon-shape .label{text-anchor:middle;}#mermaid-svg-aHdoYp42H1y9N8Ih .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aHdoYp42H1y9N8Ih .rough-node .label,#mermaid-svg-aHdoYp42H1y9N8Ih .node .label,#mermaid-svg-aHdoYp42H1y9N8Ih .image-shape .label,#mermaid-svg-aHdoYp42H1y9N8Ih .icon-shape .label{text-align:center;}#mermaid-svg-aHdoYp42H1y9N8Ih .node.clickable{cursor:pointer;}#mermaid-svg-aHdoYp42H1y9N8Ih .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aHdoYp42H1y9N8Ih .arrowheadPath{fill:#333333;}#mermaid-svg-aHdoYp42H1y9N8Ih .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aHdoYp42H1y9N8Ih .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aHdoYp42H1y9N8Ih .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aHdoYp42H1y9N8Ih .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aHdoYp42H1y9N8Ih .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aHdoYp42H1y9N8Ih .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster text{fill:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih .cluster span{color:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aHdoYp42H1y9N8Ih .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aHdoYp42H1y9N8Ih rect.text{fill:none;stroke-width:0;}#mermaid-svg-aHdoYp42H1y9N8Ih .icon-shape,#mermaid-svg-aHdoYp42H1y9N8Ih .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aHdoYp42H1y9N8Ih .icon-shape p,#mermaid-svg-aHdoYp42H1y9N8Ih .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aHdoYp42H1y9N8Ih .icon-shape .label rect,#mermaid-svg-aHdoYp42H1y9N8Ih .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aHdoYp42H1y9N8Ih .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aHdoYp42H1y9N8Ih .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aHdoYp42H1y9N8Ih :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OrderService 调方法
UserClient 动态代理
解析注解生成 HTTP 请求
按服务名从注册中心拉取实例
Ribbon 负载均衡选实例
发起 HTTP 调用 user-service
所以 Feign 并不是什么黑魔法,它的本质就是 RestTemplate/OkHttp 这类 HTTP 客户端 + 负载均衡的一层封装 ,只是把这层封装藏到了动态代理背后。打开 Feign 的核心依赖树能看到 feign-core,其内部已经带上了 Ribbon,负载均衡不用你操心。
实战避坑
这几个坑在真实项目里出现频率极高,改造时提前避开:
- 启动类忘加
@EnableFeignClients:最常见的启动坑。现象是注入UserClient时报Field userClient required a bean,找不到 Feign 客户端的 Bean。检查启动类注解即可。 - 客户端接口不在扫描范围内 :
@EnableFeignClients默认只扫启动类所在包及子包。如果UserClient放在cn.itcast.feign.clients这类外部包里,必须在注解上显式指定,两种写法二选一:
java
// 写法一:指定扫描包
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
// 写法二:直接指定接口类
@EnableFeignClients(clients = {UserClient.class})
- 方法参数漏写注解 :Feign 方法有多个参数时,
@RequestParam("xxx")、@PathVariable("xxx")一个都不能省,且要写明参数名。漏写后 Feign 无法确定参数该放 query、path 还是 body,多参数场景会冲突甚至直接把参数塞进请求体导致提供方收不到。 - 服务名大小写与拼写 :
@FeignClient("userService")与注册中心里的userservice对不上,启动不报错,一调用就报No instances available。服务名以注册中心列表里显示的为准。 - 返回类型与提供方不一致 :提供方返回
User,客户端方法却声明成Order,反序列化字段全为 null 或者直接抛解析异常。排查时先对齐两边的方法签名。
API 速览
| 注解 / 组件 | 位置 | 作用 |
|---|---|---|
@EnableFeignClients |
启动类 | 开启 Feign 功能,扫描 @FeignClient 接口;basePackages/clients 指定扫描范围 |
@FeignClient("服务名") |
接口上 | 声明这是 Feign 客户端,value 填注册中心里的服务名 |
@GetMapping / @PostMapping |
接口方法上 | 声明请求方式与请求路径,与 SpringMVC 注解通用 |
@PathVariable("x") |
方法参数 | 路径占位符参数,必须写参数名 |
@RequestParam("x") |
方法参数 | query 参数,必须写参数名 |
@LoadBalanced |
RestTemplate 的 Bean | RestTemplate 方案下开启负载均衡;Feign 内部已集成,无需再配 |
官方文档
总结
- RestTemplate 的三个问题:URL 硬编码拼接、复杂参数难维护、编程体验不统一。
- Feign 是声明式 HTTP 客户端:把服务名称、请求方式、请求路径、请求参数、返回值类型五个信息用注解声明在接口上,请求由框架发送。
- 改造四步:引依赖
spring-cloud-starter-openfeign→ 启动类加@EnableFeignClients→ 编写UserClient接口 → 业务代码注入接口调方法。 - 客户端接口全部使用 SpringMVC 注解,照着提供方的 Controller 抄即可,
@FeignClient的 value 是服务名不是地址。 - 底层没有黑魔法:动态代理生成实现,内部集成 Ribbon 自动负载均衡,本质是 HTTP 客户端 + 负载均衡的封装。
- 排查口诀:启动报找不到 Bean 查
@EnableFeignClients;调用报找不到实例查服务名拼写;参数收不到查@RequestParam/@PathVariable是否写全。