7.11 SpringBoot实战 全局异常处理 - 深入细节详解

news2024/11/25 4:21:39

CSDN成就一亿技术人

文章目录

  • 前言
  • 一、异常分类
    • 1.1 业务异常
    • 1.2 参数校验异常
    • 1.3 通用异常兜底
  • 二、保留异常现场
    • 2.1 请求地址
    • 2.2 请求header
    • 2.3 请求参数+body
    • 2.4 构建异常上下文消息
  • 最后


前言

全局异常处理, 你真的学会了吗?

学完上文,你有思考和动手实践吗?

上文咱们主要讲的是全局异常处理机制,说句实在话,如果没有人带你,即使你掌握了机制,也未必能玩转异常处理!异常处理真的很重要,所以本文带大家在图书实战项目中落地!非常深入,非常细节,非常详细!你绝对没看过这么全的,最后有源码齐全可直接Copy!

我们的重点是利用全局异常处理机制来为我们好好服务,达到异常为我、我爱异常

上文地址:7.10 SpringBoot实战 全局异常处理


一、异常分类

对于@ExceptionHandler,如果你只定义一个@ExceptionHandler(Exception.class)未免过于粗!

但是,如果你把所有异常都加一个@ExceptionHandler,又未免过于太细!没有必要!

所以,我们将需要【独立解析的异常】归为一类,统一处理!

1.1 业务异常

这里说的业务异常,不是JDK或第三方类库封装的异常类,而是由你自定义,并由你主动抛出的异常,可能是一个,也可能是N个,具体取决于你业务的复杂度!

本项目目前只需要先定义一个业务异常:BizException

我们在业务逻辑校验不通过时,统一抛出该异常,并且统一在全局异常处理该异常!

这正是我对于【7.1】中如何优雅处理的答案!你懂了吗? 7.1「实战」图书录入和修改API --如何优雅处理校验逻辑?

在这里插入图片描述

因为BizException可能在项目中任意地方抛出,所以需要将此类定义在common

注意, 业务异常是在运行时由我们主动抛出,属于运行时异常,所以继承自RuntimeException

/**
 * 业务异常类
 *
 * @author 天罡gg
 * @date 2023/8/27
 **/
public class BizException extends RuntimeException {

    private String code;

    public BizException(String message) {
        this("400", message);
    }

    public BizException(String message, Throwable cause) {
        this("400", message, cause);
    }

