Feign在实际项目中使用详解

news2024/11/17 9:43:25

Feign在实际项目中使用详解

  • 简介
  • 一 Feign客户端应该如何提供?
  • 二 Feign调用的接口要不要进行包装?
    • 2.1.问题描述
    • 2.2.问题解决
  • 三 Feign如何抓取业务生产端的业务异常?
    • 3.1.分析
    • 3.2.Feign捕获不到异常
    • 3.3.异常被额外封装
    • 3.4.解决方案
  • 案例源码

简介

我们在平时学习中简单知道调用feign接口或者做服务降级;但是在企业级项目中使用feign时会面临以下几个问题:

  1. Feign客户端应该如何提供?
  2. Feign调用的接口要不要进行包装?
  3. Feign如何抓取业务生产端的业务异常?

一 Feign客户端应该如何提供?

feign接口到底改如何对外提供?
分析:

  1. 消费者端需要引用到这些feign接口,那么feign接口直接写在消费者项目中的话,那如果另外一个也需要feign接口那是不是又得写一遍!自然而然的就会考虑到将feign接口独立出来。谁需要feign接口谁添加相应的依赖即可。

  2. feign接口中包含实体对象。那这些实体一般情况下我们都是在provider中,通过feign接口改造时我们需要将controller中用到的实体类进行提取。可以进行如下两种方式处理
    方式一将实体类提取出来放在独立模块中,provider和feign接口分别依赖实体类模块;
    方式二将实体类放在feign接口的模块中,provider依赖这个feign模块;
    项目中按照方式一来处理的情况比较多,这样不会造成依赖到不需要使用的代码;

    在这里插入图片描述

二 Feign调用的接口要不要进行包装?

2.1.问题描述

平前后端分离项目中,后端给前端返回接口数据时一般会统一返回格式;我们的Controller基本上会是这样的:

    @GetMapping("getTest")
    public Result<TestVO> getTest() {
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return Result.success(testVO);
    

而Feign的接口定义需要跟实现类保持一致;

在这里插入图片描述
所以我们在使用这个方法的feign接口时,情况是这样的。

    @GetMapping("getContent")
    public Result<String> getContent() {
        String content=null;
        Result<TestVO> test = commentRestApi.getTest();
        if (test.isSuccess()) {
            TestVO data = test.getData();
             content = data.getContent();
        }else {
            throw new  RuntimeException(test.getMessage());
        }
        
        return Result.success(content);
    }

这里要先获取到​​Result​​​包装类,再通过判断返回结果解成具体的​​TestVO ​​对象,很明显这段代码有两个问题:

  • 每个Controller接口都需要手动使用Result.success对结果进行包
  • Feign调用时又需要从包装类解装成需要的实体对象

那项目中的接口有很多很多个,不断的做这种操作是不是太鸡肋了!!!无疑是增加了不必要的开发负担。

2.2.问题解决

优化的目标也很明确:​

  • 当我们通过Feign调用时,直接获取到实体对象,不需要额外的解装。
  • 前端通过网关直接调用时,返回统一的包装体。

这里我们可以借助​​ResponseBodyAdvice​​来实现,通过对Controller返回体进行增强,如果识别到是Feign的调用就直接返回对象,否则给我们加上统一包装结构。(SpringBoot统一封装controller层返回的结果)

新的问题: 如何识别出是Feign的调用还是网关直接调用呢?

基于自定义注解实现和基于Feign拦截器实现。

  • ​基于自定义注解实现​

    自定义一个注解,比如@ResponseNotIntercept​​,给Feign的接口标注上此注解,这样在使用ResponseBodyAdvice匹配时可以通过此注解进行匹配。
    在这里插入图片描述
    不过这种方法有个弊端,就是前端和feign没法公用,如一个接口​​user/get/{id}​​既可以通过feign调用也可以通过网关直接调用,采用这种方法就需要写2个不同路径的接口。

  • ​基于Feign拦截器实现​

    对于Feign的调用,在Feign拦截器上加上特殊标识,在转换对象时如果发现是feign调用就直接返回对象。

在这里插入图片描述
在这里插入图片描述

第二种方式具体实现步骤:

  1. 在feign拦截器中给feign请求添加特定请求头​​T_REQUEST_ID

/**
 * @ClassName: OpenFeignConfig Feign拦截器
 * @Description: 对于Feign的调用,在请求头中加上特殊标识
 * @Author: wang xiao le
 * @Date: 2023/08/25 23:13
 **/
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig implements RequestInterceptor {

    /**
     * Feign请求唯一标识
     */
    public static final String T_REQUEST_ID = "T_REQUEST_ID";


    /**
     * get请求标头
     *
     * @param request 请求
     * @return {@link Map }<{@link String }, {@link String }>
     * @Author wxl
     * @Date 2023-08-27
     **/
    private Map<String, String> getRequestHeaders(HttpServletRequest request) {
        Map<String, String> map = new HashMap<>(16);
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null != attributes) {
            HttpServletRequest request = attributes.getRequest();
            Map<String, String> headers = getRequestHeaders(request);

            // 传递所有请求头,防止部分丢失
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                requestTemplate.header(entry.getKey(), entry.getValue());
            }

            // 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
            if (request.getHeader(T_REQUEST_ID) == null) {
                String sid = String.valueOf(UUID.randomUUID());
                requestTemplate.header(T_REQUEST_ID, sid);
            }

        }
    }


}

  1. 自定义CommonResponseResult并实现ResponseBodyAdvice​​
