news 2026/8/15 10:29:08

SpringBoot_1:业务异常与全局异常处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot_1:业务异常与全局异常处理

目录

一、构建Result统一结果封装

二、定义业务异常

三、定义全局异常处理

四、全局异常捕获底层原理

五、总结

1、请求到达

2、异常捕获

3、异常处理入口

4、责任链模式遍历解析器

5、核心解析:ExceptionHandlerExceptionResolver

6、返回响应


在做业务开发时,我们希望将预期异常捕获并返回给前端,反馈给用户并让用户按照异常信息进行重试。业务异常区分于系统异常,能让用户明白到底是自身操作的问题,还是系统崩溃了。

因此,异常处理就显得非常重要。我们先学习如何定义异常并捕获异常,然后再学习底层原理。

一、构建Result统一结果封装

不同的异常信息格式有所不同,我们希望能给前端返回统一的格式进行解析。

所以需要先在common中定义Result统一结果返回。

package com.miao.user.common; import lombok.Data; @Data public class Result<T> { // 响应码:200成功,其他代表业务错误 private Integer code; // 提示信息 private String msg; // 返回数据 private T data; // 成功静态方法 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } // 不带数据成功 public static <T> Result<T> success() { return success(null); } // 业务失败 public static <T> Result<T> fail(Integer code,String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); result.setData(null); return result; } }

我们这里使用到了@Data注解,其功能就是不用手写set和get方法,使用这个注解需要引入Lombok依赖。

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

optional设置为true表示,不需要将此依赖传递给其他项目。比如当其他项目引入本项目时,如果设置成false会强制要求下载这个jar包。Lombok只在编译期工作,编译时生成我们需要的方法,编译后运行时就直接使用这些方法所以Lombok的职责就结束了。

我们尝试编译发现:

set和get方法,以及equals方法,hashCode,toString方法都给我们生成好了。

定义好Result后,就开始定义我们的业务异常。

二、定义业务异常

自定义业务异常拥有两个属性,一个是错误码code,另一个是错误信息。我们在抛出异常的同时,需要将错误码和错误信息同时交给Resul进行处理。

随着业务的扩展,方便开发者快速定位错误,我们考虑将错误码,错误信息升级成一个枚举类。每一个错误码都对应着一段错误信息,涵盖了我们业务的方方面面。同时我们还兼容原来的方式,如果枚举类里的错误信息过于模糊,可以手动指定错误信息的内容。

因此枚举类可以构建如下:

package com.miao.user.common; import lombok.Getter; @Getter public enum ErrorCodeEnum { //业务异常 //用户异常 USER_NOT_EXIST(1001,"用户不存在"), USER_PASSWORD_ERROR(1002,"用户名密码错误"); // 错误码 private final Integer code; // 错误信息 private final String msg; ErrorCodeEnum(Integer code, String msg) { this.code = code; this.msg = msg; } }

我们使用了@Getter注解,这也是Lombok依赖提供的一个注解,帮我们只生成get方法,而无需生成set方法。因为两个属性code和msg都是fianl类型,而且都已提前定义好,无需设置。

接着我们定义业务异常,在exception中定义BuisnessException。

package com.miao.user.exception; import com.miao.user.common.ErrorCodeEnum; public class BusinessException extends RuntimeException { private final Integer code; //方式1:只传错误信息,错误码默认为500客户端报错 public BusinessException(String message) { super(message); this.code = 500; } //方式2:使用错误枚举 public BusinessException(ErrorCodeEnum errorCodeEnum) { super(errorCodeEnum.getMsg()); this.code = errorCodeEnum.getCode(); } //方式3:借助错误枚举的错误码,晚上错误信息 public BusinessException(ErrorCodeEnum errorCodeEnum, String message) { super(message); this.code = errorCodeEnum.getCode(); } public Integer getCode() { return code; } }

我们现在可以试试能否手动抛出我们想要的异常,并打印异常信息。

忽略掉我最上方的MockBean,只看test,可以看到我们的业务异常被成功捕获了。

那么我们想能否将其结合起来,封装到Result中呢 。

我们首先明确,Result封装是在Controller层做的事情,那么也就意味着想在这层抛出错误,底层就不能捕获,必须在controller层捕获才行。所以我们可以这样模拟。

三、定义全局异常处理

其实我们以及清楚了一件事情,Result统一结果封装是在controller层返回的,包括成功和失败的情况。但是我们只会写返回成功的情况,那失败的情况怎么反应呢?如果要写失败的情况的话,那每个controller方法都得try-catch,很麻烦,我们想到只要异常留在controlelr层进行处理,然后有个自动捕获的异常的工具就好了。

我们先思考一个问题,为什么说要让异常在controller层被捕获,service层,其他地方抛出的异常为什么不去处理。

举个例子,比如service层抛出异常,这个异常会抛向哪里?会沿着调用栈往上抛。

在MVC模型中,controller->service->mapper,异常可以发生在任何一处,假如mapper中数据写入发生异常,那么会创建一个异常对象,沿着mapper->service->controller抛出。所以我们不能在service层捕获异常,要让异常能抛到controller层,然后在controller层再抛出并被全局异常捕获。

那么我们就需要配置全局异常处理了。

在exception包下创建GlobalExceptionHandler处理类

package com.miao.user.exception; import com.miao.user.common.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @Slf4j public class GlobExceptionHandler { // 优先捕获自定义异常 @ExceptionHandler(BusinessException.class) public Result<?> handleBusinessException(BusinessException e) { log.error("【系统业务异常】 ",e); return Result.fail(e.getCode(),e.getMessage()); } // 其次捕获其他运行异常 @ExceptionHandler(RuntimeException.class) public Result<?> handleRuntimeException(RuntimeException e) { log.error("【系统运行时异常】 ",e); return Result.fail(500,e.getMessage()); } // Exception兜底 @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("【系统兜底异常】 ",e); return Result.fail(500,e.getMessage()); } }

注意我们的书写顺序和执行顺序无关,spring会先去捕获范围更小的异常:业务异常。匹配异常类型是否为BusinessExcepion,如果不是再去匹配其他异常类型。

其次我们需要log.eror时选择打印堆栈的方法,不然看不到堆栈信息,找不到错误源头。

这里贴一张Java异常分类图

值得注意的是,我么用了@Slf4j和@ControllerAdvice这两个注解,前者来自于Lombok后者来自于spring-boot-starter-web这些依赖我们一开始已经引入了,所以无需再引入。

稍后我们去学习全局异常捕获的机制,现在测试一下效果。

考虑这样一个场景:controller->service,service抛出异常,异常在controller处捕获。

预期会分别抛出业务异常、运行时异常,IO异常。

我们编写controller

我们希望,从controller抛出的异常都被全局异常捕获。

测试一下。

这样错误信息,和堆栈都能看到了,用户也能拿到必要的错误信息。

四、全局异常捕获底层原理

我们现在肯定的一件事是:service层及其其他层的异常都统一向上抛了,最后从controller层抛出并被全局异常GlobalException捕获。而且我们知道,@RestControllerAdvice注解的作用肯定是给每个controller接口等价“包裹上了try-catch”。那么我们想具体深入了解,@RestControllerAdvice到底是怎么做到呢?我们并没有去手动给每一个controller方法都添加try-catch却能实现这样的效果。

从@RestControllerAdvice进去后,我们发现这个注解是一个复合注解,它由@ControllerAdvice + @ResponseBody组成,后者我们熟悉,这在controller往前端返回时告诉前端浏览器返回的是数据而非视图。那么@ControllerAdvice呢?

先看DispatcherServlet.doDispatch()方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... try { try { ModelAndView mv = null; Exception dispatchException = null; try { ...// 调用controller mv = ha.handle(processedRequest, response,mappedHandler.getHandler()); } catch (Exception ex) { dispatchException = ex; //将异常缓存起来 } catch (Throwable err) { ... } //处理异常 this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { //处理异常时又出现异常,触发完成回调 this.triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + err, err)); } } finally { ... } }

DispatcherServlet.processDispatchResult()方法

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { // 特殊异常:已经定义好了ModelAndView直接用 if (exception instanceof ModelAndViewDefiningException) { ... mv = mavDefiningException.getModelAndView(); } else { // 普通异常调用异常处理器 Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; mv = this.processHandlerException(request, response, handler, exception); errorView = mv != null; } } ... }

DispatcherServlet.processHandlerException()

@Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { // 找到能处理的解析器 for(HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { ... } else { // 没找到继续抛 throw ex; } }

resolveException()是父类实现,具体是:AbstractHandlerExceptionResolver.resolveException()

@Nullable public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { if (!this.shouldApplyTo(request, handler)) { return null; } else { this.prepareResponse(ex, response); //子类重写 ModelAndView result = this.doResolveException(request, response, handler, ex); if (result != null) { ... } return result; } }

AbstractHandlerMethodExceptionResolver.doResolveException()

@Nullable protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { HandlerMethod var10000; if (handler instanceof HandlerMethod hm) { // 将handerl 转成 HandlerMethod var10000 = hm; } else { var10000 = null; } HandlerMethod handlerMethod = var10000; // 调用核心方法 return this.doResolveHandlerMethodException(request, response, handlerMethod, ex); }

看其子类实现ExceptionHandlerExceptionResolver.doResolveHandlerMethodException()

@Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 从缓存中找到@ExcetpionHandler方法 ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { // 找不到就交给下一个解析器 return null; } else { ... try { ... // 通过反射执行异常处理方法 Object[] arguments = new Object[]{exception}; exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { ... return null; } if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ... // 创建ModelAndView ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); ... //返回ModelAndView return mav; } } }

把结果返回给前端,整个流程不涉及AOP,不涉及代理,就是靠的是反射机制!!

五、总结

现在我们梳理一下全局异常处理的流程:

1、请求到达

HTTP 请求首先被 Servlet 容器(如 Tomcat)接收,然后交给DispatcherServlet。DispatcherServlet 的核心方法是doDispatch,负责将请求分发给对应的 Controller。

2、异常捕获

在doDispatch中,Controller的执行被包裹在try-catch中:

try { mv = ha.handle(...); // 调用 Controller } catch (Exception ex) { dispatchException = ex; // 异常暂存 }

3、异常处理入口

doDisaptch捕获异常后,调用processDispatchResult,判断有异常则进入processHandlerException

4、责任链模式遍历解析器

processHandlerException遍历handlerExceptionResolvers列表(默认3个解析器),找到第一个能处理的解析器:

1.ExceptionHandlerExceptionResolver -> 处理@ControllerAdvice

2.ResultStatusException -> 处理@ResponseStatus

3.DefaultHandlerExceptionResolver -> 处理SpringMVC内置异常

5、核心解析:ExceptionHandlerExceptionResolver

当遍历到 ExceptionHandlerExceptionResolver时,调用其doResolveHandlerMethodException方法:

第一步:查找 @ExceptionHandler 方法

Spring 从两个缓存中查找:

  • 第一级exceptionHandlerCache→ 当前 Controller 自己的@ExceptionHandler

  • 第二级exceptionHandlerAdviceCache→ 全局@ControllerAdvice中的@ExceptionHandler

查找过程就是从Map<异常类型, Method>中根据异常类型获取Method对象。

第二步:反射调用

找到Method后,通过ServletInvocableHandlerMethod.invokeAndHandle执行:

method.invoke(bean, args);

6、返回响应

@ExceptionHandler方法执行完成后:返回@ResponseBody或者视图名。

ModelAndView沿着调用链向上返回,最终通过HttpServletResponse返回给客户端。

以上就是Spring全局异常处理的所有内容,如有错误,请指出,作者会及时勘误。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/15 10:27:22

Java Web项目实战:Naver第三方登录集成与OAuth 2.0安全实践

1. 项目缘起&#xff1a;为什么需要关注Naver第三方登录的Web实现&#xff1f; 最近在做一个面向韩国市场的项目&#xff0c;对接Naver第三方登录时&#xff0c;发现了一个挺有意思的现象&#xff1a;市面上关于Naver登录的教程&#xff0c;十篇里有九篇都在讲移动端SDK&#x…

作者头像 李华
网站建设 2026/8/15 10:26:46

勒索病毒攻击链解析与防御实战指南

1. 勒索病毒攻击链全景解析 勒索病毒&#xff08;Ransomware&#xff09;作为当前网络安全领域最具破坏性的威胁之一&#xff0c;其攻击过程呈现出高度专业化的特征。典型的攻击链包含六个关键阶段&#xff1a; 1.1 初始入侵阶段 攻击者主要通过以下三种途径建立初始立足点&a…

作者头像 李华
网站建设 2026/8/15 10:26:41

PyCharm与VSCode远程开发深度对比:SSH与Dev Containers实战解析

1. 项目概述&#xff1a;为什么我们需要远程开发工具&#xff1f; 作为一名写了十几年Python的老码农&#xff0c;我经历过从本地单机开发到如今多环境、多服务器协同开发的完整变迁。早期&#xff0c;我们习惯在Windows或Mac上装好Python、配好环境&#xff0c;然后吭哧吭哧地…

作者头像 李华
网站建设 2026/8/15 10:25:22

Nginx安全配置实战:禁止IP访问与防盗链策略详解

1. 项目概述与核心价值在运维和Web开发的实际工作中&#xff0c;我们经常会遇到一些看似基础&#xff0c;但处理不当就会引发安全风险或资源滥用的问题。其中两个典型场景就是&#xff1a;服务器被用户直接用IP地址访问&#xff0c;以及站点的静态资源&#xff08;如图片、视频…

作者头像 李华
网站建设 2026/8/15 10:23:58

Niagara 4.3英文版硬盘修复工具|替代PC3000的Win平台日立硬盘维修软件

温馨提示&#xff1a;文末有联系方式 Niagara 4.3英文版硬盘维修工具简介 Niagara 4.3是一款面向专业数据恢复与硬盘固件工程师的英文版硬盘诊断与修复软件&#xff0c;全面支持日立&#xff08;HGST&#xff09;、西部数据、东芝等主流品牌硬盘&#xff0c;尤其在日立硬盘的底…

作者头像 李华