springboot-异常处理
背景
软件开发过程中,不可避免的是需要处理各种异常
,就我自己来说,至少有一半以上的时间都是在处理各种异常情况
,所以代码中就会出现大量的try {...} catch {...} finally {...} 代码块
,不仅有大量的冗余代码
,而且还影响代码的可读性
既然想要业务代码不显式地对异常进行捕获、处理
,而异常肯定还是处理的
,不然系统岂不是动不动就崩溃了,所以必须得有其他地方捕获并处理这些异常。
自定义异常错误页面
默认情况下,在遇到异常时,SpringBoot 会自动跳到一个统一的异常页面,Spring Boot提供/error处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
SpringBoot 默认的异常处理机制:一旦程序中出现了异常 SpringBoot 就会请求 /error 的 url 。在 SpringBoot 中提供了一个叫 BasicErrorController 来处理 /error 请求,然后跳转到默认显示异常的页面来展示异常信息。
接下来就是自定义异常错误页面了,方法很简单,就是在目录 src/main/resources/templates/ 下定义一个叫 error 的文件,可以是 jsp 也可以是 html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>自定义 springboot 异常处理页面</title>
</head>
<body>
Springboot BasicExceptionController 错误页面
<br>
<span th:text="${msg}"></span>
</body>
</html>
也可以在error文件夹下放相应状态码的页面,error/下的4xx,5xx页面会被自动解析,有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
error/下的4xx,5xx页面会被自动解析原理
系统默认的异常解析器为 DefaultErrorAttributes
-
DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null
-
默认没有任何人能处理异常,所以异常会被抛出
- 如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
- 解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
-
默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
优先级 * <li>{@code '/<templates>/error/404.<ext>'}</li> * <li>{@code '/<static>/error/404.html'}</li> * <li>{@code '/<templates>/error/4xx.<ext>'}</li> * <li>{@code '/<static>/error/4xx.html'}</li>
DefaultErrorViewResolver 源码,会拼接出 error/404.html 访问路径 private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
-
模板引擎最终响应这个页面 error/500.html
统一异常处理
Spring3.2
版本增加了一个注解@ControllerAdvice
,可以与@ExceptionHandler
、@InitBinder
、@ModelAttribute
等注解注解配套使用
跟异常处理相关
的只有注解@ExceptionHandler
,从字面上看,就是 异常处理器
的意思,其实际作用也是:
若在某个 Controller类 定义一个 异常处理方法
并 在方法上添加该注解
那么当 出现指定的异常 时
执行该处理异常的方法
其可以使用springmvc提供的数据绑定
,比如注入HttpServletRequest
等,还可以接受一个当前抛出的Throwable对象
。
但是,这样就必须在每一个Controller类都定义一套这样的异常处理方法
,因为异常可以是各种各样。这样就会造成大量的冗余代码
,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类
了,很不优雅。
当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。
这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性
。简简单单的Controller,我为啥非得继承这样一个类呢,万一已经继承其他基类了
呢。大家都知道Java只能继承一个类
只使用 @ExceptionHandler 注解处理局部异常
使用这个注解就容易了,但是只能处理使用 @ExceptionHandler 注解的方法的 Controller 的异常
,对于其他 Controller 的异常就无能为力
了,只能再使用同样的方法将使用 @ExceptionHandler 注解的方法写入要捕获异常的 Controller 中,所以不推荐使用
。
使用方式:在ExceptionController
中 加入使用 @ExceptionHandler 注解的方法
代码,整个 ExceptionController
代码如下:
@Controller
public class ExceptionController {
/**
* 描述:捕获 ArithmeticException 异常
* @param model 将Model对象注入到方法中
* @param e 将产生异常对象注入到方法中
* @return 指定错误页面
*/
@ExceptionHandler(value = {ArithmeticException.class})
public String arithmeticExceptionHandle(Model model, Exception e) {
model.addAttribute("msg", "@ExceptionHandler" + e.getMessage());
log.info(e.getMessage());
return "error";
}
/**
* 服务器异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public R<String> exception(Exception e) {
log.error("服务器异常! msg: -> ", e);
return R.failed("服务器异常!");
}
}
代码说明:
注解 @ExceptionHandler
中 value 的值为数组
,表示指定捕获的异常类型
,这里表示捕获 ArithmeticException
异常,跳转的页面为统一的 error.html 页面
,model.addAttribute("msg", "@ExceptionHandler" + e.getMessage())
用来区分是 SpringBoot 处理的异常还是我们自己的方法处理的异常
,也是使用这个方式来区分。
@ExceptionHandler(value = MyException.class) -- 注解类型
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) -- 错误码
@ResponseBody -- 返回json
使用 @ControllerAdvice + @ExceptionHandler 注解处理全局异常
那有没有一种方案,既不需要跟Controller耦合
,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice
出现了,简单的说,该注解可以把异常处理器应用到所有控制器
,而不是单个控制器
。
借助该注解,我们可以实现:在独立的某个地方
,比如单独一个类
,定义一套对各种异常的处理机制
,然后在类的签名加上注解@ControllerAdvice
,统一对 不同阶段的、不同异常 进行处理
。这就是统一异常处理的原理。
使用 @ControllerAdvice
+@ExceptionHandler
注解能够处理全局异常
,这种方式推荐使用,可以根据不同的异常对不同的异常进行处理
。底层是 ExceptionHandlerExceptionResolver 支持
的
如果需要处理其他异常,例如 NullPointerException
异常,则只需要在 GlobalException 类中定义一个方法使用 @ExceptionHandler(value = {NullPointerException.class}) 注解该方法
,在该方法内部处理异常就可以了。
使用方式:定义一个类,使用 @ControllerAdvice 注解该类
,使用 @ExceptionHandler 注解方法
,这里定义了一个 GlobalException 类表示来处理全局异常,
代码如下:
@ControllerAdvice
//或者 @RestControllerAdvice 内部有 @ControllerAdvice 注解
@Order(Ordered.HIGHEST_PRECEDENCE) //代表这个过滤器在众多过滤器中级别最高,也就是过滤的时候最先执行
public class GlobalException {
/**
* 描述:捕获 ArithmeticException 异常
* @param model 将Model对象注入到方法中
* @param e 将产生异常对象注入到方法中
* @return 指定错误页面
*/
@ExceptionHandler(value = {ArithmeticException.class})
public String arithmeticExceptionHandle(Model model, Exception e) {
model.addAttribute("msg", "@ControllerAdvice + @ExceptionHandler :" + e.getMessage());
log.info(e.getMessage());
return "error";
}
/**
* 服务器异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<String> exception(Exception e) {
log.error("服务器异常! msg: -> ", e);
return R.failed("服务器异常!");
}
}
实现 HandlerExceptionResolver 接口处理异常
实现 HandlerExceptionResolver
重写 resolveException 方法
处理异常;可以作为默认的全局异常处理规则
注意:在类上加上 @Configuration 注解
@Configuration
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高
public class HandlerExceptionResolverImpl implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", "实现 HandlerExceptionResolver 接口处理异常");
//判断不同异常类型,做不同视图跳转
if(ex instanceof ArithmeticException){
modelAndView.setViewName("error");
}
return modelAndView;
}
}
通过ErrorViewResolver
实现自定义处理异常,原理:
basicErrorController
要去的页面地址是ErrorViewResolver
自己调用
response.error,请求也会被转发给basicErrorController
进行处理。- 如果
自己没有调用
,并且异常没有任何人能够处理
,tomact底层会自动调用response.sendError将请求转发给basicErrorController处理
,里面使用了ErrorViewResolver进行解析
。ErrorViewResolver
会转到一个页面或者返回json数据
@ResponseStatus+自定义异常
底层是ResponseStatusExceptionResolver
,把responsestatus注解的信息底层调用
response.sendError(statusCode, resolvedReason)
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException(){
}
public UserTooManyException(String message){
super(message);
}
}
使用时直接throw new UserTooManyException()
异常捕获规范
捕获异常
throw 方式
通过 e instanceof BusinessException 来进行区别抛出提示
try 里面的通过判断,进行自定义 throw
,具体的就没写,如果想要再添加异常,则必须在 try 里面 throw,catch 才能够捕获
。
// try 里面抛了两个自定义异常
// catch 里面进行捕获,根据类型返回不同的提示消息
// 这里的 BusinessException 异常是添加失败
// 这里的 Exception 异常是数据库异常或者表不存在又或者表被改名了......
try {
throw new BusinessException("添加失败!");
// throw new Exception("数据库异常!");
} catch (Exception e) {
if (e instanceof BusinessException) {
throw new Exception(e.getMessage());
} else if (e instanceof Exception) {
throw new BusinessException(msg);
}
}
// 我的 catch 里面 throw 不一样是因为 BusinessException 里面有自定义拼接的通用字符串。
// 这里面可以一样:catch 里面的 throw
注释的那个异常是写不写都一样(前提是出现这个异常),都能捕获得到,就是都能够进 catch
try 里面的通过判断,进行自定义 throw,上面我就没写。
通过 try {} catch () {} catch () {} 来进行区别抛出提示(建议此种方法)
catch里面写异常
,大括号里面进行处理,切记,最里面的catch异常是最小的,先执行第一个catch,如果没有才会执行第二个catch,从第一个到后面的等级越来越大。
try {
throw new BusinessException("添加失败!");
// throw new Exception("数据库异常!");
} catch (BusinessException e) {
String msg = "添加失败";
throw new BusinessException(msg); // throw new Exception(msg)
} catch (Exception e) {
String msg = "数据库异常!";
throw new Exception(msg); // throw new BusinessException(msg)
}
// throw 自定义异常根据公司业务需求自定义选择,这里仅仅列举了两个
需要注意的是,无论哪一种方式,当程序处理运行异常时候,只能进入特定的一个,如果你想要进入自定义异常,在 try 里面必须抛出来,才会捕获到自定义的异常!!!!
其实,换句话说,这篇文章就是讲这个多情况异常的,肯定是需要自定义的,所以 try 里面肯定会抛异常的,只不过根据自己的业务去定具体需要抛几个的问题而已。
获取异常类型:e.getClass()
获取异常信息:e.getMessage()
在 service 里面实现的方法里面进行业务处理
,进行 try catch,要注意的是,一般来说都会用一个 try 把代码全部包起来
,如果有多个业务处理,需要进行区分
,就多写 catch
,叫做 try 不够,用 catch 来补
。
提示消息:是把 msg 以参数的形式
,抛给上一层
,即放在自定义的异常里面,action 获取到的就是 msg
,这一步做到了把代码的异常错误消息,转为自定义提示消息
。
示例
报错
分析
public X todo(xxxx) throws BusinessException {
try {
return xxxxx;
} catch (ApiException e) {
String message = e.getMessage();
// 这里 拿到报错的 message ,如果是 conflict 就上抛异常,异常类型为 BusinessException
if ("Conflict".equalsIgnoreCase(message)) {
throw 异常.buildException();
}
log.error("xxxx", e);
throw 异常.buildException();
}
}
public String xxxx(xxxx param) {
try {
//方法调用
todo(....);
if (初步判断) {
throw 异常.buildException();
}
} catch (DuplicateKeyException e) {
throw 异常.buildException();
} catch (FeignException.BadRequest e) {
log.error("xxxx", e);
throw 异常.buildException();
} catch (Exception e) {
try {
//方法调用
} catch (Exception e1) {
log.error("xxxx", e1);
}
// 捕获到抛出的 BusinessException ,抛出 e ,这里 e 就是 todo 中 throw 的 e
if (e instanceof BusinessException) {
throw e;
}
throw 异常.buildException();
}
return xxxxx;
}
其他
通常情况下,我们都会定义一个全局异常处理类来处理异常,但是当我们定义了多个异常处理类,我们如何去保证它的执行顺序
呢?
可以通过@Order
注解来设置处理器的捕获执行顺序
,如果没有该注解,他就会使用默认值
,也就是Ordered.LOWEST_PRECEDENCE
。
越小的优先执行
,所以我们只需要在全局参数异常处理器加上
@Order(Ordered.LOWEST_PRECEDENCE - 2)
就能保证它的执行顺序优先于全局异常处理器
。