Lecture05-SpringCloud-OpenFeign服务调用

文章目录

    [toc]

    单词词性音标含义
    declarativeadj./dɪˈklærətɪv/声明式的(只声明"调哪个接口",不用手写 HTTP 细节)
    clientn./ˈklaɪənt/客户端(这里指 OpenFeign 生成的远程调用代理)
    timeoutn./ˈtaɪmaʊt/超时(调用超过设定时间没响应就放弃)
    retryv./n./ˈriːtraɪ/重试(调用失败后再次尝试)
    circuit breakern./ˈsɜːkɪt ˈbreɪkə(r)/熔断器(连续失败后快速打开开关,保护下游)
    fallbackn./ˈfɔːlbæk/降级(调用失败时执行的兜底逻辑,返回默认值而非抛异常)
    interfacen./ˈɪntəfeɪs/接口(Java 接口,OpenFeign 用它声明远程调用)

    Lecture05-SpringCloud-OpenFeign服务调用

    目标:用 OpenFeign 把"服务间调用"变成"调用一个本地接口",并配置超时、重试、日志与服务降级(Fallback),替代第 03 讲的手写 HTTP 代码。

    1、为什么用 OpenFeign

    第 03 讲我们手写了"发现实例 + 拼 URL + RestTemplate 调用",问题:

    • 模板代码多:每个接口都要重复写。
    • 不参与负载均衡:每次都取第一个实例。
    • 不好维护:远程接口一变,调用处要跟着改。

    OpenFeign 的做法:定义一个 Java 接口 + 注解,声明"我要调用哪个服务的哪个路径",框架自动生成调用代理,还默认接入负载均衡。

    feign-call


    2、接入 OpenFeign

    1、加依赖

    <!-- order-service/pom.xml -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- 为什么需要 loadbalancer? -->
    <!-- 因为 OpenFeign 底层根据微服务名称查找对应微服务实例信息,这部分功能实际上是 loadbalancer 在底层做的 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    

    2、启用 Feign

    在启动类上加 @EnableFeignClients:

    @SpringBootApplication
    @EnableFeignClients            // 扫描并生成所有 @FeignClient 代理
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        }
    }
    

    3、定义远程调用接口

    注意:必须准确设置 provider 的微服务名称!因为这个名称就是 OpenFeign 底层获取 provider 微服务实例信息的依据!OpenFeign 底层会根据微服务名称到 Nacos 注册中心查找对应实例信息

    // 声明:调用名为 user-service 的服务
    @FeignClient(name = "user-service")
    public interface UserClient {
    
        // 对应 user-service 的 GET /user/{id}
        @GetMapping("/user/{id}")
        String getUser(@PathVariable("id") Long id);
    }
    

    4、业务里直接"注入调用"

    @RestController
    public class OrderController {
    
        @Autowired
        private UserClient userClient;     // 注入 Feign 代理
    
        @GetMapping("/order/user/{id}")
        public String getOrderUser(@PathVariable Long id) {
            // 像调用本地方法一样调用远程服务,无需手写 HTTP
            return userClient.getUser(id);
        }
    }
    

    底层:Feign 自动从 Nacos 发现 user-service 的所有实例,配合 LoadBalancer 选一个,发出真实 HTTP 请求。


    3、参数与返回值

    Feign 用 Spring MVC 注解声明远程接口的参数:

    @FeignClient(name = "user-service")
    public interface UserClient {
    
        // GET + 路径参数
        @GetMapping("/user/{id}")
        User getUser(@PathVariable("id") Long id);
    
        // GET + 查询参数
        @GetMapping("/user/list")
        List<User> listUsers(@RequestParam("page") int page,
                             @RequestParam("size") int size);
    
        // POST + 请求体
        @PostMapping("/user")
        User createUser(@RequestBody User user);
    
        // 请求头(如携带 token)
        @GetMapping("/user/info")
        User getInfo(@RequestHeader("Authorization") String token);
    }
    

    远程接口的返回类型要能反序列化:复杂对象请让两端共用一个 DTO 类(可放独立模块),字段名一致即可。


    4、超时与重试配置

    远程调用必须有超时保护,否则下游卡住会拖垮调用方。

    spring:
      cloud:
        openfeign:
          client:
            config:
              default:                  # 对所有 FeignClient 生效;也可写服务名
                connect-timeout: 2000   # 连接超时(毫秒)
                read-timeout: 3000      # 读取超时(毫秒)
          httpclient:
            enabled: true               # 用 Apache HttpClient(比默认更稳)
    

    提示:LoadBalancer 自带重试能力(第 06 讲配置);调用失败后的兜底处理见下一节"服务降级",熔断器原理在 L09(Sentinel)概念课展开。


    5、服务降级:Fallback

    1、为什么需要降级

    超时和重试管的是"这一次调用怎么办"。可一旦下游彻底挂了(服务没启动、网络不通、一直超时),调用方不能跟着崩——用户点下单,不能因为用户服务挂了就返回一个 500 报错页。

    **降级(Fallback)**就是在消费端给自己准备一条"退路":失败时不抛异常,而是执行一段兜底逻辑,返回一个"凑合能用"的结果。

    正常:order-service → user-service → 返回真实用户信息
    糟糕:order-service → user-service(挂了)→ 连带 order-service 崩溃 → 返回 500 错误信息
    降级:order-service → user-service(挂了)→ 走兜底方法 → 返回默认值
    

    降级的好处:

    • order-service 能够得到一个和正常情况相同类型的数据、对象,不至于因为调用 user-service 失败而崩溃,order-service 这边代码可以继续执行
    • order-service 可以给用户返回一个不太糟糕的响应结果,最起码不是抛异常或 500 这样的错误

    2、加依赖 + 打开开关

    Feign 自己不做熔断降级,要借助 Spring Cloud CircuitBreaker(本课用 Resilience4j 实现):

    <!-- order-service/pom.xml -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
    </dependency>
    
    spring:
      cloud:
        openfeign:
          circuitbreaker:
            enabled: true      # 关键开关:不打开的话 fallback 不生效(默认 false)
    

    ⚠️ 最容易踩的坑:spring.cloud.openfeign.circuitbreaker.enabled 默认是 false。不打开它,@FeignClient(fallback = ...) 会被静默忽略——不报错、不生效,调用失败照样抛异常,白白排查半天。

    3、写降级类

    降级类实现 Feign 接口,并声明成 Spring Bean:

    @Component                        // 必须注册成 Bean
    public class UserClientFallback implements UserClient {
    
        @Override
        public String getUser(Long id) {
            // 兜底逻辑:返回安全的默认值
            return "用户服务暂时不可用,请稍后重试";
        }
    }
    

    注意:降级方法里不要再抛异常,也不要再去调别的远程服务——它本身就是最后一道防线。

    4、挂到 Feign 接口上

    @FeignClient(name = "user-service", fallback = UserClientFallback.class)
    public interface UserClient {
    
        @GetMapping("/user/{id}")
        String getUser(@PathVariable("id") Long id);
    }
    

    5、验证降级效果

    1. 启动 order-service,故意不启动 user-service;
    2. 访问 http://localhost:8083/order/user/1;
    3. 期望:不出现 500 报错页,而是返回兜底文案"用户服务暂时不可用,请稍后重试"。

    对比实验:把 fallback 属性删掉重启,同样的访问会直接抛异常(500)。这就是降级省下来的东西。

    6、想知道"为什么失败"?用 fallbackFactory

    fallback 拿不到异常原因,要打日志、区分失败类型时改用 fallbackFactory:

    @FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
    public interface UserClient {
        @GetMapping("/user/{id}")
        String getUser(@PathVariable("id") Long id);
    }
    
    @Component
    public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    
        @Override
        public UserClient create(Throwable cause) {
            // cause 就是触发降级的异常,可打日志、可区分原因
            System.out.println("调用 user-service 失败,触发降级:" + cause.getMessage());
            // 接口只有一个方法,可用 Lambda 简写
            return id -> "用户服务暂时不可用,请稍后重试";
        }
    }
    
    方式能拿到异常原因适用场景
    fallback❌只要兜底返回值,简单场景
    fallbackFactory✅需要记日志、区分失败原因

    7、两个注意点

    ① 容器里会出现两个同类型 Bean

    加了 fallback 后,UserClient 在容器里有两个实现(Feign 代理 + 降级类)。

    Feign代理负责发起远程调用;降级类负责返回兜底结果。

    Spring Cloud OpenFeign 默认把 Feign 代理标为 @Primary,所以 @Autowired UserClient 注入的仍是代理,行为正常。

    确有需要时可用 @FeignClient(primary = false) 关掉。

    ② TimeLimiter 默认只等 1 秒,比你的 read-timeout 更早触发

    • 每个 Feign 方法对应一个熔断器,名字是 接口名#方法名(参数类型),如 UserClient#getUser(Long)。
    • Resilience4j 的 TimeLimiter 默认超时是 1 秒。也就是说,即便上一节配了 read-timeout: 3000,下游卡住 1 秒就会走降级——读超时根本没机会生效。要一起调整:
    resilience4j:
      timelimiter:
        configs:
          default:
            timeout-duration: 3s        # 与 read-timeout 对齐
      circuitbreaker:
        configs:
          default:
            failure-rate-threshold: 50  # 失败率超 50% 触发"熔断"
            sliding-window-size: 10
    

    降级 ≠ 熔断,两个概念放一起理解:

    • 降级:调用失败就执行兜底逻辑,每次失败都会走;
    • 熔断:失败率超过阈值后"跳闸",后续请求不再真正发出,直接走兜底;过一段时间再"半开"试探恢复。

    熔断的完整内容在 L09(Sentinel)概念课展开。

    8、延伸:生产里用 Resilience4j 还是 Sentinel

    先说结论:Resilience4j 不是临时方案,它是 Spring Cloud 官方 CircuitBreaker 抽象下的标准实现之一(Hystrix 退役后,纯 Spring Cloud 体系就用它)。但 SCA 体系(本课程这套 Nacos + Gateway + Sentinel + Seata)里,生产更常用 Sentinel:

    维度Resilience4jSentinel
    规则存放配置文件 / 代码专门启动的服务的控制台界面,可持久化到 Nacos
    改阈值要重启发布动态下发,不重启
    能力范围熔断、重试、限流、舱壁熔断、QPS 限流、热点参数、系统自适应保护、网关限流
    可视化自己接监控自带 Dashboard

    好消息:切换几乎不用改代码——降级写法一个字都不用动:

    @FeignClient(name = "user-service", fallback = UserClientFallback.class)
    

    只换依赖(spring-cloud-starter-alibaba-sentinel)和开关(feign.sentinel.enabled=true)。

    • 更换依赖:把 spring-cloud-starter-circuitbreaker-resilience4j 换成 spring-cloud-starter-alibaba-sentinel

    • 更换开关配置:把 spring.cloud.openfeign.circuitbreaker.enabled 换成 feign.sentinel.enabled

    一句话记住:降级代码是稳定的,变的只是"谁来触发降级"。本讲先用 Resilience4j 把降级跑通,Sentinel 在 L09 展开。


    6、日志与调试

    1、为什么需要日志

    从前端浏览器上发送请求给后端时,可以借助浏览器开发者工具查看请求报文、响应报文等等这些详细信息,那现在通过 OpenFeign 调用其它服务,没有浏览器开发者工具可以使用了,怎么看报文呢?

    此时就需要在日志中打印请求和响应的相关信息

    2、开启 Feign 日志级别

    提问:为什么是 debug 级别?

    回答:因为很多具体详细信息在 info 级别不打印,想看到这些详细信息就必须使用 debug 级别

    提问:为什么不是针对微服务全局统一设置 debug 级别?

    回答:微服务全局默认是 info 级别,如果全局改成 debug 那么微服务启动和运行期间打印的日志就过于多了;一方面可能由于打印日志太多导致微服务启动超时失败,另一方面调试时看到日志太多也很难找到我们自己关心的内容

    logging:
      level:
        com.example.order.feign.UserClient: debug   # 打印该接口的请求/响应
    

    3、配置 Feign 日志输出级别(代码)

    import feign.Logger;        // 注意是 feign.Logger,不是 java.util.logging.Logger,也不是 slf4j
    
    @Configuration
    public class FeignLogConfig {
        @Bean
        Logger.Level feignLoggerLevel() {
            return Logger.Level.FULL;   // NONE / BASIC / HEADERS / FULL
        }
    }
    
    • NONE:不打印(默认)。
    • BASIC:只打印 URL 与方法。
    • HEADERS:在 BASIC 基础上,再打印请求/响应的头信息(不打印 body)。
    • FULL:打印请求/响应头与体(排查问题常用)。

    image-20261016103509664


    7、课堂练习

    1. 给 order-service 接入 OpenFeign,定义 UserClient 调用 user-service 的 /user/{id}。
    2. 加 connect-timeout: 2000、read-timeout: 3000,验证配置不报错。
    3. 把 Feign 日志调到 FULL,观察一次调用的完整请求响应。
    4. 给 UserClient 加 fallback(或 fallbackFactory):停掉 user-service,验证接口返回兜底文案而不是 500。
    5. (挑战)给 UserClient 加一个 POST 创建用户的方法,测试 @RequestBody 传参。

    8、本讲小结

    • OpenFeign 用"接口 + 注解"声明远程调用,替代手写 HTTP。
    • 三步:加依赖 → @EnableFeignClients → @FeignClient 定义接口,业务注入调用。
    • 参数用 Spring MVC 注解:@PathVariable / @RequestParam / @RequestBody / @RequestHeader。
    • 务必配置超时;Feign 默认自动接入 Nacos 注册中心与 LoadBalancer 负载均衡。
    • 调用失败要有降级兜底:加 circuitbreaker 依赖 + spring.cloud.openfeign.circuitbreaker.enabled=true + @FeignClient(fallback = ...);需要异常原因就用 fallbackFactory。

    9、本节验证

    • 理解 OpenFeign 声明式调用的价值(不手写 HTTP 细节)✅
    • 会加依赖、启用 Feign、定义远程调用接口并注入调用 ✅
    • 会处理参数与返回值 ✅
    • 会配置超时与重试,开启 Feign 日志调试 ✅
    • 会配置 fallback 降级:知道 circuitbreaker.enabled 不打开就静默失效、fallback 与 fallbackFactory 的区别、TimeLimiter 默认 1 秒会提前触发降级 ✅
    • 知道生产选型:SCA 体系用 Sentinel、纯 Spring Cloud 用 Resilience4j,切换时 fallback 代码不用改 ✅
    end