Feign在实际项目中使用详解

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.mfbz.cn/a/93000.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4.网络设计与redis、memcached、nginx组件(一)

网络组件系列文章目录 第四章 网络设计与redis、memcached、nginx组件 文章目录 网络组件系列文章目录文章的思维导图前言一、网络相关的问题&#xff0c;网络开发中要处理那些问题&#xff1f;网络操作IO连接建立连接断开消息到达消息发送网络操作IO特性 二、网络中IO检测IO函…

springboot整合rabbitmq死信队列

springboot整合rabbitmq死信队列 什么是死信 说道死信&#xff0c;可能大部分观众大姥爷会有懵逼的想法&#xff0c;什么是死信&#xff1f;死信队列&#xff0c;俗称DLX&#xff0c;翻译过来的名称为Dead Letter Exchange 死信交换机。当消息限定时间内未被消费&#xff0c;…

上门服务系统|上门服务小程序如何提升生活质量?

上门服务其实就是本地生活服务的升级&#xff0c;上门服务包含很多行业可以做的。例如&#xff1a;厨师上门、上门家电维修、跑腿等等。如今各类本地化生活服务越来越受大家的喜爱。基于此市场愿景&#xff0c;我们来谈谈上门服务系统功能。 一、上门服务系统功能 1、预约服务…

Go 第三方库引起的线上问题、如何在线线上环境进行调试定位问题以及golang开发中各种问题精华整理总结

Go 第三方库引起的线上问题、如何在线线上环境进行调试定位问题以及golang开发中各种问题精华整理总结。 01 前言 在使用 Go 语言进行 Web 开发时&#xff0c;我们往往会选择一些优秀的库来简化 HTTP 请求的处理。其中&#xff0c;go-resty 是一个被广泛使用的 HTTP 客户端。…

Jetpack Compose UI架构

Jetpack Compose UI架构 引言 Jetpack Compose是我职业生涯中最激动人心的事。它改变了我工作和问题思考的方式&#xff0c;引入了易用且灵活的工具&#xff0c;几乎可轻松实现各种功能。 早期在生产项目中尝试了Jetpack Compose后&#xff0c;我迅速着迷。尽管我已有使用Co…

信息化发展2

信息系统生命周期 1 、软件的生命周期通常包括&#xff1a;可行性分析与项目开发计划、需求分析、概要设计、详细设计、编码、测试、维护等阶段。 2 、信息系统的生命周期可以简化为&#xff1a;系统规划&#xff08;可行性分析与项目开发计划&#xff09;&#xff0c;系统分析…

基于Pytorch的神经网络部分自定义设计

一、基础概念&#xff08;学习笔记&#xff09; &#xff08;1&#xff09;训练误差和泛化误差[1] 本质上&#xff0c;优化和深度学习的目标是根本不同的。前者主要关注的是最小化目标&#xff0c;后者则关注在给定有限数据量的情况下寻找合适的模型。训练误差和泛化误差通常不…

机器学习十大算法之七——随机森林

0 引言 集成学习&#xff08;ensemble learning&#xff09;是时下非常流行的机器学习算法&#xff0c;它本身不是一个单独的机器学习算法&#xff0c;而是通过在数据上构建多个横型&#xff0c;集成所有模型的建模结果&#xff0c;基本上所有的机器学习领域都可以看到集成学习…

Docker部署gogs仓库

Docker部署gogs Git仓库 拉取镜像 docker pull gogs/gogs查看本地镜像 docker images启动gogs仓库服务 创建数据挂在目录 我在/root目录下创建gogs挂在目录 mkdir gogs启动gogs docker run --namegogs -d -p 10022:22 -p 10880:3000 -v /root/gogs:/data gogs/gogs10022…

破除“中台化”误区,两大新原则考核中后台

