CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Spring Cloud 微服务入门/课时/OpenFeign 服务调用
RawBlame
课时目录10 节

Spring Cloud 微服务入门

  1. 01微服务与 Spring Cloud
  2. 02架构模式与术语
  3. 03服务注册与 Eureka
  4. 04Spring Cloud Gateway
  5. 05Cloud Config 配置中心
  6. 06OpenFeign 服务调用
  7. 07熔断与容错
  8. 08Spring Data 扩展
  9. 09Micrometer 与可观测性
  10. 10综合练习与部署直觉

OpenFeign 服务调用

声明式 HTTP 客户端、负载均衡。

35 分钟

OpenFeign 服务调用

这一课你将能做到

  1. 在 user-service 里声明一个 Feign 接口,像调本地方法一样调用 problem-service 的 HTTP API
  2. 配置全局与单个 Client 的超时,并通过 RequestInterceptor 把登录态透传给下游
  3. 说清 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、状态码、耗时,排查「调没调到、调了多久」最直观。

讲清楚要点

  1. @FeignClient(name) 的值就是注册中心里的服务名:Feign 把它交给 LoadBalancer 查实例列表;没有注册中心时也可写 url = "http://..." 直连(仅测试用)。
  2. 动态代理是核心原理:启动时 Feign 为接口生成代理对象并注册为 Bean;调用方法时代理把注解元数据翻译成 HTTP 请求模板,编码器序列化参数,发出请求后解码器反序列化返回值。
  3. 负载均衡在客户端:Spring Cloud LoadBalancer 默认轮询(Round Robin);Eureka 给列表,LoadBalancer 挑一个——「发现」与「均衡」分层解耦。
  4. 超时两层都要懂:connectTimeout(建连)+ readTimeout(等响应),可在 default 全局、单个服务名下、@FeignClient(configuration=...) 三层覆盖,粒度越小优先级越高。
  5. 错误传播:下游非 2xx 会抛 FeignException,状态码和响应体都在异常里;配合 ErrorDecoder 可把下游错误翻译成本服务更语义化的异常。
  6. 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 爆炸下游提供批量接口,一次拉回

卡点急救

  1. 报 No Feign Client for loadBalancing defined 或找不到负载均衡:缺 spring-cloud-starter-loadbalancer,或 name 拼写与注册中心的服务名(大小写不敏感但中划线要对)不一致。
  2. 调用报 404:Feign 接口路径与目标服务实际路径不一致(别忘了目标服务前面有没有 context-path);开 DEBUG 日志看实际请求 URL 逐字符对比。
  3. 启动报 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,配合指数退避;写操作不盲目重试,用幂等键。三者与熔断组合:重试解决瞬时抖动,熔断防持续故障,超时是两者的安全底线。

自测三问

  1. @FeignClient(name = "problem-service") 发起调用时,name 在哪几步被用到?
  2. Feign 的超时可以在哪几层配置?优先级如何?
  3. 为什么 Feign 接口里 @PathVariable 必须写参数名?

小结一句话

OpenFeign 用「接口 + 注解」声明远程调用,动态代理在运行时完成服务名寻址、负载均衡、HTTP 编解码,让跨服务调用长得像本地方法。

延伸

  • Spring Cloud OpenFeign 官方文档
  • 下一课:熔断与容错——下游挂了,你的服务怎么不跟着挂。
加载本节练习…
← 上一课Cloud Config 配置中心下一课 →熔断与容错