Java框架 SpringCloud 快速入门: Feign 替代 RestTemplate 实现声明式远程调用

概述

上一篇用 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 请求,恰好需要五个信息:

  1. 服务名称(发给谁)
  2. 请求方式(GET / POST)
  3. 请求路径
  4. 请求参数
  5. 返回值类型

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 是否写全。
相关推荐
谢亮_vipxieliang1 小时前
Go 结构体与方法集核心知识点
java·开发语言·golang
郝学胜-神的一滴1 小时前
Numpy数据处理详解 01:NumPy 从环境搭建到入门上手
开发语言·人工智能·python·程序人生·数据分析·numpy
尘客-追梦1 小时前
qmake / jom / 影子构建:把编译管起来
开发语言·c++·qt
Ai-_Man1 小时前
从聊天框到正式文档:AI导出鸭瞄准AI大模型应用的“最后一公里”
开发语言·前端·javascript·人工智能·小程序
EatFan1 小时前
2026 Java 后端面试风向变了:八股文只是门槛,场景化追问才是淘汰线
java·开发语言·jvm·面试·后端面试·八股文·full gc
Q26433650231 小时前
【有源码】基于Spring Boot+Vue的骑行爱好者平台的设计与实现-计算机毕设选题推荐
java·vue.js·spring boot·毕业设计·课程设计·数据可视化·源代码
xieliyu.1 小时前
LeetCode 1419 数青蛙:状态模拟求最少青蛙数量|Java 题解
java·算法·leetcode
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 微服务配置拉取
java·spring cloud·微服务
江屿风1 小时前
【Linux系统】【从菜鸟驿站到操作系统:一节课打通Linux重定向与缓冲区真相】流食般投喂
linux·运维·服务器·开发语言·笔记