fileFeignApi.test() 看微服务全流程


一、开篇小故事:学校里的“铁三角”

上回我们说到,OpenFeign 是帮你“打电话”的智能小助手。今天,学校里又来了两个新角色,和小助手组成了“铁三角”:

角色

类比

作用

🧓 传达室大爷(Nacos)

学校传达室

知道所有部门(服务)的位置(IP+端口),有人问路就告诉他

🛡️ 正门保安(Gateway)

学校正门保安

所有访客(请求)都要经过正门,保安会指引你去正确的部门

🤖 智能小助手(OpenFeign)

之前的小助手

帮你把“找人办事”(方法调用)翻译成“出门问路”(HTTP请求)

现在,我们要办的事是:教务处(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-oss8081 房间,敲了敲门(发送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 并测试

  1. 启动 Nacos(如果没装,先去下载一个,单机模式启动即可)

  2. 启动 OSS 服务(端口8081)

  3. 启动 User 服务(端口8082)

  4. 测试调用

用 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() 这行代码背后的完整流程:

  1. Feign 翻译:把方法调用翻译成 HTTP 请求;

  2. Nacos 指路:Feign 通过 Spring Cloud 接口,从 Nacos 获取 OSS 服务的地址;

  3. Gateway 放行:指引请求到正确的 OSS 服务;

  4. OSS 处理:处理请求并返回结果,异常时执行 Feign 降级逻辑。

同时,记住这5个核心最佳实践(整合优化后):

  1. 用服务发现,不要硬编码 URL:依赖 Nacos 自动获取地址,灵活应对服务地址变化;

  2. 用真正的业务接口,不要只用 test():贴合业务需求,避免无效调用;

  3. 集成 Sentinel,做好容错:给服务调用加 Plan B,防止服务雪崩;

  4. 配置 Feign 日志,方便调试:开发环境开完整日志,生产环境精简日志;

  5. 依赖抽象接口,Feign 接口独立打包:解耦具体组件,减少代码冗余,便于维护。


两块二每分钟