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

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 {@Overrideprotected 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);}@Overridepublic ServletInputStream getInputStream() throws IOException {return new CacheBodyServletInputStream(this.body);}@Overridepublic 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);}@Overridepublic int read() throws IOException {return cacheBodyInputStream.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return true;}@Overridepublic 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");// 获取全部headerEnumeration<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();// 获取bodyString 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://xiahunao.cn/news/1620838.html

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

相关文章

苹果开源iOS和macOS内核源代码 | 十一献礼

没想到&#xff0c;国庆节这天醒来一看&#xff0c;素以“封闭”闻名的苹果公司&#xff0c;竟然在GitHub上公布了旗舰操作系统的XNU内核源代码。 XNU代表的含义是“XNU is Not Unix”&#xff08;XNU不是Unix&#xff09;&#xff0c;这是一个类似Unix的内核&#xff0c;用在各…

KSZ9897 switch以及官方驱动KSZ9897

使用KSZ9897来替换QCA8337N&#xff0c; 其中官方有针对imx8mm的驱动移植的patch代码&#xff0c;注意&#xff1a;其中已KSZ9477为git名字进行命名的。 KSZ9897&#xff0c;KSZ9477驱动 https://github.com/Microchip-Ethernet/EVB-KSZ9477.git 注意下面的配置&#xff1a;…

nyoj463

九九乘法表 时间限制&#xff1a; 1000 ms | 内存限制&#xff1a; 65535 KB 难度&#xff1a; 1 描述 小时候学过的九九乘法表也许将会扎根于我们一生的记忆,现在让我们重温那些温暖的记忆,请编程输出九九乘法表. 现在要求你输出它的格式与平常的 不同啊! 是那种反过来的三角…

高性能霍尔开关CH442PN与AKM EW-632的分析对比

意瑞的高性能霍尔锁存芯片CH442PN广泛应用于电动工具&#xff0c;工业风机&#xff0c;流量计等场合。在缺芯潮下&#xff0c;针对AKM EW632的一些需求&#xff0c;CH442PN也为广大客户提供了更多选择。 以下是两者的对比分析。 相较于EW632&#xff0c;CH442PN可以支持更宽的…

nyoj 311 和995

这两个题其实是一个解法 完全背包 时间限制&#xff1a; 3000 ms | 内存限制&#xff1a; 65535 KB 难度&#xff1a; 4 描述 直接说题意&#xff0c;完全背包定义有N种物品和一个容量为V的背包&#xff0c;每种物品都有无限件可用。第i种物品的体积是c&#xff0c;价值是w。…

comp9334-proj2

对本文有疑问可以加微信 Tutor_0914联系。也可以访问我的个人辅导网站 &#xff1a; tutoryou 解析 原文 COMP9334 Project, Term 1, 2022: Priority queueing for server farms Due Date: 5:00pm Friday 22 April 2022 Version 1.01, 20 March 2022 Updates to the projec…

AWR2243

TDA2xx-AWRx243 TI毫米波板&#xff08;代完善更新和作者的继续研究&#xff09; 1、安装mmwave studio和驱动&#xff08;链接&#xff1a; https://download.csdn.net/download/weixin_42501561/19775644 &#xff09; 2、设置网络端口IP地址&#xff08;如果不能更改路由器I…

Window基础命令

文章目录 查看哪些端口被禁用TCP协议删除开机启动项方案1方案2 查看哪些端口被禁用TCP协议 netsh interface ipv4 show excludedportrange protocoltcp删除开机启动项 方案1 列出所有启动项 bcdedit /enum仔细看你要删除的是哪一项&#xff08;看description&#xff09;&a…

Git企业开发控制理论和实操-从入门到深入(六)|多人协作开发

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

SpringBoot在IDEA里实现热部署

使用步骤 1.引入依赖 <!--devtools热部署--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional><scope>true</scope><versi…

Django基础6——数据模型关系

文章目录 一、基本了解二、一对一关系三、一对多关系3.1 增删改查3.2 案例&#xff1a;应用详情页3.2 案例&#xff1a;新建应用页 四、多对多关系4.1 增删改查4.2 案例&#xff1a;应用详情页4.3 案例&#xff1a;部署应用页 一、基本了解 常见数据模型关系&#xff1a; 一对一…

算法通过村第四关-栈青铜笔记|手写栈操作

文章目录 前言1. 栈的基础概要1.1 栈的特征1.2 栈的操作1.3 Java中的栈 2. 栈的实现&#xff08;手写栈&#xff09;2.1 基于数组实现2.2 基于链表实现2.3 基于LinkedList实现 总结 前言 提示&#xff1a;我自己一个人的感觉很好 我并不想要拥有你 除非你比我的独处更加宜人 --…

安利超实用的(cc协议)游戏3d模型素材网站

前方干货满满&#xff0c;建议先收藏再看哦&#xff01;为大家整理&#xff08;cc协议&#xff09;游戏3d模型素材&#xff0c;总有满足你需求的一款&#xff0c;除此之外&#xff0c;免费&#xff0c;资源质量好&#xff0c;一键打包下载&#xff0c;你还不心动吗&#xff1f;…

基于VHDL语言的汽车测速系统设计_kaic

摘 要 汽车是现代交通工具。车速是一项至关重要的指标。既影响着汽车运输的生产率,又关乎着汽车行驶有没有超速违章&#xff0c;还影响着汽车行驶时人们的人身安全。而伴随着我国国民的安全防范意识的逐步增强&#xff0c;人们也开始越来越关心因为汽车的超速而带来的极其严重…

在Adobe illustrator中创建线呼吸描记器

Spirographs look complex, but most of them aren’t that hard to create. As promised, here is a follow-up tutorial on how to create a spirograph, only this time it’s a different kind of spirograph than the one we’ve created before. We’ll create a line sp…

当前最强的免费AI画图、AI绘图工具-2

Midjourney比较贵&#xff0c;而且无法访问&#xff0c;Stable Diffusion部署起来很麻烦。网上有哪些可以直接在网页端或者下载的app可以实现AI画图的工具。我们整理了45个相关工具&#xff0c;这是系列2&#xff0c;收录到 当前最强的免费AI画图、AI绘图工具-2https://www.web…

AI绘画扩展安装

想问一下各位大佬&#xff0c;有没有懂这个的&#xff0c;能不能帮我解决一下这个是啥问题&#xff0c;一直安装不了&#xff0c;拜托了&#xff0c;万分感谢

AI-绘图

人工智能肯定是未来的趋势&#xff0c;随着AI的不断发展&#xff0c;ai遍及的方面肯定越来越多。今天我想推荐的是一款免费的关于绘图的AI产品——KOAYEE元宇宙。它只需根据自己提供关键词就能够生成4张不同角度风格的图片&#xff0c;并且可以自己上传图片&#xff0c;融合关键…

Adobe Illustrator 2023 for mac安装教程,可用。

Adobe Illustrator 是行业标准的矢量图形应用程序&#xff0c;可以为印刷、网络、视频和移动设备创建logos、图标、绘图、排版和插图。数以百万计的设计师和艺术家使用Illustrator CC创作&#xff0c;从网页图标和产品包装到书籍插图和广告牌。此版本是2023版本&#xff0c;适配…

adobe illustrator的描边如何选择 HSB 空间

坑 1、 2、 这个空间 如何修改成&#xff1f; H 的色调空间&#xff1f; adobe illustrator 颜色修改为色调空间 坑&#xff1b; 好像不行&#xff0c;