    public BizException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BizException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public String getCode() {
        return this.code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

上面这些代码比较基础,message在父类已定义,所以主要定义了一个code,并实现了4个构造函数重载,以适用于不同的业务场景调用!

你可以根据你的业务定义不同的BizException,增加不同的参数!

然后,我们在GlobalExceptionHandler中通过@ExceptionHandler(BizException.class)通用处理!

@ExceptionHandler(BizException.class)
public TgResult handleBizException(BizException e) {
    log.warn("BizException", e);
    return TgResult.fail(e.getCode(), e.getMessage());
}

1.2 参数校验异常

除了业务异常,通常还有一类必须处理的异常:参数校验异常!

在springboot中,在controller层通常都是基于注解的参数校验!这部分目前我们还没有在项目中应用,这是不够健壮性的,所以在后面也会安排讲这部分!我们先处理校验失败抛出的异常!

校验失败会抛出:BindExceptionMethodArgumentNotValidException,至于为什么不做展开!

@ExceptionHandler(BindException.class)
public TgResult handleBindException(BindException e) {
    StringBuilder sb = new StringBuilder();
    e.getBindingResult().getAllErrors().forEach(error -> {
        sb.append(error.getDefaultMessage()).append("\r\n");
    });
    log.warn("BindException:{}", sb, e);
    return TgResult.fail("400", sb.toString());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public TgResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    StringBuilder sb = new StringBuilder();
    e.getBindingResult().getAllErrors().forEach(error -> {
        sb.append(error.getDefaultMessage()).append("\r\n");
    });
    log.warn("MethodArgumentNotValidException:{}", sb, e);
    return TgResult.fail("400", sb.toString());
}

1.3 通用异常兜底

这个兜底就是我们上文加过的@ExceptionHandler(Exception.class),所有异常通吃,所以用这个兜底!

本文以此3类抛转引玉,相信能解决大部分场景!如果超出处理范围,原则是当你发现通过@ExceptionHandler(Exception.class)无法解析出想要的信息时,就可以定义新的@ExceptionHandler(XXX.class)!


二、保留异常现场

解决BUG就像破案一样,通过异常反推,总有一些诡异的异常,绞尽脑汁,让你想破了天,可能依然摸不着头脑,但是如果测试人员能够复现,那么你解决起来就会水到渠成!认同的,点个赞 (≧▽≦)/

那么如何才能不依赖测试人员,只靠自己就能复现问题呢?

今天再教你实用一招,让你以后Happy的解决异常,那就是保留好异常现场,或者说是现场还原!

难的不会,会的不难,主要使用 HttpServletRequest 记录这一次Http请求的3大部分:请求地址、请求header、请求参数

实际上,在@RestControllerAdvice中,我们依然可以在@ExceptionHandler修饰的方法参数上加入HttpServletRequest,例如:

在这里插入图片描述

2.1 请求地址

  • 获取API的请求地址request.getRequestURI()

  • 获取API的请求方法通过:request.getMethod()

2.2 请求header

  • 获取指定header的值:request.getHeader

    规范的程序,我们在请求报文中定义的header都是固定的,所以只需要按header来获取值即可!

    例如本项目有个header叫tgCsrfToken,就这样获取:

    `request.getHeader("tgCsrfToken")`
    
  • 获取全部header:request.getHeaderNames

    Enumeration<String> headers = request.getHeaderNames();
    StringBuilder sbAllHeaders = new StringBuilder();
    sbAllHeaders.append("headers:\r\n");
    while (headers.hasMoreElements()) {
        String headerKey = headers.nextElement();
        String headerValue = request.getHeader(headerKey);
        sbAllHeaders.append(headerKey+":"+headerValue+"\r\n");
    }
    

2.3 请求参数+body

  • 获取拼接地址上的参数:request.getParameterMap()
  • 获取body的参数:request.getReader()

不过此时使用getReader()会报异常:getInputStream() has already been called for this request

原因是因为流总是向前的,只可以读取一次,所以要反复使用,需提前缓存body,以达到反复使用的目的。

解决方案是使用Filter,在doFilter时传入我们缓存的的HttpServletRequestWrapper,具体的实现:

CacheBodyFilter,优先级最高的过滤器、只执行一次,== 目的是将HttpServletRequest包装成CacheBodyHttpServletRequestWrapper ==

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@WebFilter(filterName = "CacheBodyFilter", urlPatterns = "/*")
@Component
public class CacheBodyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        CacheBodyHttpServletRequestWrapper servletRequest = new CacheBodyHttpServletRequestWrapper(httpServletRequest);
        filterChain.doFilter(servletRequest, httpServletResponse);
    }
}

CacheBodyHttpServletRequestWrapper 缓存body

public class CacheBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    public CacheBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.body = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CacheBodyServletInputStream(this.body);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.body);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
    }

    public byte[] getBody() {
        return body;
    }

    public static class CacheBodyServletInputStream extends ServletInputStream {
        private final InputStream cacheBodyInputStream;

        public CacheBodyServletInputStream(byte[] cachedBody) {
            this.cacheBodyInputStream = new ByteArrayInputStream(cachedBody);
        }

        @Override
        public int read() throws IOException {
            return cacheBodyInputStream.read();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    }
}

2.4 构建异常上下文消息

如何获取都有了,那么我们加一个方法来构建消息吧~

/**
 * 构建异常上下文消息
 **/
private String buildContextMessage(HttpServletRequest request) {
    // 请求地址
    String url = request.getRequestURI();
    String method = request.getMethod();
    // 获取指定header
    // String oneHeader = request.getHeader("tgCsrfToken");
    // 获取全部header
    Enumeration<String> allHeaders = request.getHeaderNames();
    StringBuilder sbAllHeaders = new StringBuilder();
    while (allHeaders.hasMoreElements()) {
        String headerKey = allHeaders.nextElement();
        String headerValue = request.getHeader(headerKey);
        sbAllHeaders.append(headerKey).append(":").append(headerValue).append("\r\n");
    }
    // 请求参数
    String parameterMap = request.getParameterMap().toString();
    // 获取body
    String body = null;
    if (request instanceof CacheBodyHttpServletRequestWrapper) {
        CacheBodyHttpServletRequestWrapper wrapper = (CacheBodyHttpServletRequestWrapper) request;
        body = new String(wrapper.getBody());
    }
    return String.format("url:%s, method:%s, headers:%s, parameterMap:%s, body:%s"
                         , url, method, sbAllHeaders.toString(), parameterMap, body);
}

最终调用的完整代码如下:

// 业务异常 ===========================================
@ExceptionHandler(BizException.class)
public TgResult handleBizException(HttpServletRequest request, BizException e) {
    String contextMessage = buildContextMessage(request);
log.warn("BizException:code:{}, message:{}, contextMessage:{}", e.getCode(), e.getMessage(), contextMessage, e);
return TgResult.fail(e.getCode(), e.getMessage());
}

// 参数校验异常 ===========================================
@ExceptionHandler(BindException.class)
public TgResult handleBindException(HttpServletRequest request, BindException e) {
    StringBuilder sb = new StringBuilder();
e.getBindingResult().getAllErrors().forEach(error -> {
    sb.append(error.getDefaultMessage()).append("\r\n");
});
String contextMessage = buildContextMessage(request);
log.warn("BindException: message:{}, contextMessage:{}", sb, contextMessage, e);
return TgResult.fail("400", sb.toString());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public TgResult handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
    StringBuilder sb = new StringBuilder();
e.getBindingResult().getAllErrors().forEach(error -> {
    sb.append(error.getDefaultMessage()).append("\r\n");
});
String contextMessage = buildContextMessage(request);
log.warn("MethodArgumentNotValidException: message:{}, contextMessage:{}", sb, contextMessage, e);
return TgResult.fail("400", sb.toString());
}

// 通用异常兜底 ===========================================
@ExceptionHandler(Exception.class)
public TgResult handleException(HttpServletRequest request, Exception e) {
    String contextMessage = buildContextMessage(request);
log.warn("Exception: message:{}, contextMessage:{}", e.getMessage(), contextMessage, e);
return TgResult.fail("500", "服务器内部错误");
}

最后

看到这,觉得有帮助的,刷波666,投个票,感谢大家的支持~

想要看更多实战好文章,还是给大家推荐我的实战专栏–>《基于SpringBoot+SpringCloud+Vue前后端分离项目实战》,由我和 前端狗哥 合力打造的一款专栏,可以让你从0到1快速拥有企业级规范的项目实战经验!

具体的优势、规划、技术选型都可以在《开篇》试读!

订阅专栏后可以添加我的微信,我会为每一位用户进行针对性指导!

另外,别忘了关注我:天罡gg ,怕你找不到我,发布新文不容易错过: https://blog.csdn.net/scm_2008

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

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

相关文章

stm32之25.FLASH闪存