/**
 * 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
 *
 * @RestControllerAdvice(basePackages = "com.wxl52d41")
 * @ClassName: CommonResponseResult
 * @Description: controller返回结果统一封装
 * @Author wxl
 * @Date 2023-08-27
 * @Version 1.0.0
 **/
@RestControllerAdvice
public class CommonResponseResult implements ResponseBodyAdvice<Object> {
    /**
     * 支持注解@ResponseNotIntercept,使某些方法无需使用Result封装
     *
     * @param returnType    返回类型
     * @param converterType 选择的转换器类型
     * @return true 时会执行beforeBodyWrite方法,false时直接返回给前端
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
            //若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
            return false;
        }
        if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
            //若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

        if (request.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID)) {
            //Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
            return body;
        }
        if (body instanceof Result) {
            // 提供一定的灵活度,如果body已经被包装了,就不进行包装
            return body;
        }
        if (body instanceof String) {
            //解决返回值为字符串时,不能正常包装
            return JSON.toJSONString(Result.success(body));
        }
        return Result.success(body);
    }
}

  1. 修改provider后端接口返回对象以及feign接口
    如果为Feign请求,则不做转换,否则通过Result进行包装。
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return testVO;
    }

在这里插入图片描述

  1. 修改consumer模块中feign调用逻辑
    不需要在接口上返回封装体ResultData,经由ResponseBodyAdvice实现自动增强。
   @GetMapping("getOne")
    public TestVO getOne() {
        TestVO one = commentRestApi.getOne();
        return one;
    }

  1. 测试
    在消费者端调用。发现控制台中调用feign接口返回的方法并没有被统一封装。
    在这里插入图片描述

直接通过postman调用provider层方法。发现方法被统一封装了。

在这里插入图片描述

在​正常情况下​达到了我们优化目标,通过Feign调用直接返回实体对象,通过网关调用返回统一包装体。看上去很完美,但是实际很糟糕,这又导致了第三个问题,Feign如何处理异常?

三 Feign如何抓取业务生产端的业务异常?

3.1.分析

生产者对于提供的接口方法会进行业务规则校验,对于不符合业务规则的调用请求会抛出业务异常​​BusinessException​​,而正常情况下项目上会有个全局异常处理器,他会捕获业务异常BusinessException,并将其封装成统一包装体返回给调用方,现在让我们来模拟这种业务场景:

  1. 生产者抛出业务异常
    模拟业务中名称为空
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        if (true) {
            throw new BusinessException(ResultEnum.VALIDATE_FAILED.getCode(), "名称为空");
        }
        return testVO;
    }

  1. 全局异常拦截器捕获业务异常
   /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({BusinessException.class}
    public Result<?> handleBusinessException(BusinessException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }
  1. 消费者端调用异常的feign接口
    @Resource
    CommentRestApi commentRestApi;


    @GetMapping("getOne")
    public TestVO getOne() {
        TestVO one = commentRestApi.getOne();
        System.out.println("one = " + one);
        return one;
    }

3.2.Feign捕获不到异常

  1. 观察结果
    调用consumer中getOne()方法发现返回的信息中并没有异常,data中对象字段全部设置为null,如下:
    在这里插入图片描述
    查看provider端日志确实抛出了自定义异常:
    在这里插入图片描述
    将Feign的日志级别设置为FULL查看返回结果:
    @Bean
    Logger.Level feginLoggerLevel(){
        return Logger.Level.FULL;
    }

在这里插入图片描述
通过日志可以看到Feign其实获取到了全局异常处理器转换后的统一对象Result,并且响应码为200,正常响应。而消费者接受对象为TestVO,属性无法转换,全部当作NULL值处理。

很显然,这不符合我们正常业务逻辑,我们应该要直接返回生产者抛出的异常,​那如何处理呢?​

很简单,我们只需要给全局异常拦截器中​业务异常设置一个非200的响应码​即可,如:
在这里插入图片描述

    /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({BusinessException.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handleBusinessException(BusinessException ex) {
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }

这样消费者就可以正常捕获到生产者抛出的业务异常,如下图所示:在这里插入图片描述

3.3.异常被额外封装

虽然能获取到异常,但是Feign捕获到异常后又在业务异常的基础上再进行了一次封装。

原因是​当feign调用结果为非200的响应码时就触发了Feign的异常解析,Feign的异常解析器会将其包装成FeignException,即在我们业务异常的基础上再包装一次​。

可以在​​feign.codec.ErrorDecoder#decode()​​方法上打上断点观察执行结果,如下:
在这里插入图片描述
很显然,这个包装后的异常我们并不需要,我们应该直接将捕获到的生产者的业务异常直接抛给前端,那这又该如何解决呢?

3.4.解决方案

很简单,​我们只需要重写Feign的异常解析器,重新实现decode逻辑,返回正常的BusinessException即可,而后全局异常拦截器又会捕获BusinessException!​(感觉有点无限套娃的感觉)

代码如下:

  1. 重写Feign异常解析器
/**
 * @ClassName: OpenFeignErrorDecoder
 * @Description: 解决Feign的异常包装,统一返回结果
 * @Author wxl
 * @Date 2023-08-26
 * @Version 1.0.0
 **/
@Configuration
public class OpenFeignErrorDecoder implements ErrorDecoder {
    /**
     * Feign异常解析
     *
     * @param methodKey 方法名
     * @param response  响应体
     * @return {@link Exception }
     * @Author wxl
     * @Date 2023-08-26
     **/
    @SneakyThrows
    @Override
    public Exception decode(String methodKey, Response response) {
        //获取数据
        String body = Util.toString(response.body().asReader(Charset.defaultCharset()));
        Result<?> result = JSON.parseObject(body, Result.class);
        if (!result.isSuccess()) {
            return new BusinessException(result.getStatus(), result.getMessage());
        }
        return new BusinessException(500, "Feign client 调用异常");
    }
    
}

