微服务学习笔记(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"
    }
}
相关推荐
需要8263 小时前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
苏渡苇1 天前
Spring Insight 里如何把 Span 画成瀑布时间线
后端·spring·spring cloud·springboot·监控·apm
叶总没有会1 天前
Nacos—注册中心
spring cloud·微服务
苏渡苇2 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring·spring cloud·系统监控·apm
叶总没有会2 天前
微服务--分布式基础
java·spring·spring cloud·微服务
孙启超3 天前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
行百里er4 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控
砍材农夫4 天前
spring|spring web|拦截器、过滤器、aop
java·spring boot·spring·spring cloud
weixin_BYSJ19875 天前
【计算机毕设】基于SpringBoot与Vue的文物保护档案管理系统08621
vue.js·spring boot·spring cloud·微服务·架构·django·课程设计