打开标准库 源码--- int main(void) {uint32_t d;Led_init();key_init();/* 初始化串口1波特率为115200bps&#xff0c;若发送/接收数据有乱码&#xff0c;请检查PLL */usart1_init(115200);printf("this is flash test\r\n");/* 解锁FLASH&#xff08;闪存&#xf…

腾讯云服务器搭建网站详细教程_2023更新

使用腾讯云服务器搭建网站全流程&#xff0c;包括轻量应用服务器和云服务器CVM建站教程&#xff0c;轻量可以使用应用镜像一键建站&#xff0c;云服务器CVM可以通过安装宝塔面板的方式来搭建网站&#xff0c;腾讯云服务器网分享使用腾讯云服务器建站教程&#xff0c;新手站长搭…

系统架构设计高级技能 · Web架构

现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wings, let the dream fly in reality. 点击进入系列文章目录 系统架构设计高级技能 Web架构 一、Web架构介绍1.1 Web架构涉及技术1.2 单台服务…

几个nlp的小任务(生成任务(摘要生成))

几个nlp的小任务生成任务——摘要生成 安装库选择模型加载数据集展示数据集数据预处理 tokenizer注意特殊的 token处理组成预处理函数调用map,对数据集进行预处理微调模型,设置参数设置数据收集器,将处理好的数据喂给模型封装测评方法将参数传给 trainer,开始训练安装库 选…

交叉熵的简单理解:真实分布与非真实分布的交叉,完全对应,熵为0

目录 交叉熵的简单理解&#xff1a;真实分布与非真实分布的交叉&#xff0c;完全对应&#xff0c;熵为0 交叉熵的简单理解&#xff1a;真实分布与非真实分布的交叉&#xff0c;完全对应&#xff0c;熵为0 这个式子就是熵的表达式. 简单来说, 其意义就是在最优化策略下, 猜到颜…

哪些自主品牌「霸榜」30万元向上战场?硬派越野/MPV再助力

占乘用车市场不到20%份额的30万元以上价位&#xff0c;一直以来都是合资品牌的天下。现在&#xff0c;三家中国本土自主品牌已经率先突围。 高工智能汽车研究院监测数据显示&#xff0c;2023年1-7月&#xff0c;理想、比亚迪、蔚来进入30万元以上价位新车交付量TOP10&#xff…

c++系列之指针

今天不是做题系列&#xff0c;是知识系列啦。 说到指针&#xff0c;我们初学这一定会气的牙痒痒把&#xff0c;笔者也是&#xff0c;这么我好久而不得呀&#xff0c;今天来让我们聊聊指针。 其一 首先&#xff0c;我们明确的知道&#xff0c;假如我们开一个变量&#xff0c;…

Android studio 软件git使用

在 test 分支添加的方法 , 现在切换到 master分支 总共 2 个分支 , 当前的分支是 test 出现了 先试一下 force checkout , 尝试之后发现 , 你更改没有带过来 , 以为哪个类在master分支没有 , 所以这边也没有 , 切回分支 test 发现之前的跟改没有 , 这样即可以找回 继续切换…

SE5 - BM1684 人工智能边缘开发板入门指南 -- 模型转换、交叉编译、yolov5、目标追踪

介绍 我们属于SoC模式&#xff0c;即我们在x86主机上基于tpu-nntc和libsophon完成模型的编译量化与程序的交叉编译&#xff0c;部署时将编译好的程序拷贝至SoC平台&#xff08;1684开发板/SE微服务器/SM模组&#xff09;中执行。 注&#xff1a;以下都是在Ubuntu20.04系统上操…

element-plus指定el-date-picker的弹出框位置

此处记录一下,通过popper-options指定popper出现的位置

Presto之Driver个数

一. 前言 在Presto的Stage Performace中&#xff0c;每个Operator中都会有Driver个数的显示&#xff0c;如下图所示。本文主要介绍Presto中是如何决定Driver的个数的。 二. Driver个数 在Presto中&#xff0c;一个pipeline中启动多少个Driver&#xff0c;是由此Pipeline处理的S…

tidb数据库5.4.3和6.5.3版本性能测试对比

作者&#xff1a; qizhining 原文来源&#xff1a; https://tidb.net/blog/5454621f 一、测试需求&#xff1a; 基于历史原因&#xff0c;我们的业务数据库一直使用5.4.3&#xff0c;最近由于研发提出需求&#xff1a;需要升级到6.5.3版本&#xff0c;基于版本不同&#x…

sql:SQL优化知识点记录(四)

&#xff08;1&#xff09;explain之ref介绍 type下的ref是非唯一性索引扫描具体的一个值 ref属性 例如&#xff1a;ti表先加载&#xff0c;const是常量 t1.other_column是个t1表常量 test.t1.ID&#xff1a;test库t1表的ID字段 t1表引用了shared库的t2表的col1字段&#x…

基于黑寡妇算法优化的BP神经网络(预测应用) - 附代码

基于黑寡妇算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于黑寡妇算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.黑寡妇优化BP神经网络2.1 BP神经网络参数设置2.2 黑寡妇算法应用 4.测试结果&#xff1a;5.Matlab代…

JUC常用并发工具类

JUC常用并发工具类 1、什么是JUC? JUC 就是 java.util.concurrent 包&#xff0c;这个包俗称 JUC&#xff0c;里面都是解决并发问题的一些东西&#xff0c;该包的位置位于 java 下 面的 rt.jar 包下面。 2、4大常用并发工具类 2.1 CountDownLatch CountDownLatch&#x…

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

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

WebGL矩阵变换

目录 变换矩阵&#xff1a;旋转 变换矩阵&#xff1a;平移 44的旋转矩阵 示例代码&#xff1a; gl.uniformMatrix4fv&#xff08;&#xff09;规范 平移&#xff1a;相同的策略 变换矩阵&#xff1a;缩放 变换矩阵&#xff1a;旋转 对于简单的变换&#xff0c;你可以使用…

【坑】Vue中带有__ob__: Observer的数组无法遍历的问题

控制台可以打印出数据但是渲染不出结构 解决办法&#xff1a; setTimeout(() > {Bus.$emit(shareRes, this.result.filter(item > item.id id)) }, 500)替换 Bus.$emit(shareRes, this.result.filter(item > item.id id))总结 解决和总结 好像和__ob__.Observe无…

电路原理分析2:应急照明灯电路

k是线圈&#xff0c;1-2&#xff08;常开&#xff09;和2-3&#xff08;常闭&#xff09;是2个触点。 1、220v交流电正常供电时&#xff0c;变压器触头位置提供12v的电压&#xff0c;这个时候&#xff0c;v2二极管是导通状态&#xff0c;所以线圈k吸合&#xff0c;这个时候1-2…