近年来&#xff0c;“中台化”已成为许多企业追求的目标&#xff0c;旨在通过打通前后台数据和业务流程&#xff0c;提升运营效率和创新能力。然而&#xff0c;在实施过程中&#xff0c;一些误解可能导致“中台化”未能如预期般发挥作用。本文将探讨这些误解&#xff0c;并提出…

兄弟,王者荣耀的段位排行榜是通过Redis实现的?

目录 一、排行榜设计方案1、数据库直接排序2、王者荣耀好友排行 二、Redis实现计数器1、什么是计数器功能&#xff1f;2、Redis实现计数器的原理&#xff08;1&#xff09;使用INCR命令实现计数器&#xff08;2&#xff09;使用INCRBY命令实现计数器 三、通过Redis实现“王者荣…

Pycharm链接远程mysql报错

Pycharm链接远程mysql配置及相应报错如下&#xff1a; 解决方法&#xff1a; 去服务器确认Mysql版本号&#xff1a; 我的Mysql为5.7.43&#xff0c;此时Pycharm mysql驱动为8.0版本&#xff0c;不匹配&#xff0c;所以需要根据实际的版本选择对应的驱动&#xff1b;选择对应的版…

【Java架构-包管理工具】-Maven私服搭建-Nexus(三)

本文摘要 Maven作为Java后端使用频率非常高的一款依赖管理工具&#xff0c;在此咱们由浅入深&#xff0c;分三篇文章&#xff08;Maven基础、Maven进阶、私服搭建&#xff09;来深入学习Maven&#xff0c;此篇为开篇主要介绍Maven私服搭建-Nexus 文章目录 本文摘要1. Nexus安装…

Mr. Cappuccino的第64杯咖啡——Spring循环依赖问题

Spring循环依赖问题 什么是循环依赖问题示例项目结构项目代码运行结果 Async注解导致的问题使用Lazy注解解决Async注解导致的问题开启Aop使用代理对象示例项目结构项目代码运行结果 Spring是如何解决循环依赖问题的原理源码解读 什么情况下Spring无法解决循环依赖问题 什么是循…

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

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

Kotlin协程flow的debounce参数timeoutMillis特性

Kotlin协程flow的debounce参数timeoutMillis特性 <dependency><groupId>org.jetbrains.kotlinx</groupId><artifactId>kotlinx-coroutines-core</artifactId><version>1.7.3</version><type>pom</type></dependency&…

error: can‘t find Rust compiler

操作系统 win11 pip install -r requirements.txt 报错如下 Using cached https://pypi.tuna.tsinghua.edu.cn/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl (95 kB) Building wheels for collected p…

聚观早报 | 云鲸扫拖机器人J4体验;芯科科技第三代无线开发平台

【聚观365】8月24日消息 云鲸扫拖机器人J4体验 芯科科技推出第三代无线开发平台 英伟达与VMWare宣布扩大合作 万物新生&#xff08;爱回收&#xff09;2023年二季度财报 充电桩需求增长带动汽车后服务市场 云鲸扫拖机器人J4体验 家庭卫生清洁是每个人都无法回避的事情&am…

老网工的爱情故事二:从VPN到SD-WAN,爱情与技术的升华

— 前言 — 为什么爱情不能像设置VLAN一样 把不同的“IP”的人绑在一起&#xff1f; 为什么周围的事物 不能像创建ACL那样随心所欲的控制&#xff1f; 为什么相爱的人远在天涯 不能像做VPN一样拉到近在咫尺&#xff1f; 为什么你我之间没有一个边界路由呢&#xff1f; 我已经给…

ISIS路由协议

骨干区域与非骨干区域 凡是由级别2组建起来的邻居形成骨干区域&#xff1b;级别1就在非骨干区域&#xff0c;骨干区域有且只有一个&#xff0c;并且需要连续&#xff0c;ISIS在IP环境下目前不支持虚链路。 路由器级别 L1路由器只能建立L1的邻居&#xff1b;L2路由器只能建立L…