微服务学习笔记(3)——基于SpringCloud OpenFeign实现远程调用


🔥我的主页: 九转苍翎
⭐️个人专栏: 《Java SE》 《Java集合框架系统精讲》 《MySQL高手之路:从基础到高阶》 《计算机网络》 《Java工程师核心能力体系构建》 《RabbitMQ理论与实践》
天行健,君子以自强不息。


0.前言

  • SpringBoot版本:3.2.5
  • SpringCloud版本:2023.0.3
  • SpringCloud Alibaba版本:2023.0.1.0
  • nacos版本:2.2.3(已免费上传至我的资源)
  • 项目源码spring-cloud-blog

在引入 Nacos 等服务发现组件后,服务调用方虽然不再需要记忆具体的 IP 地址,只需知道服务提供方的服务名即可发起调用,但其底层本质仍然是基于 IP 和端口的 URL 拼接与 HTTP 请求。

java 复制代码
@Service
public class OrderService {
    private final OrderMapper orderMapper;
    private final RestTemplate restTemplate;

    @Autowired
    public OrderService(OrderMapper orderMapper, RestTemplate restTemplate) {
        this.orderMapper = orderMapper;
        this.restTemplate = restTemplate;
    }

    public OrderInfo selectOrderById(Integer id) {
        OrderInfo orderInfo = orderMapper.selectOrderById(id);
        String url = "http://product-service/product/getProductById?id=" + orderInfo.getProductId();
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

为了进一步简化这一过程,Spring 官方在 Spring Framework 5 中已将传统的 RestTemplate 标记为 @Deprecated(已弃用)。与此同时,Spring 团队对 Netflix 开源的 OpenFeign 组件进行了封装,推出了 Spring Cloud Feign,通过声明式的编程模型,更优雅地实现了微服务间的远程调用:像调用本地方法一样调用远程服务

1.快速上手

1.1 引入依赖

在order-service服务的pom.xml文件中添加如下依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

1.2 启动Feign客户端的功能

在order-service服务的启动类上添加@EnableFeignClients

java 复制代码
@EnableFeignClients // 激活并启动 Feign 客户端的功能。添加此注解后,Spring 容器才会自动扫描 @FeignClient 注解的接口
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

1.3 创建OpenFeign客户端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
    @GetMapping("/getProductById")
    ProductInfo getProductById(@RequestParam("id") Integer id);
}
  • value:指定目标服务的服务名称
  • path:为当前接口中的所有请求方法添加统一的前缀路径(OpenFeign客户端的url需要和服务提供方保持一致)

1.4 远程调用

java 复制代码
@Service
public class OrderService {
    private final OrderMapper orderMapper;
    private final ProductApi productApi;

    @Autowired
    public OrderService(OrderMapper orderMapper, ProductApi productApi) {
        this.orderMapper = orderMapper;
        this.productApi = productApi;
    }

