☰
Java框架 SpringCloud 快速入门: Feign 替代 RestTemplate 实现声明式远程调用
2026/10/7 20:00:24 网站建设 项目流程

概述

上一篇用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 里查询订单时远程查询用户的逻辑:

// 2.利用RestTemplate发起http请求,查询用户// 2.1.url路径:服务名写死在字符串里,参数靠手工拼接Stringurl="http://userservice/user/"+order.getUserId();// 2.2.发送http请求,实现远程调用Useruser=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 结构:

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:

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

starter 意味着自动装配,Feign 运行所需的各种组件由 Spring Boot 帮我们配好。

启动类加 @EnableFeignClients

@EnableFeignClients是 Feign 功能的总开关,不加它,接口声明得再规范也不会生效。这一步改的就是启动类这一个注解:

packagecn.itcast.order;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.cloud.client.loadbalancer.LoadBalanced;importorg.springframework.cloud.openfeign.EnableFeignClients;importorg.springframework.context.annotation.Bean;importorg.springframework.web.client.RestTemplate;@MapperScan("cn.itcast.order.mapper")@SpringBootApplication@EnableFeignClients// 开启Feign功能,默认扫描启动类所在包及其子包中的@FeignClient接口publicclassOrderApplication{publicstaticvoidmain(String[]args){SpringApplication.run(OrderApplication.class,args);}/** * 创建RestTemplate并注入Spring容器 * 改用Feign后这个Bean可以删掉,这里暂时保留用于对比 */@Bean@LoadBalancedpublicRestTemplaterestTemplate(){returnnewRestTemplate();}}

@EnableFeignClients默认扫描启动类所在包及子包。UserClient放在cn.itcast.order.client,在扫描范围内,什么都不用配。如果客户端接口放在别的包,就要显式指定basePackages或clients属性——这是后面避坑清单里的第一名。

编写 UserClient 接口

新建一个接口,封装所有对 userservice 服务的远程调用:

packagecn.itcast.order.client;importcn.itcast.order.pojo.User;importorg.springframework.cloud.openfeign.FeignClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;@FeignClient("userservice")publicinterfaceUserClient{@GetMapping("/user/{id}")UserfindById(@PathVariable("id")Longid);}

仔细看这个接口,全是 SpringMVC 的注解,没有任何新东西——这正是 Feign 降低学习成本的设计,它默认采用 SpringMVC 的注解来声明调用信息。五个要素对应关系如下:

声明位置代码对应要素
类上@FeignClient("userservice")"userservice"服务名称(注册中心里的服务名,不是 IP 地址)
方法上@GetMapping("/user/{id}")@GetMapping请求方式 GET
同上"/user/{id}"请求路径,{id}是路径占位符
方法参数@PathVariable("id") Long idLong id请求参数
方法返回值User返回值类型

写这个接口时对着提供方的UserController抄即可,两边的方法签名和注解必须保持一致。user-service 里的接口长这样:

packagecn.itcast.user.web;importcn.itcast.user.pojo.User;importcn.itcast.user.service.UserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")publicclassUserController{@AutowiredprivateUserServiceuserService;/** * 路径: /user/110 * * @param id 用户id * @return 用户 */@GetMapping("/{id}")publicUserqueryById(@PathVariable("id")Longid){returnuserService.queryById(id);}}

类上的@RequestMapping("/user")加方法上的@GetMapping("/{id}"),合并起来就是/user/{id}——这正是UserClient里声明的路径。

有一个关键认知:@FeignClient的 value 是服务名,对应 nacos/eureka 注册中心里注册的服务名。Feign 拿到服务名后自己去注册中心拉实例列表,你永远不需要在代码里写 IP 和端口。

OrderService 注入 UserClient 调用

最后一步,把原来 RestTemplate 的代码整段删掉,注入UserClient,直接调方法:

