楼层: 首页/ 软件技术/ Spring Cloud 微服务/ 服务调用:OpenFeign
四

服务调用:OpenFeign

Declarative HTTP Client with OpenFeign

服务之间要互相调接口。用 RestTemplate 吧,拼 URL、设 header、转 JSON,一堆样板代码。OpenFeign 的思路是:你只定义一个 Java 接口,加几个注解,剩下的 HTTP 调用 Feign 帮你搞定——就像调用本地方法一样调远程服务。

论OpenFeign 解决了什么痛点

没有 Feign 时:订单服务要调用户服务,得自己写 restTemplate.getForObject("http://user-service/user/" + id, User.class),URL 拼错了、超时没设、JSON 转错了,全是坑。

有 Feign 时:定义一个 UserClient 接口,方法上加 @GetMapping("/user/{id}"),注入到订单服务里直接 userClient.getUser(1L) 调用。Feign 在运行时给你生成实现类,自动走负载均衡、自动序列化。

三步接入 OpenFeign

第一步:订单服务的 pom.xml 加依赖

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>

第二步:启动类加 @EnableFeignClients

@SpringBootApplication @EnableFeignClients // 打开 Feign 开关,扫描 @FeignClient 接口 public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }

第三步:写一个 Feign 接口,声明要调的远程服务

import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; // name = 要调用的服务名(Nacos 里注册的那个名字) // fallback = 降级类,后面讲 Sentinel 时再用 @FeignClient(name = "user-service", path = "/user") public interface UserClient { // GET /user/{id} —— @PathVariable 对应路径里的 {id} @GetMapping("/{id}") UserDTO getUserById(@PathVariable("id") Long id); // POST /user —— @RequestBody 把对象序列化成 JSON 发过去 @PostMapping UserDTO createUser(@RequestBody UserDTO user); // GET /user/search?keyword=xxx —— @RequestParam 对应 URL 参数 @GetMapping("/search") List<UserDTO> search(@RequestParam("keyword") String keyword); }

在订单服务里注入并使用,跟调本地方法一模一样

@RestController @RequestMapping("/order") public class OrderController { // Feign 会自动生成实现类,直接注入 @Autowired private UserClient userClient; @GetMapping("/{orderId}") public OrderDTO detail(@PathVariable Long orderId) { OrderDTO order = orderService.getById(orderId); // 远程调用户服务拿用户信息,写起来像本地调用 UserDTO user = userClient.getUserById(order.getUserId()); order.setUserName(user.getName()); return order; } }

超时配置与日志级别

Feign 默认超时很短(connect 2 秒、read 60 秒,具体以官方为准),调慢接口容易超时。日志级别默认 NONE,出问题时啥都看不到。

application.yml 里调超时和日志

feign: client: config: # default 表示对所有 FeignClient 生效,也可以写具体服务名 user-service default: connect-timeout: 3000 # 连接超时 3 秒 read-timeout: 5000 # 读取超时 5 秒 logger-level: FULL # NONE/BASIC/HEADERS/FULL,调试用 FULL

Feign 参数绑定与 LoadBalancer 的关系

Feign 接口上的注解,跟 Spring MVC 控制器上的注解长得几乎一样——因为 Feign 就是把这套注解"反过来用":MVC 是接收请求,Feign 是构造请求。对应关系如下表,写 Feign 接口时直接照着 MVC 那套习惯来就行。

注解作用对应 HTTP 部分
@PathVariable("id")路径里的变量/user/{id}
@RequestParam("kw")URL 查询参数?kw=xxx
@RequestBody请求体 JSONPOST/PUT 的 body
@RequestHeader("token")请求头Header 字段

Feign 调 lb://user-service 时,背后是 Spring Cloud LoadBalancer 在挑实例。老版本用 Ribbon,Ribbon 已停更,现在统一用 LoadBalancer。默认是轮询(round-robin),想换随机或权重,写个 ReactorLoadBalancer Bean 覆盖即可,学习阶段不用碰。

完整案例:Feign 接口 + fallback 降级类 + 超时日志

前面讲了怎么定义接口,这里把"超时、日志、降级"一次配齐。降级有两种写法:fallback(出问题直接调兜底类)和 fallbackFactory(能拿到异常原因,更灵活)。

第一步:写一个降级实现类,跟 Feign 接口同签名

// 这个类实现 UserClient 接口,每个方法都返回兜底数据 @Component public class UserClientFallback implements UserClient { @Override public UserDTO getUserById(Long id) { UserDTO dto = new UserDTO(); dto.setId(id); dto.setName("用户服务暂时不可用(降级)"); return dto; } @Override public UserDTO createUser(UserDTO user) { return new UserDTO(); } @Override public List<UserDTO> search(String keyword) { return Collections.emptyList(); } }

第二步:@FeignClient 挂上 fallback

@FeignClient(name = "user-service", path = "/user", fallback = UserClientFallback.class) // 调用失败走这个类 public interface UserClient { ... }

第三步:application.yml 配超时 + 全量日志(调试用)

feign: client: config: user-service: # 只对 user-service 这个客户端生效 connect-timeout: 2000 read-timeout: 5000 logger-level: FULL # 打印请求头/体/响应,线上别开 # 让 Feign 集成 Sentinel(熔断才能触发 fallback) sentinel: enabled: true

跑起来的输出对比

# 正常调用 user-service 在线: GET /order/detail/1 → {"orderId":1, "userName":"张三", "product":"机械键盘"} # 把 user-service 停掉,再调: GET /order/detail/1 → {"orderId":1, "userName":"用户服务暂时不可用(降级)"} # 全程不报错、不卡,前端拿到的是兜底文案 —— 这就是降级
Feign 超时默认 1 秒是历史遗留坑

老版本 Spring Cloud(Hoxton 之前)Feign 默认 read timeout 是 1 秒,调用稍微慢一点就 ReadTimeoutException。新版本默认值放宽了,但只要你调的是数据库慢查询、第三方接口,一定要显式配超时。另外,Feign 的超时会被 Ribbon/LoadBalancer 的超时覆盖——现在统一用 Spring Cloud LoadBalancer,配置项别找错地方。

本章面试题

面试快答 · OpenFeign

Q1. Feign 和 RestTemplate 比,好在哪?

参考答案

Feign 是声明式:定义接口 + 注解,运行时动态生成实现,自动序列化、自动负载均衡、自带超时/日志/降级配置;RestTemplate 要手写拼 URL、设 header、转 JSON。本质是把"远程调用"伪装成"本地方法调用"。

Q2. fallback 和 fallbackFactory 区别?

参考答案

fallback 只给一个兜底结果,拿不到异常原因;fallbackFactory 能拿到 Throwable,知道是超时、限流还是业务异常,可以区别处理(比如超时返回"稍后重试"、熔断返回"维护中")。

Q3. Feign 是同步还是异步?底层用什么?

参考答案

默认同步阻塞,底层默认 HttpURLConnection,可换 Apache HttpClient/OkHttp。要响应式流式,用 WebClient。别和 Gateway 的 WebFlux 混——Feign 走 Tomcat 线程模型。