    public OrderInfo getOrderById(Integer id) {
        OrderInfo orderInfo = orderMapper.selectOrderById(id);
        ProductInfo productInfo = productApi.getProductById(orderInfo.getProductId());
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

访问URL:127.0.0.1:8000/order/getOrderById?id=1

返回响应:

javascript 复制代码
{
    "id": 1,
    "userId": 2001,
    "productId": 1001,
    "num": 1,
    "price": 99,
    "deleteFlag": 0,
    "createTime": "2026-02-21T19:46:25.000+00:00",
    "updateTime": "2026-02-21T19:46:25.000+00:00",
    "productInfo": {
        "id": 1001,
        "productName": "T恤",
        "productPrice": 101,
        "status": null,
        "createTime": "2026-02-21T19:46:34.000+00:00",
        "updateTime": "2026-02-21T19:46:34.000+00:00"
    }
}
  • 注意1:如果OpenFeign客户端的url与服务提供方不一致,会发生以下报错:

  • 注意2:OpenFeign客户端接收不同类型参数时,需要添加对应注解(如下)

    java 复制代码
    @FeignClient(value = "product-service", path = "/product")
    public interface ProductApi {
    
        @GetMapping("/getProductById")
        ProductInfo getProductById(@RequestParam("id") Integer id);
    
        @GetMapping("/demo1")
        String demo1(@RequestParam("id") Integer id,@RequestParam("name") String name);
    
        @GetMapping("/demo2")
        String demo2(@SpringQueryMap ProductInfo productInfo);
    
        @PostMapping("/demo3")
        String demo3(@RequestBody ProductInfo productInfo);
    }

2.OpenFeign 最佳实践:继承与抽取

在微服务架构中,使用 OpenFeign 时最大的痛点是 Feign 客户端接口 和 服务提供方 Controller 的代码重复维护。下文将介绍通过 继承抽取公共模块 来解决这一问题

2.1 传统方式痛点分析

java 复制代码
// 服务提供方
@RestController
@RequestMapping("/product")
@Slf4j
public class ProductController {
    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/getProductById")
    public ProductInfo getProductById(@RequestParam("id") Integer id){
        log.info("getProductById接收到参数,id:{}",id);
        return productService.selectProductById(id);
    }

    @GetMapping("/demo1")
    public String demo1(@RequestParam("id") Integer id,@RequestParam("name") String name){
        return "id:"+id+",name:"+name;
    }

    @GetMapping("/demo2")
    public String demo2(ProductInfo productInfo){
        return productInfo.toString();
    }

    @PostMapping("/demo3")
    public String demo3(@RequestBody ProductInfo productInfo){
        return productInfo.toString();
    }
}
// Feign 客户端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @GetMapping("/getProductById") // 重复
    ProductInfo getProductById(@RequestParam("id") Integer id); // 重复

    @GetMapping("/demo1") // 重复
    String demo1(@RequestParam("id") Integer id,@RequestParam("name") String name); // 重复
  
    @GetMapping("/demo2") // 重复
    String demo2(@SpringQueryMap ProductInfo productInfo); // 重复

    @PostMapping("/demo3") // 重复
    String demo3(@RequestBody ProductInfo productInfo); // 重复
}
  • 方法签名、注解、路径定义重复维护
  • 修改 API 时容易遗漏同步

2.2 抽取公共模块

  • 整体架构

  • 项目结构

    text 复制代码
    spring-cloud-blog/
    │
    ├── common/                                              # 公共模块
    │   ├── core/                                            # 核心公共模块
    │   │   ├── src/
    │   │   │   ├── main/
    │   │   │   │   ├── java/org/example/
    │   │   │   │   │   ├── api/
    │   │   │   │   │   │   └── ProductApi.java              # Feign客户端
    │   │   │   │   │   └── core/
    │   │   │   │   │       ├── OrderInfo.java               # 订单信息实体
    │   │   │   │   │       └── ProductInfo.java             # 产品信息实体
    │
    ├── modules/                                             # 业务模块
    │   ├── order-service/                                   # 订单服务
    │   │   ├── src/
    │   │   │   ├── main/
    │   │   │   │   ├── java/org/example/orderservice/
    │   │   │   │   │   ├── feign/
    │   │   │   │   │   │   └── ProductFeignClient.java      # 继承 ProductApi + @FeignClient
    │   │
    │   ├── product-service/                                 # 产品服务
    │   │   ├── src/
    │   │   │   ├── main/
    │   │   │   │   ├── java/org/example/productservice/
    │   │   │   │   │   ├── controller/
    │   │   │   │   │   │   └── ProductController.java       # 实现ProductApi

2.3 核心代码实现

2.3.1 common-core模块:定义公共接口

java 复制代码
public interface ProductApi {

    @GetMapping("/getProductById")
    ProductInfo getProductById(@RequestParam("id") Integer id);

    @GetMapping("/demo1")
    String demo1(@RequestParam("id") Integer id,@RequestParam("name") String name);

    @GetMapping("/demo2")
    String demo2(@SpringQueryMap ProductInfo productInfo);

    @PostMapping("/demo3")
    String demo3(@RequestBody ProductInfo productInfo);
}

2.3.2 product-service模块:实现公共接口

java 复制代码
@RestController
@RequestMapping("/product")
@Slf4j
public class ProductController implements ProductApi {
    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/getProductById")
    public ProductInfo getProductById(@RequestParam("id") Integer id){
        log.info("getProductById接收到参数,id:{}",id);
        return productService.selectProductById(id);
    }