packagecn.itcast.order.service;importcn.itcast.order.client.UserClient;importcn.itcast.order.mapper.OrderMapper;importcn.itcast.order.pojo.Order;importcn.itcast.order.pojo.User;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;@ServicepublicclassOrderService{@AutowiredprivateOrderMapperorderMapper;@AutowiredprivateUserClientuserClient;publicOrderqueryOrderById(LongorderId){// 1.查询订单Orderorder=orderMapper.findById(orderId);// 2.用Feign远程调用,查用户Useruser=userClient.findById(order.getUserId());// 3.封装user到Orderorder.setUser(user);// 4.返回returnorder;}}

启动 order-service,浏览器访问http://localhost:8080/order/101并多刷新几次,每次都返回完整订单数据。观察 user-service 的 8081、8082 两个实例的日志,会发现两个实例都被访问到了——Feign 不仅完成了远程调用,负载均衡也在生效。

改造前后对比

同一个"查用户"动作,两种写法放在一起看:

改造前(RestTemplate):

Stringurl="http://userservice/user/"+order.getUserId();Useruser=restTemplate.getForObject(url,User.class);

改造后(Feign):

Useruser=userClient.findById(order.getUserId());
维度RestTemplateFeign
调用风格拼 URL 字符串发请求调接口方法
URL 维护硬编码在业务代码里声明在客户端接口上,集中管理
参数处理手工字符串拼接,多个参数极易出错方法参数 + 注解,几个参数写几个形参
负载均衡需要@LoadBalanced手动开启内部集成 Ribbon,自动生效
可读性不看注释不知道在干什么不说明都以为是本地方法调用
复杂 URL参数七八个时基本没法维护方法列表里加形参即可

将来遇到参数非常多的接口,Feign 的应对方式很朴素:方法列表里多加几个参数,每个参数配好@RequestParam或@PathVariable,维护成本恒定。

Feign 底层是怎么工作的

你写的只是一个接口,没有实现类,那调用方法时发生了什么?答案是动态代理:Feign 在启动时为每个@FeignClient接口生成代理对象注入容器,调用方法时,代理对象把注解里声明的信息组装成一个 HTTP 请求发出去。

user-service注册中心(Nacos)Ribbon负载均衡UserClient(动态代理对象)OrderServiceuser-service注册中心(Nacos)Ribbon负载均衡UserClient(动态代理对象)OrderServicefindById(101L) 看似调用本地方法解析@FeignClient/@GetMapping注解组装请求: GET /user/101请求目标: 服务名 userservice拉取 userservice 实例列表[8081, 8082]按负载均衡策略选出一个实例HTTP GET http://192.168.x.x:8082/user/101返回 JSON响应体反序列化为 User 对象

整个过程可以概括成一条链路:

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这类外部包里,必须在注解上显式指定,两种写法二选一:
// 写法一:指定扫描包@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 参数,必须写参数名
@LoadBalancedRestTemplate 的 BeanRestTemplate 方案下开启负载均衡;Feign 内部已集成,无需再配

官方文档

  • Spring Cloud OpenFeign 官方文档
  • Spring Cloud Netflix Ribbon

总结

  • RestTemplate 的三个问题:URL 硬编码拼接、复杂参数难维护、编程体验不统一。
  • Feign 是声明式 HTTP 客户端:把服务名称、请求方式、请求路径、请求参数、返回值类型五个信息用注解声明在接口上,请求由框架发送。
  • 改造四步:引依赖spring-cloud-starter-openfeign→ 启动类加@EnableFeignClients→ 编写UserClient接口 → 业务代码注入接口调方法。
  • 客户端接口全部使用 SpringMVC 注解,照着提供方的 Controller 抄即可,@FeignClient的 value 是服务名不是地址。
  • 底层没有黑魔法:动态代理生成实现,内部集成 Ribbon 自动负载均衡,本质是 HTTP 客户端 + 负载均衡的封装。
  • 排查口诀:启动报找不到 Bean 查@EnableFeignClients;调用报找不到实例查服务名拼写;参数收不到查@RequestParam/@PathVariable是否写全。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询