编码集成OpenFeign

下面笔者结合实际的编码讲解 OpenFeign 是怎样把服务通信变得像本地方法调用一样简单的。本节代码是在 spring-cloud-alibaba-multi-service-demo 模板项目的基础上修改的,具体步骤如下。

  1. 修改项目名称。

    将项目名称修改为 spring-cloud-alibaba-openfeign-demo,并把各个模块中 pom.xml 文件的 artifactId 修改为 spring-cloud-alibaba-openfeign-demo

  2. 引入 OpenFeign 依赖。

    打开 order-service-demo 项目中的 pom.xml 文件,在 dependencies 标签下引入 OpenFeign 依赖文件,新增代码如下:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  3. 新增 OpenFeign 代码。

    order-service-demo 项目中新建 ltd.order.cloud.newbee.openfeign 包,在 openfeign 包中依次新增 NewBeeGoodsDemoService 文件和 NewBeeShopCartDemoService 文件,分别用于创建对商品服务和购物车服务的 OpenFeign 调用。

    NewBeeGoodsDemoService.java 代码如下:

    package ltd.order.cloud.newbee.openfeign;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @FeignClient(value = "newbee-cloud-goods-service", path = "/goods")
    public interface NewBeeGoodsDemoService {
    
        @GetMapping(value = "/{goodsId}")
        String getGoodsDetail(@PathVariable(value = "goodsId") int goodsId);
    }

    NewBeeShopCartDemoService.java 代码如下:

    package ltd.order.cloud.newbee.openfeign;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @FeignClient(value = "newbee-cloud-shopcart-service", path = "/shop-cart")
    public interface NewBeeShopCartDemoService {
    
        @GetMapping(value = "/{cartId}")
        String getCartItemDetail(@PathVariable(value = "cartId") int cartId);
    }

    @FeignClient 注解的常用字段如下。

    • name:指定 FeignClient 的名称,如果项目中使用了负载均衡器,则 name 属性将作为服务实例的名称,用于服务发现。其作用与 value 字段的作用一致。

    • value:其作用与 name 字段的作用一致。

    • url:一般用于调试,手动指定 @FeignClient 调用的地址。

    • decode404:当发生 404 错误时,如果该字段为 true,则调用 decoder 进行解码,否则抛出异常。

    • configuration:指定设置自定义的相关配置类。

    • fallback:指定处理服务容错的类。

    • fallbackFactory:工厂类,用于生成 fallback 类实例,通过这个字段配置可以实现每个接口通用的容错逻辑,减少重复的代码。

    • path:定义当前 FeignClient 路径的统一前缀。

  4. 增加配置,启用 OpenFeign 并使 FeignClient 生效。

    order-service-demo 项目的启动类上添加 @EnableFeignClients 注解启用,并配置相关的 FeignClient 类,代码如下:

    @SpringBootApplication
    @EnableFeignClients(basePackages = {"ltd.order.cloud.newbee.openfeign"})
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        }
    }

    这里直接使用 basePackages 配置了扫描包,即 ltd.order.cloud.newbee.openfeign 包中标注了 @FeignClient 注解的类都会生效。也可以使用 clients 字段直接指定所有的类名,代码如下:

    @EnableFeignClients(clients={ltd.order.cloud.newbee.openfeign.NewBeeShopCartDemoService.class,ltd.order.cloud.newbee.openfeign.NewBeeGoodsDemoService.class})
  5. 使用 OpenFeign 声明的接口实现服务通信。

    由于已经使用 OpenFeign 声明了相关接口并配置完毕,因此这里修改 NewBeeCloudOrderAPI 类中远程调用 HTTP 请求的方式即可。先分别注入 NewBeeGoodsDemoServiceNewBeeShopCartDemoService,然后将使用 RestTemplate 工具调用商品服务和购物车服务的代码删掉,改为调用本地方法形式的代码。

    修改 NewBeeCloudOrderAPI 类的代码如下:

    @RestController
    public class NewBeeCloudOrderAPI {
    
        @Resource
        private NewBeeGoodsDemoService newBeeGoodsDemoService;
    
        @Resource
        private NewBeeShopCartDemoService newBeeShopCartDemoService;
    
        @GetMapping("/order/saveOrder")
        public String saveOrder(@RequestParam("cartId") int cartId, @RequestParam("goodsId") int goodsId) {
            // 简单地模拟下单流程,包括服务间的调用流程
    
            // 调用商品服务
            String goodsResult = newBeeGoodsDemoService.getGoodsDetail(goodsId);
    
            // 调用购物车服务
            String cartResult = newBeeShopCartDemoService.getCartItemDetail(cartId);
    
            // 执行下单逻辑
            return "success! goodsResult={" + goodsResult + "},cartResult={" + cartResult + "}";
        }
    }

    使用 OpenFeign 之前的代码如下:

    @RestController
    public class NewBeeCloudOrderAPI {
    
        @Resource
        private RestTemplate restTemplate;
    
        // 商品服务调用地址
        private final String CLOUD_GOODS_SERVICE_URL = "http://newbee-cloud-goods-service";
        // 购物车服务调用地址
        private final String CLOUD_SHOPCART_SERVICE_URL = "http://newbee-cloud-shopcart-service";
    
        @GetMapping("/order/saveOrder")
        public String saveOrder(@RequestParam("cartId") int cartId,
                                @RequestParam("goodsId") int goodsId) {
            // 简单模拟下单流程,包括服务间的调用流程
    
            // 调用商品服务
            String goodsResult = restTemplate.getForObject(CLOUD_GOODS_SERVICE_URL
                    + "/goods/" + goodsId, String.class);
    
            // 调用购物车服务
            String cartResult = restTemplate.getForObject(CLOUD_SHOPCART_SERVICE_URL
                    + "/shop-cart/" + cartId, String.class);
    
            // 执行下单逻辑
            return "success! goodsResult={" + goodsResult + "},cartResult={" + cartResult + "}";
        }
    }

    二者的区别是很明显的:不用在业务代码里处理 HTTP 请求地址、与请求参数相关的内容,只需要暴露 OpenFeign 接口,直接调用本地方法即可;代码风格上更统一、服务通信的编码也更简洁,并且二者所得到的效果是相同的。这就是真实的项目开发普遍选择 OpenFeign 的原因。当然,本书所编写的测试代码,不管是传参还是响应,结果都是简单的 Java 类型字符串,如果是对象类型或复杂类型的对象,使用 OpenFeign 与不使用 OpenFeign 的区别就更明显了。

  6. 进行功能测试。

启动 Nacos Server,之后依次启动这三个项目。如果未能成功启动,则开发人员需要查看控制台中的日志是否报错,并及时确认问题和修复。启动成功后进入 Nacos 控制台,单击 “服务管理” 中的服务列表,可以看到服务列表中已经存在这三个服务的服务信息,如图 8-1 所示。

image 2025 04 16 16 48 59 532
Figure 1. 图8-1 Nacos 控制台中的服务列表

打开浏览器验证是否整合成功,在地址栏中输入如下地址:

http://localhost:8117/order/saveOrder?cartId=2022&goodsId=2035

响应结果如图 8-2 所示。

image 2025 04 16 16 50 08 677
Figure 2. 图8-2/order/saveOrder 请求的访问结果

整合 OpenFeign 组件后,order-service-demo 对另外两个服务的调用没有问题,测试成功!通信组件由 RestTemplate+Spring Cloud LoadBalancer 成功替换为 OpenFeign+Spring CloudLoadBalancer

NewBeeShopCartDemoService.java 为例,该接口中定义了一个方法 getCartItemDetail(),参数是 cartId。同时,NewBeeShopCartDemoService.java@FeignClient 注解标注,其 value 字段为 newbee-cloud-shopcart-service。“newbee-cloud-shopcart-service” 就是购物车服务在 Nacos Server 上注册的名称。path 字段为 /shop-cart,表示 FeignClient 路径的统一前缀。getCartItemDetail() 方法其实已经表明了调用时的通信地址,即 http://newbee-cloud-shopcart-service/shop-cart/{cartId}。读者对这个地址的写法应该很熟悉,前面几个章节中介绍过的基于服务中心的通信地址皆如此。

上层方法在调用 getCartItemDetail() 方法时,会传入一个 Int 类型的 cartId 参数,如 2022。上述的这个请求地址就变成了 http://newbee-cloud-shopcart-service/shop-cart/2022。

读者看明白了吗?OpenFeign 组件的整合其实与之前使用 RestTemplate 这类工具的整合并没有太大区别,只是 OpenFeign 会让服务通信的编码工作变得像方法调用一样简单。在项目启动时,OpenFeign 自己生成了一些代理对象,利用这些增强的代理 Bean 来完成请求处理。先是负责解析 @FeignClient 标注接口类中这些方法的请求地址,然后依然获取负载均衡器、解析服务地址、发起请求、处理响应。与未整合 OpenFeign 组件之前相比,又多了解析 FeignClient 接口中方法调用时的服务请求地址这个步骤。

关于源码解析,可以参考笔者在前几章中的步骤。看一下与 OpenFeign 组件相关的自动配置流程中做了什么操作,请求又是如何被 OpenFeign 接管的。因篇幅有限,这里就不再赘述了。不过有一个类读者可以重点关注一下——FeignBlockingLoadBalancerClient,打一下断点,之后根据这个类去看一下向其他服务发出请求后的方法调用栈。这个类的名称也很眼熟,读者是否能够联想到之前介绍过的一个类?对!BlockingLoadBalancerClient 类。

其实源码分析到最后,会发现请求流程仍然是建立在第 5~7 章中介绍过的知识上。微服务架构的核心基础知识是服务通信和服务治理,OpenFeign 组件或类似组件的出现及整合,大大地方便了开发人员编码,使用起来逻辑也变得更加清晰。不过底层依然还是那些知识点,只是在原有知识的基础上多做了一些封装。

任它东西南北风,我自岿然不动。