    @GetMapping("/demo1")
    public String demo1(@RequestParam("id") Integer id,@RequestParam("name") String name){
        return "id:"+id+",name:"+name;
    }

    @GetMapping("/demo2")
    public String demo2(ProductInfo productInfo){
        return productInfo.toString();
    }

    @PostMapping("/demo3")
    public String demo3(@RequestBody ProductInfo productInfo){
        return productInfo.toString();
    }
}

2.3.3 order-service模块:Feign客户端继承公共接口

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductFeignClient extends ProductApi {
}

2.3.4 order-service模块:调用ProductFeignClient并注释ProductApi

java 复制代码
@Service
public class OrderService {
    private final OrderMapper orderMapper;
    private final RestTemplate restTemplate;
    // private final ProductApi productApi;
    private final ProductFeignClient productFeignClient;

    @Autowired
    public OrderService(OrderMapper orderMapper, RestTemplate restTemplate/*, ProductApi productApi*/, ProductFeignClient productFeignClient) {
        this.orderMapper = orderMapper;
        this.restTemplate = restTemplate;
        // this.productApi = productApi;
        this.productFeignClient = productFeignClient;
    }

    public OrderInfo selectOrderById(Integer id) {
        OrderInfo orderInfo = orderMapper.selectOrderById(id);
        String url = "http://product-service/product/getProductById?id=" + orderInfo.getProductId();
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }

    public OrderInfo getOrderById(Integer id) {
        OrderInfo orderInfo = orderMapper.selectOrderById(id);
        // ProductInfo productInfo = productApi.getProductById(orderInfo.getProductId());
        ProductInfo productInfo = productFeignClient.getProductById(orderInfo.getProductId());
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }

    public String demo1(Integer id, String name) {
        // return productApi.demo1(id, name);
        return productFeignClient.demo1(id, name);
    }

    public String demo2(ProductInfo productInfo) {
        // return productApi.demo2(productInfo);
        return productFeignClient.demo2(productInfo);
    }

    public String demo3(ProductInfo productInfo) {
        // return  productApi.demo3(productInfo);
        return productFeignClient.demo3(productInfo);
    }
}

访问URL:127.0.0.1:8000/order/getOrderById?id=1

返回响应:

javascript 复制代码
{
    "id": 1,
    "userId": 2001,
    "productId": 1001,
    "num": 1,
    "price": 99,
    "deleteFlag": 0,
    "createTime": "2026-02-21T19:46:25.000+00:00",
    "updateTime": "2026-02-21T19:46:25.000+00:00",
    "productInfo": {
        "id": 1001,
        "productName": "T恤",
        "productPrice": 101,
        "status": null,
        "createTime": "2026-02-21T19:46:34.000+00:00",
        "updateTime": "2026-02-21T19:46:34.000+00:00"
    }
}
相关推荐
Devin~Y12 小时前
大厂Java面试实录:Spring Boot/Cloud、Kafka、Redis、K8s 与 Spring AI(RAG/Agent)三轮连环问
java·spring boot·redis·mysql·spring cloud·kafka·kubernetes
LSL666_20 小时前
快速Spring Cloud+ELK+AOP搭建一个简单的项目
spring·elk·spring cloud
杰克尼21 小时前
天机学堂项目总结(day11~day12)
spring·spring cloud
青槿吖1 天前
第二篇:从复制粘贴到自定义规则!Spring Cloud Gateway 断言 + 过滤全玩法,拿捏微服务流量管控
java·spring boot·后端·spring cloud·微服务·云原生·架构
武超杰1 天前
Spring Cloud Gateway 从入门到实战
spring cloud·gateway
Han.miracle1 天前
Spring Cloud + Nacos 环境切换与配置管理最佳实践
数据库·spring boot·spring cloud·maven
旷世奇才李先生2 天前
Docker实战:容器化部署与Docker Compose集群管理(附企业级案例)
spring cloud·docker·eureka
卷毛的技术笔记2 天前
从“拆东墙补西墙”到“最终一致”:分布式事务在Spring Boot/Cloud中的破局之道
java·spring boot·分布式·后端·spring cloud·面试·rocketmq
2601_949816682 天前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud