• 微服务架构整理-(十一、SpringCloud实战之OpenFeign)


    OpenFeign介绍

    OpenFeign是 Netflix 公司开发的一个声明式的 REST 调用客户端,其前身为Feign,Feign在2019年5月停止了更新,直接转为OpenFeign,OpenFeign继承了Feign的所有特性,并且在OpenFeign中可以使用SpringMvc的注解,使其在调用服务时更方便。
    之前的博客中提到过Ribbon和Hystrix[1,2,3], 它们是SpringCloud进行微服务开发过程中经常使用到的基础组件,每次使用时两者基本都会同时出现,并且配置也是非常相似,因此相同代码很多,从而造成代码冗余,在此基础上OpenFeign整合了Ribbon和Hystrix两个组件,让开发变得更加简单,不仅在配置上大大简化了开发工作,同时还提供了一种声明式的 Web 服务客户端定义方式。
    本文给出使用OpenFeign定义消费者,并实现负责均衡和熔断的功能。

    实现消费者功能

    在之前的博文中,给出了服务提供者serive-product, 以及对应的服务消费者service-order, 这里使用OpenFeign定义一个服务消费者service-openfeign来代替service-order 。

    创建Spring Boot 工程

    springboot版本:2.2.6.RELEASE 。工程名:open_feign。启动类为:OpenFeignApplication

    添加依赖

    需要添加3个依赖,分别为eureka,hystrix,openfeign.

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
                <version>2.2.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>2.2.6.RELEASE</version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    添加注解

    启动类上添加@EnableFeignClients注解

    @SpringBootApplication
    //开启openfeign,此注解与feign注解一样
    @EnableFeignClients
    public class OpenFeignApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(OpenFeignApplication.class, args);
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    声明服务

    定义一个接口,接口中声明需要调用的服务,接口上一定要加@FeignClient注解来指定服务名称(忽略大小写),进而绑定服务,然后再通过SpringMVC 中提供的注解来绑定服务提供者提供的接口。这里以绑定SERVICE-PRODUCT中的/product/{id}为例。

    @FeignClient(value = "SERVICE-PRODUCT")
    public interface OrderService {
        @GetMapping(value = "/product/{id}")
        public Product findById(@PathVariable("id") Long id);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    SERVICE-PRODUCT中的/product/{id}接口源码如下:

    /**
     * @author :浦江之猿
     */
    @RestController
    @RequestMapping("/product")
    public class ProductController {
        @Autowired
        ProductService productService;
    
        @GetMapping(value = "/{id}")
        public Product findById(@PathVariable Long id) {
            Product product = productService.findById(id);
            product.setName(product.getName() + "1");
            return product;
        }
    
        @GetMapping(value = "/{id}/{price}")
        public Product findByIdAndPrice(@PathVariable Long id, @PathVariable BigDecimal price) {
            Product product = productService.findProductByIdAndPrice(id, price);
            product.setName(product.getName() + "1");
            return product;
        }
    
        @PostMapping(value = "/addProduct")
        public Product addProduct(@RequestParam String name, @RequestParam BigDecimal price) {
            Product product = new Product();
            product.setName(name);
            product.setPrice(price);
            return productService.addProduct(product);
        }
    
        @PutMapping(value = "/updateProduct")
        public void updateProduct(@RequestParam Long id, @RequestParam BigDecimal price) {
            Product product = productService.findById(id);
            product.setPrice(price);
            productService.updateProduct(product);
        }
    
        @DeleteMapping(value = "/deleteProduct/{id}")
        public void deleteProduct(@PathVariable Long id) {
            productService.deleteById(id);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    在Controller 中调用服务

    定义一个Controller 来调用上面的service层,可以发现service层只有接口,没有接口的实现。

    /**
     * @author :浦江之猿
     */
    @RestController
    public class OrderController {
        @Resource
        OrderService orderService;
    
        @GetMapping(value = "/buy/{id}")
        public Product findById(@PathVariable("id") Long id) {
            return orderService.findById(id);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    添加配置文件

    因为OpenFeign需要获取服务提供者且自己本身也是一个服务,因此配置文件中需要添加eureka。

    server:
      port: 9007 #端口
    spring:
      favicon:
        enabled: false
      application:
        name: service-openfeign #服务名称
    #配置eureka server
    eureka:
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
          #注册到两个eureka上
          defaultZone: http://localhost:9005/eureka/,http://localhost:9006/eureka/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    结果

    到这,整个OpenFeign客户端就搭建好了,浏览器中输入http://localhost:9007/buy/1。其结果如下:

    {
        "id": 1,
        "name": "apple2",
        "price": 8000.00
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Ok,整个服务就打通了,接下来看看OpenFeign是如何实现负载均衡功能和熔断功能的。

    实现负载均衡功能

    在声明服务创建接口时,就已经添加了负载均衡功能(通过@RequestMapping注解,可以理解为OpenFeign内置了Ribbon),只是这里的负载均衡算法用的是默认算法(轮询)。如果需要改变负载算法,则需要定义一个配置类,然后在配置类中注入需要的算法。以改成随机算法为例:

    /**
     * @author :浦江之猿
     * @date :Created in 2022/5/31 22:02
     */
    @Configuration
    public class OpenFeignConfig {
        @Bean
        public IRule getIRule() {
            return new RandomRule();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    关于负载均衡的其算法可以参考博文SpringCloud实战之Ribbon

    实现熔断功能

    OpenFeign整合了Hystrix,所有熔断原理一样,当出现异常时直接进行服务降级,回调异常处理函数。但是这里的异常处理函数与接口一一对应,不是太好理解,可以看下面的例子。OpenFeign中实现熔断需要两步:

    配置熔断开关

    配置文件中将熔断开关打开

    #熔断开关打开
    feign:
      hystrix:
        enabled: true
    
    • 1
    • 2
    • 3
    • 4

    实现回调函数

    为了更好的解释异常处理函数与接口一一对应,再声明一个服务,即在OrderService接口中再绑定一个服务提供者提供的接口。

    @GetMapping(value = "/product/{id}/{price}")
    public Product findByIdAndPrice(@PathVariable Long id, @PathVariable BigDecimal price);
    
    • 1
    • 2

    接下来实现OrderService接口,实现类中实现的每个方法就是此接口对应的熔断回调函数。注意@Component注解一定要解,不然启动失败,报找不到实例异常。

    @Component
    public class OrderServiceImpl implements OrderService {
    
        @Override
        public Product findById(Long id) {
            return handleBreakCircuit(id, "A fake product from findById");
        }
    
        @Override
        public Product findByIdAndPrice(Long id, BigDecimal price) {
            return handleBreakCircuit(id, "A fake product from findByIdAndPrice");
        }
    
        public Product handleBreakCircuit(Long id, String des) {
            Product product = new Product();
            product.setId(0L);
            product.setName(des);
            product.setPrice(new BigDecimal(0));
            return product;
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    最后需要在声明接口中回调OrderServiceImple中的函数,只需要在@FeignClient注解添加fallback属性即可,完整的OrderService如下:

    @FeignClient(value = "SERVICE-PRODUCT", fallback = OrderServiceImpl.class )
    public interface OrderService {
        @GetMapping(value = "/product/{id}")
        public Product findById(@PathVariable("id") Long id);
    
        @GetMapping(value = "/product/{id}/{price}")
        public Product findByIdAndPrice(@PathVariable Long id, @PathVariable BigDecimal price);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    相应的,在服务提供者1中针对findById选择制造一个超时(默认为1000ms),服务提供者2正常。

       @GetMapping(value = "/{id}")
        public Product findById(@PathVariable Long id) {
            Product product = productService.findById(id);
            //故意制造一个超时
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.setName(product.getName() + "1");
            return product;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    最终结果在以下两者之间随机切换:

    //服务提供者1返回的结果
    {
        "id": 0,
        "name": "A fake product from findById",
        "price": 0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    //服务提供者2返回的结果
    {
        "id": 1,
        "name": "apple2",
        "price": 8000.00
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    获取异常信息

    通过fallback可以回调服务降级函数 ,但时当服务提供抛出异常后没法获取异常信息。OpenFeign中提供了fallbackFactory属性,在fallbackFactory中实现实现OrderService接口,就可以获取异常信息。用下面的类代替上面的OrderServiceImpl不仅能够回调降级函数 ,还能获取异常信息。

    //注解不能少
    @Component
    public class OrderFallbackFactory implements FallbackFactory<OrderService> {
    
        @Override
        public OrderService create(Throwable arg0) {
            return new OrderService() {
    
                @Override
                public Product findById(Long id) {
                    return handleBreakCircuit(id, "A fake product from findById");
                }
    
                @Override
                public Product findByIdAndPrice(Long id, BigDecimal price) {
                    return handleBreakCircuit(id, arg0.getMessage());
                }
    
                public Product handleBreakCircuit(Long id, String des) {
                    Product product = new Product();
                    product.setId(0L);
                    product.setName(des);
                    product.setPrice(new BigDecimal(0));
                    return product;
                }
    
            };
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    相应的,在服务提供者2中针对findByIdAndPrice选择制造除0异常,服务提供者1正常。

        @GetMapping(value = "/{id}/{price}")
        public Product findByIdAndPrice(@PathVariable Long id, @PathVariable BigDecimal price) {
        //制造一个除0异常
            int i = 1 / 0;
            Product product = productService.findProductByIdAndPrice(id, price);
            product.setName(product.getName() + "2");
            return product;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最终结果在以下两者之间随机切换:

    //服务提供者1返回的结果
    {
        "id": 1,
        "name": "apple1",
        "price": 8000.00
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    //服务提供者2返回的结果
    {
        "id": 0,
        "name": "[500] during [GET] to [http://SERVICE-PRODUCT/product/1/8000] [OrderService#findByIdAndPrice(Long,BigDecimal)]: [{\"timestamp\":\"2022-06-02T12:51:06.846+0000\",\"status\":500,\"error\":\"Internal Server Error\",\"message\":\"/ by zero\",\"path\":\"/product/1/8000\"}]",
        "price": 0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    总结

    关于OpenFeign的基本使用就介绍完了,使用其来不是很难,细心就可以了。最后,希望本文能帮助大家,祝大家在IT之路上少走弯路,一路绿灯不堵车,测试一性通过,bug秒解!
    源码下载

  • 相关阅读:
    高通开发系列 - ALSA声卡驱动中音频通路kcontrol控件
    Python爬虫教程:从入门到实战
    我的创作纪念日
    社群管理助手有什么用
    springcloud基于Spring Cloud的在线学习平台
    【springboot整合ES】springboot整合ES
    YOLOv5模型训练及检测
    docker 部署mysql主从复制
    【js】日时分秒倒计时
    嵌入式分享合集43
  • 原文地址:https://blog.csdn.net/hongyinanhai00/article/details/125056128