从 fileFeignApi.test() 看微服务全流程
一、开篇小故事:学校里的“铁三角”
上回我们说到,OpenFeign 是帮你“打电话”的智能小助手。今天,学校里又来了两个新角色,和小助手组成了“铁三角”:
现在,我们要办的事是:教务处(User服务)要把学生的头像照片上传到图书馆的档案柜(OSS服务)。
让我们跟着 fileFeignApi.test() 这行代码,看看整个流程是怎么发生的!
二、完整流程拆解:4步走完全程
我们先看你提供的代码:
if (Objects.nonNull(avatarFile)) {
// 调用对象存储服务上传文件
fileFeignApi.test(); // ← 我们就从这行代码开始!
}这行代码就像你对小助手说:“帮我去图书馆档案柜(OSS服务)办个事!”。接下来会发生这4步:
第1步:Feign 把“方法调用”翻译成“HTTP请求”
小助手(Feign)收到你的指令,第一件事是把“找档案柜办事”(fileFeignApi.test())翻译成“出门的路线和要说的话”(HTTP请求)。
翻译后的内容大概是:
“我要去 lifestream-oss 这个地方,走 /file/test 这条路,用 GET 方式敲门。”
第2步:Feign 找传达室大爷(Nacos)问地址
小助手只知道部门名字叫 lifestream-oss,但不知道它在学校的哪栋楼、哪个房间。于是它跑到传达室,问大爷:
“大爷大爷,lifestream-oss 在哪呀?”
传达室大爷(Nacos)翻了翻自己的小本子(服务注册表),告诉小助手:
“在 192.168.1.233 号楼,8081 房间,快去吧!”
第3步:请求经过正门保安(Gateway)指引
小助手拿着地址出门,必须经过学校正门。保安(Gateway)看了看它的路线:
“哦,你要去 /file/** 这条路呀,那是去图书馆档案柜的,直走左拐就到了!”
保安还会顺便检查一下小助手有没有带证件(权限校验),没问题就放行。
第4步:OSS 服务处理请求,返回结果
小助手终于来到 lifestream-oss 的8081 房间,敲了敲门(发送HTTP请求)。
档案柜管理员(OSS服务的Controller)开门接待,办完事后告诉小助手:
“事情办完啦,这是回执(HTTP响应)!”
小助手把回执带回来给你,整个流程就结束了!
三、代码实现:完整的微服务调用代码
光说不练假把式,我们现在用代码把这个流程复现出来!
准备工作
三个服务:
公共模块(common):放 Feign 接口和实体类
User 服务(user-service):调用方(教务处)
OSS 服务(oss-service):服务提供方(档案柜)
一个注册中心:Nacos(传达室)
第一步:创建公共模块(common)
把 Feign 接口和实体类放在这里,避免两个服务重复写代码。
1. 定义 Feign 接口(FileFeignApi.java)
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
/**
* @FeignClient:声明这是一个 Feign 客户端
* name:目标服务的名字(告诉 Nacos 要找谁)
* path:接口前缀(所有方法的路径都会加上这个前缀)
* fallback:降级类,服务异常时执行备用逻辑
*/
@FeignClient(name = "lifestream-oss", path = "/file", fallback = FileFeignApiFallback.class)
public interface FileFeignApi {
/**
* 测试接口(对应你代码里的 test())
*/
@GetMapping("/test")
String test();
/**
* 实际的文件上传接口(后面最佳实践会用到)
*/
@PostMapping("/upload")
String uploadFile(@RequestParam("file") MultipartFile file);
}
// 降级类:服务异常时的备用逻辑
@Component
class FileFeignApiFallback implements FileFeignApi {
@Override
public String test() {
return "OSS服务暂时繁忙,请稍后再试!";
}
@Override
public String uploadFile(MultipartFile file) {
return "文件上传暂时失败,我们会稍后重试,您的其他信息已保存成功!";
}
}第二步:创建 OSS 服务(oss-service)
这是档案柜,负责存文件。
1. 编写配置文件(application.yml)
server:
port: 8081 # 档案柜的房间号
spring:
application:
name: lifestream-oss # 服务名字(告诉 Nacos 我叫什么)
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 传达室的地址2. 编写服务提供方的 Controller(FileController.java)
这是档案柜管理员:
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/file") // 和 Feign 接口的 path 对应
public class FileController {
/**
* 测试接口
*/
@GetMapping("/test")
public String test() {
System.out.println("OSS 服务收到了 test 请求!");
return "恭喜你,成功调用了 OSS 服务的 test 接口!";
}
/**
* 实际的文件上传接口
*/
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
String fileName = file.getOriginalFilename();
System.out.println("OSS 服务收到了文件:" + fileName);
// 这里可以写真正的文件上传逻辑(比如上传到阿里云 OSS、MinIO 等)
return "文件 " + fileName + " 上传成功!";
}
}3. 启动类上开启服务注册
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 通用注解:开启服务注册,不依赖具体组件
public class OssApplication {
public static void main(String[] args) {
SpringApplication.run(OssApplication.class, args);
}
}第三步:创建 User 服务(user-service)
这是教务处,负责调用 OSS 服务。
1. 引入依赖(pom.xml)
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 公共模块(引入我们刚才写的 FileFeignApi) -->
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Sentinel 容错(用于降级) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>2. 编写配置文件(application.yml)
server:
port: 8082 # 教务处的房间号
spring:
application:
name: user-service # 服务名字
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 传达室的地址
feign:
sentinel:
enabled: true # 开启 Sentinel 对 Feign 的支持,配合 fallback 降级3. 启动类上开启 Feign 和服务注册
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient // ✅ 好习惯:通用注解,不依赖具体注册中心
@EnableFeignClients(basePackages = "com.example.common.feign") // 开启 Feign,扫描公共模块的接口
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}4. 编写 User 服务的 Service(UserServiceImpl.java)
这就是你提供的代码所在的地方:
import com.example.common.feign.FileFeignApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Objects;
@Service
public class UserServiceImpl {
@Autowired
private FileFeignApi fileFeignApi; // 注入 Feign 接口
public String updateUserInfo(MultipartFile avatarFile) {
if (Objects.nonNull(avatarFile)) {
// 调用对象存储服务上传文件(最佳实践:调用真正的业务接口)
String result = fileFeignApi.uploadFile(avatarFile);
return result;
}
return "没有文件需要上传";
}
}5. 编写 User 服务的 Controller(UserController.java)
给外部一个调用入口:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserServiceImpl userService;
@PostMapping("/updateAvatar")
public String updateAvatar(@RequestParam("avatar") MultipartFile avatarFile) {
return userService.updateUserInfo(avatarFile);
}
}第四步:启动 Nacos 并测试
启动 Nacos(如果没装,先去下载一个,单机模式启动即可)
启动 OSS 服务(端口8081)
启动 User 服务(端口8082)
测试调用:
用 Postman 或 curl 发送请求:
POST http://localhost:8082/user/updateAvatar
参数:avatar(选一个文件)你会看到返回结果:
文件 XXX 上传成功!同时,OSS 服务的控制台会打印:
OSS 服务收到了文件:XXX🎉 恭喜你,整个微服务调用流程跑通了!
四、深入原理:Feign 和 Nacos 怎么“说上话”的?
你能想到“不同公司开发的组件是怎么配合的”,说明你已经触及到了架构设计的核心思想——“面向接口编程”和“解耦”!
Feign 是 Netflix 开发的,Nacos 是阿里巴巴开发的,它们根本不“认识”对方,那怎么能完美配合呢?这就要归功于 Spring Cloud 这个“和事佬”,它就像国家电网,制定了统一的“插座标准”,让不同厂商的“电器”都能正常工作。
第一阶段:小学生视角——插座与电器的魔法
想象一个简单场景,一眼看懂核心逻辑:
Feign:就像一台“美国生产的电视机”(Netflix 开发),只负责“用电”(获取服务地址、发送请求);
Nacos:就像一个“中国生产的发电站”(阿里开发),只负责“供电”(提供服务地址);
Spring Cloud:就像“国家电网”,制定了“两孔插座”的统一标准(接口)。
电视机不用认识发电站,发电站也不用认识电视机——只要两者都符合“插座标准”,电视机就能插上插座用上电。对应到微服务里就是:Feign 和 Nacos 都遵守 Spring Cloud 制定的接口标准,不用互相依赖,就能完美配合。
第二阶段:专业原理——面向接口编程(核心)
在 Java 和 Spring 的世界里,这个“国家电网”叫做 Spring Cloud Commons(Spring Cloud 公共包),它定义了一套微服务必须遵守的公共接口规范,其中最核心的就是 DiscoveryClient(服务发现客户端)。
1. 制定标准(Spring Cloud 做的事)
Spring Cloud 官方制定了一个“规矩”(接口),任何想做“服务发现”的组件(比如 Nacos、Eureka),都必须实现这个接口:
// Spring Cloud 官方制定的标准接口:服务发现的“插座标准”
public interface DiscoveryClient {
/**
* 核心方法:给服务名,返回服务的IP和端口列表
* @param serviceId 服务名字(比如 "lifestream-oss")
* @return 服务实例列表(包含IP、端口等信息)
*/
List<ServiceInstance> getInstances(String serviceId);
}2. 提供服务(Nacos 做的事)
阿里开发 Nacos 时,严格遵守了 Spring Cloud 的“规矩”,写了一个类实现了 DiscoveryClient 接口,把自己的服务地址“接入”了 Spring Cloud 的“插座”:
// Nacos 遵守 Spring Cloud 标准,实现“插座”接口
public class NacosDiscoveryClient implements DiscoveryClient {
@Override
public List<ServiceInstance> getInstances(String serviceId) {
// Nacos 自己的底层逻辑:去 Nacos 服务器查这个服务的所有IP和端口
// 比如返回:[192.168.1.100:8081, 192.168.1.101:8081]
return nacosServiceManager.getInstances(serviceId);
}
}3. 消费服务(Feign 做的事)
Feign 发送请求前,需要知道目标服务的地址,但它完全不知道 Nacos 的存在——它只认识 Spring Cloud 制定的 DiscoveryClient 接口,就像电视机只认识插座一样:
// Feign 的“用电逻辑”:只依赖 Spring Cloud 的标准接口
public class LoadBalancerFeignClient {
// Spring 自动把 Nacos 的实现(NacosDiscoveryClient)“塞”到这里(依赖注入)
private DiscoveryClient discoveryClient;
public Response execute(Request request) {
// 1. 从请求中获取服务名(比如 "lifestream-oss")
String serviceId = getServiceId(request);
// 2. 问 Spring Cloud 要地址(底层实际是 Nacos 在回答,但 Feign 不知道)
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
// 3. 选一个地址(负载均衡,比如 192.168.1.100:8081)
ServiceInstance target = instances.get(0);
// 4. 拼装 HTTP 请求并发送:http://192.168.1.100:8081/file/test
String url = "http://" + target.getHost() + ":" + target.getPort() + request.path();
return sendHttpRequest(url);
}
}核心总结
Spring Cloud 制定“接口标准”,Nacos 实现标准提供服务地址,Feign 调用标准获取地址——三者各司其职,实现了解耦。哪怕以后把 Nacos 换成其他注册中心(比如 Eureka),只要它也实现了 DiscoveryClient 接口,Feign 的代码一行都不用改!
Spring Cloud:制定“普通话”(接口标准)的“国家电网”;
Nacos:会说“普通话”、提供地址的“传达室大爷”;
Feign:会说“普通话”、发送请求的“智能小助手”;
结果:小助手和大爷用“普通话”交流,毫无障碍,还能灵活替换“大爷”(注册中心)!
这就是 Spring Cloud 的魅力:统一抽象,无缝集成!
五、最佳实践 VS 反面教材(对比案例)
案例1:硬编码 URL VS 服务发现
❌ 反面教材:把 OSS 服务的地址写死在代码里
// 反面教材:硬编码 URL
@FeignClient(name = "lifestream-oss", url = "http://192.168.1.233:8081")
public interface BadFileFeignApi {
@GetMapping("/file/test")
String test();
}问题:
OSS 服务换 IP 或端口了,你得改代码重新部署;
OSS 服务部署了多个实例,无法做负载均衡。
✅ 最佳实践:用 Nacos 服务发现
// 最佳实践:只写服务名字,不写 URL,依赖 Nacos 自动发现
@FeignClient(name = "lifestream-oss", path = "/file", fallback = FileFeignApiFallback.class)
public interface GoodFileFeignApi {
@GetMapping("/test")
String test();
}好处:
OSS 服务换地址了,不用改代码,Nacos 会自动更新;
OSS 服务部署多个实例,Feign 会自动做负载均衡。
案例2:用 test() 接口 VS 用真正的 upload() 接口
❌ 反面教材:只调用 test() 接口,不上传文件
// 反面教材:只调用 test(),没传文件,白传了参数
if (Objects.nonNull(avatarFile)) {
fileFeignApi.test(); // 头像文件根本没传给 OSS 服务,是“假上传”
}问题:
头像文件
avatarFile根本没传给 OSS 服务;这是一个“假上传”,无法实现业务需求。
✅ 最佳实践:调用真正的 upload() 接口
// 最佳实践:调用 upload(),真正上传文件,贴合业务需求
if (Objects.nonNull(avatarFile)) {
String result = fileFeignApi.uploadFile(avatarFile); // 真正上传文件
return result;
}好处:
文件真的传给 OSS 服务了,实现了头像上传的业务需求;
避免无效调用,提升服务性能。
案例3:没有容错 VS 集成 Sentinel 容错
❌ 反面教材:OSS 服务挂了,User 服务也跟着挂
// 反面教材:没有容错,服务异常直接崩溃
if (Objects.nonNull(avatarFile)) {
// OSS 服务挂了的话,这里会直接抛异常,整个请求失败
return fileFeignApi.uploadFile(avatarFile);
}问题:
OSS 服务挂了,用户连更新用户信息的其他功能都用不了;
服务雪崩:一个服务挂了,连累其他服务也挂。
✅ 最佳实践:集成 Sentinel 实现降级
// 1. Feign 接口指定降级类(Plan B)
@FeignClient(
name = "lifestream-oss",
path = "/file",
fallback = FileFeignApiFallback.class // 指定降级类,服务异常时执行
)
public interface FileFeignApi {
@PostMapping("/upload")
String uploadFile(@RequestParam("file") MultipartFile file);
}
// 2. 编写降级类:服务异常时的备用逻辑
@Component
public class FileFeignApiFallback implements FileFeignApi {
@Override
public String uploadFile(MultipartFile file) {
// OSS 服务挂了的时候,会调用这里,给用户友好提示
return "文件上传暂时失败,我们会稍后重试,您的其他信息已保存成功!";
}
}好处:
OSS 服务挂了,用户还能更新其他信息,只是头像暂时传不上去,不影响核心功能;
避免服务雪崩,提升系统稳定性。
案例4:没有日志 VS 配置 Feign 日志
❌ 反面教材:没有日志,出了问题不知道怎么回事
# 反面教材:没有配置 Feign 日志,调试困难
logging:
level:
root: info问题:
出了问题不知道请求发了什么、响应是什么;
调试非常困难,排查问题效率极低。
✅ 最佳实践:配置 Feign 日志
// 1. 编写 Feign 配置类,指定日志级别
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
// 开发环境用 FULL(完整日志),生产环境用 BASIC(只记录关键信息)
return Logger.Level.FULL;
}
}# 2. 配置文件中开启 Feign 接口的日志
logging:
level:
com.example.common.feign.FileFeignApi: debug # 只给 Feign 接口开 debug 日志好处:
控制台会打印完整的请求/响应信息,调试非常方便;
生产环境可以把日志级别改成
BASIC,只记录关键信息,不占用过多资源。
案例5:依赖具体组件 VS 依赖抽象接口(新增核心最佳实践)
❌ 反面教材:启动类依赖具体组件,耦合度高
// 反面教材:直接依赖 Nacos 具体注解,以后换注册中心要改代码
@SpringBootApplication
@EnableNacosDiscovery // ❌ 坏习惯:绑定了 Nacos,耦合度高
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
// 反面教材:Feign 接口写在调用方内部,重复冗余
@FeignClient(name = "lifestream-oss") // ❌ 订单服务调用 OSS 时,还要复制一遍
public interface FileFeignApi {
@PostMapping("/file/test")
void test();
}问题:
绑定具体组件(Nacos),以后换成 Eureka 等其他注册中心,需要修改启动类注解;
Feign 接口重复编写,多个服务调用同一服务时,需要反复复制,维护成本高。
✅ 最佳实践:依赖抽象,Feign 接口独立打包
// 1. 启动类使用通用注解,不依赖具体组件
@SpringBootApplication
@EnableDiscoveryClient // ✅ 好习惯:只说“开启服务发现”,不管底层是 Nacos 还是 Eureka
public class UserApplication { ... }
// 2. Feign 接口独立打包(比如创建 lifestream-api 模块)
// 由 OSS 服务提供这个接口 Jar 包,其他服务直接引入,不用重复编写
@FeignClient(name = "lifestream-oss", path = "/file", fallback = FileFeignApiFallback.class)
public interface FileFeignApi {
@PostMapping("/file/test")
void test();
@PostMapping("/file/upload")
String uploadFile(@RequestParam("file") MultipartFile file);
}好处:
解耦:替换注册中心时,启动类代码不用改,符合“面向接口编程”思想;
复用:Feign 接口只写一次,多个服务(User、订单等)直接引入 Jar 包即可,减少冗余,便于维护。
六、总结
现在,我们再回顾一下 fileFeignApi.test() 这行代码背后的完整流程:
Feign 翻译:把方法调用翻译成 HTTP 请求;
Nacos 指路:Feign 通过 Spring Cloud 接口,从 Nacos 获取 OSS 服务的地址;
Gateway 放行:指引请求到正确的 OSS 服务;
OSS 处理:处理请求并返回结果,异常时执行 Feign 降级逻辑。
同时,记住这5个核心最佳实践(整合优化后):
用服务发现,不要硬编码 URL:依赖 Nacos 自动获取地址,灵活应对服务地址变化;
用真正的业务接口,不要只用 test():贴合业务需求,避免无效调用;
集成 Sentinel,做好容错:给服务调用加 Plan B,防止服务雪崩;
配置 Feign 日志,方便调试:开发环境开完整日志,生产环境精简日志;
依赖抽象接口,Feign 接口独立打包:解耦具体组件,减少代码冗余,便于维护。