  1. 再次调用
    provider层抛出的异常信息能够被consumer层捕获,并通过自定义的异常解析器处理成自定义异常,不再被默认的feign异常包装;抛出的自定义异常被统一返回封装处理。在这里插入图片描述在这里插入图片描述
    在这里插入图片描述

案例源码

案例源码传送带

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/936315.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Visual Studio2022史诗级更新,增加多个提高生产力的功能

Visual Studio 2022发布了17.7x版&#xff0c;这次更新中&#xff0c;增加多个提高生产力的功能以及性能进一步改进。 如果要体验新功能&#xff0c;需要将Visual Studio 2022的版本升级到17.7及以上 下面我们看看新增的功能以及改进的功能&#xff01; 目录 文件比较自动修复代…

206. 反转链表 (简单系列)

给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 示例 3&#xff1a; 输…

【数据结构与算法篇】 手撕八大排序算法之选择排序

​&#x1f47b;内容专栏&#xff1a; 《数据结构与算法篇》 &#x1f428;本文概括&#xff1a;选择排序包括直接选择排序与堆排序&#xff0c;本篇讲述直接选择排序与堆排序的思想及实现、复杂度及稳定性的分析。 &#x1f43c;本文作者&#xff1a; 花 蝶 &#x1f438;发布…

CSS中的margin与padding

目录 一、margin 1.概念及作用 2.基本语法 3.margin的用法 二、padding 1.介绍 2.基本语法及要求 3. 用法 4.内边距和元素宽度 讲这些之前&#xff0c;先看一张图&#xff0c;便于理解 一、margin 1.概念及作用 CSS margin 属性用于在任何定义的边框之外&#xff0c;…

K8S最新版本集群部署(v1.28) + 容器引擎Docker部署(上)

温故知新 &#x1f4da;第一章 前言&#x1f4d7;背景&#x1f4d7;目的&#x1f4d7;总体方向 &#x1f4da;第二章 基本环境信息&#x1f4d7;机器信息&#x1f4d7;软件信息&#x1f4d7;部署用户kubernetes &#x1f4da;第三章 Kubernetes各组件部署&#x1f4d7;安装kube…

二分队列+决策单调性优化dp:P6246

https://www.luogu.com.cn/problem/P6246 决策单调性 若 d p i dp_i dpi​ 由 j j j 转移&#xff0c;则 d p i 1 dp_{i1} dpi1​ 转移点 k k k 满足 k ≥ j k\ge j k≥j 发现决策点满足单调&#xff0c;但遍历的点不满足单调&#xff0c;不能用双指针&#xff0c;考虑…

计算机组成原理学习笔记-精简复习版

一、计算机系统概述 计算机系统硬件软件 计算机硬件的发展&#xff1a; 第一代计算机&#xff1a;(使用电子管)第二代计算机&#xff1a;(使用晶体管)第三代计算机&#xff1a;(使用较小规模的集成电路)第四代计算机&#xff1a;(使用较大规模的集成电路) 冯诺依曼体系结构…

VSCode连接服务器

Pycharm连接服务器参考我的另一篇文章Pycharm远程连接服务器_pycharm进入服务器虚拟环境终端_Jumbo星的博客-CSDN博客 本质上Pycharm和VSCode都只是IDE&#xff0c;没有什么好坏之分。但是因为Pycharm连接服务器&#xff08;准确来说是部署&#xff09;需要买professional。而…

5 群起集群

1.在启动集群之前&#xff0c;先配置workers,有几个节点就配置几个 [atguiguhadoop102 hadoop]$ vim /opt/module/hadoop-3.1.3/etc/hadoop/workers在该文件中增加如下内容&#xff1a; hadoop102 hadoop103 hadoop104 注意&#xff1a;该文件中添加的内容结尾不允许有空格&a…

权限提升-手工-系统权限提升

权限提升基础信息 1、具体有哪些权限需要我们了解掌握的&#xff1f; 后台权限&#xff0c;网站权限&#xff0c;数据库权限&#xff0c;接口权限&#xff0c;系统权限&#xff0c;域控权限等 2、以上常见权限获取方法简要归类说明&#xff1f; 后台权限&#xff1a;SQL注入,数…

检测链表中是否存在环

题目、解析和代码 题目&#xff1a;给定一个单链表&#xff0c;判断其中是否有环的存在 解析&#xff1a;这里使用两个遍历速度不一样的结点进行判断&#xff0c;一个慢结点从首结点开始遍历&#xff0c;这个结点每次只遍历一个结点&#xff1b;一个快结点从第二个结点进行遍历…

设计模式之八:迭代器与组合模式

有许多方法可以把对象堆起来成为一个集合&#xff08;Collection&#xff09;&#xff0c;比如放入数组、堆栈或散列表中。若用户直接从这些数据结构中取出对象&#xff0c;则需要知道具体是存在什么数据结构中&#xff08;如栈就用peek&#xff0c;数组[]&#xff09;。迭代器…

WinPlan经营大脑:精准预测,科学决策,助力企业赢得未来

近年,随着国内掀起数字化浪潮,“企业数字化转型”成为大势所趋下的必选项。但数据显示,大约79%的中小企业还处于数字化转型初期,在“企业经营管理”上存在着巨大的挑战和风险。 WinPlan经营大脑针对市场现存的企业经营管理难题,提供一站式解决方案,助力企业经营管理转型…

设计模式--代理模式

笔记来源&#xff1a;尚硅谷Java设计模式&#xff08;图解框架源码剖析&#xff09; 代理模式 1、代理模式的基本介绍 1&#xff09;代理模式&#xff1a;为一个对象提供一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标对象2&#xff09;这样做的好处是…

数据结构--队列与循环队列

队列 队列是什么&#xff0c;先联想一下队&#xff0c;排队先来的人排前面先出&#xff0c;后来的人排后面后出&#xff1b;队列的性质也一样&#xff0c;先进队列的数据先出&#xff0c;后进队列的后出&#xff1b;就像图一的样子&#xff1a; 图1 如图1&#xff0c;1号元素是…

软件测试考试中的环路复杂度、线性无关路径的理解

题目如下&#xff0c;回答问题1至3 int GetMaxDay (int year ,int month){int maxday0; //1if (month>1 && month <12) { //2,3if (month2) { //4if (year %4 0 ) { //5if (year %100 0) { //6if (year %400 0) {//7maxday29; //8else //9maxda…

安防行业深度报告:技术创新与格局重构

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。通过运用云计算、人工智能等新技术&#xff0c;安防行业正经历从传统安防向 AI 智慧安防转型升级的过程。这种升级将改变传统安防只能事后查证、人工决策的劣势&#xff0c;使得全程监控、智能决策成为可能。通过运用后端云…

C/C++ 个人笔记

仅供个人复习&#xff0c; C语言IO占位符表 %d十进制整数(int)%ldlong%lldlong long%uunsigned int%o八进制整型%x十六进制整数/字符串地址%c单个字符%s字符串%ffloat&#xff0c;默认保留6位%lfdouble%e科学计数法%g根据大小自动选取f或e格式&#xff0c;去掉无效0 转义符表…

epoll() 多路复用 和 两种工作模式

1.epoll API 介绍 typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ };常见的Epoll检测事件&#xff1a;- EPOLLIN- EPOLLOUT- …

C#---第十九课:不同类型方法的执行顺序(new / virtual / common / override)

本文介绍不同类型的方法&#xff0c;在代码中的执行顺序问题&#xff1a; 构造方法普通方法&#xff08;暂用common代替&#xff09;、虚方法&#xff08;Virtual修饰&#xff09;、New方法&#xff08;new修饰&#xff09;三个优先级相同overide方法&#xff08;会替换virtual…