Spring Boot 優(yōu)雅捕捉異常的幾種姿勢!
01、背景介紹
在上文中,我們介紹了在 Spring Boot 中實現(xiàn)接口數(shù)據(jù)格式的統(tǒng)一返回處理實現(xiàn),其中就包括程序運行時的異常處理,通過全局異常處理器,可以簡化代碼邏輯,統(tǒng)一響應(yīng)格式。
其實在 Spring Boot 中,針對controller層的異常處理有很多種辦法。今天通過這篇文章,我們就一起來總結(jié)一下相關(guān)異常處理的實現(xiàn)方式。
02、方案實踐
在 Spring Boot 中針對controller層的異常處理,有兩種常用實現(xiàn)方式,都可以達到簡化代碼邏輯的效果。
- 方式一:通過@ControllerAdvice和@ExceptionHandler注解實現(xiàn)全局異常的處理
- 方式二:通過實現(xiàn)HandlerExceptionResolver接口來完成全局異常的處理
下面我們一起來看看具體實現(xiàn)。
2.1、全局異常處理方式一
通過@ControllerAdvice和@ExceptionHandler注解實現(xiàn)全局異常攔截,在之前的文章中我們有多次介紹過,它可以攔截controller層請求方法拋出的異常信息,同時外加@ ResponseBody注解,可以實現(xiàn)響應(yīng)類型為json格式。
例如,現(xiàn)在有兩種異常類型NullPointerException和Exception,分別對其進行捕捉,具體實現(xiàn)如下!
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 優(yōu)先處理空指針異常
* @param e
* @return
*/
@ExceptionHandler(value = {NullPointerException.class})
@ResponseBody
public Object nullPointerExceptionHandler(HttpServletRequest request, NullPointerException e){
LOGGER.error("發(fā)生空指針異常,請求地址:{}, 錯誤信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(500, e.getMessage());
}
/**
* 兜底處理其它異常
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知異常,請求地址:{}, 錯誤信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(999, e.getMessage());
}
}
測試代碼,如下:
@RestController
public class HelloController {
@GetMapping(value = "/add")
public String hello(){
if(1 ==1){
throw new NullPointerException("空指針測試");
}
return "hello world";
}
@GetMapping(value = "/delete")
public String delete(){
if(1 ==1){
throw new RuntimeException("其它測試");
}
return "hello world";
}
}
啟動服務(wù)后,在瀏覽器中請求http://localhost:8080/add,結(jié)果如下:
圖片
請求http://localhost:8080/delete,結(jié)果如下:
圖片
結(jié)果與預(yù)期一致。
2.1.1、自定義異常類實現(xiàn)
很多場景下,我們希望通過自定義異常類來返回相關(guān)錯誤信息,如何實現(xiàn)呢?
首先自定義一個異常類CustomerException。
public class CustomerException extends RuntimeException {
private Integer code;
public Integer getCode() {
return code;
}
public CustomerException(String message) {
super(message);
this.code = 500;
}
public CustomerException(Integer code, String message) {
super(message);
this.code = code;
}
}
然后,在全局異常處理器中增加相關(guān)的捕捉方法。
/**
* 處理自定義的異常
* @param e
* @return
*/
@ExceptionHandler(value = {CustomerException.class})
@ResponseBody
public Object customerExceptionHandler(HttpServletRequest request, CustomerException e){
LOGGER.error("發(fā)生業(yè)務(wù)異常,請求地址:{}, 錯誤信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(e.getCode(), e.getMessage());
}
測試代碼,如下:
@GetMapping(value = "/update")
public String update(){
if(1 ==1){
throw new CustomerException(4003, "請求ID不能為空");
}
return "hello world";
}
啟動服務(wù)后,在瀏覽器中請求http://localhost:8080/update,結(jié)果如下:
圖片
結(jié)果與預(yù)期一致!
2.1.2、404 異常特殊處理
默認(rèn)情況下,@ExceptionHandler注解無法捕捉到 404 異常,比如請求一個無效的地址,返回信息如下:
圖片
如果想要捕捉到這種異常,可以在application.properties文件中添加如下配置來實現(xiàn)。
# 如果沒有找到請求地址,拋異常
spring.mvc.throw-exception-if-no-handler-found=true
# 關(guān)閉默認(rèn)的靜態(tài)資源路徑映射
spring.resources.add-mappings=false
啟動服務(wù)后,再次發(fā)起地址請求,結(jié)果如下:
圖片
對于前后端分離開發(fā)的情況,這種方式非常實用;但是如果前后端不分離的項目,比如訪問項目/static目錄下的靜態(tài)資源,可能前端無法正常訪問。
此時,我們可以手動添加資源映射,比如如下操作,前端就能正常訪問靜態(tài)資源了。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 允許訪問localhost:8080/static/目錄下的靜態(tài)資源
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
2.1.3、自定義異常頁面實現(xiàn)
某些場景下,當(dāng)發(fā)生異常時希望跳轉(zhuǎn)到自定義的異常頁面,如何實現(xiàn)呢?
首先,這里基于thymeleaf模板引擎來開發(fā)頁面,在templates目錄下創(chuàng)建一個異常頁面error.html。
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>錯誤頁面</title>
</head>
<body>
出錯啦,請與管理員聯(lián)系<br>
<span th:text="${message}"></span>
</body>
</html>
然后,修改異常捕捉方法,這里無需添加@ResponseBody注解,示例如下。
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 跳轉(zhuǎn)到異常頁面
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知異常,請求地址:{}, 錯誤信息:{}", request.getRequestURI(), e.getMessage());
ModelAndView mv = new ModelAndView();
// 添加錯誤信息對象
mv.addObject("message", e.getMessage());
// 要跳轉(zhuǎn)的頁面視圖
mv.setViewName("error");
return mv;
}
}
啟動服務(wù)后,在瀏覽器中再次請求http://localhost:8080/update,結(jié)果如下:
圖片
結(jié)果與預(yù)期一致!
2.1.4、RestControllerAdvice和ControllerAdvice的區(qū)別
很多同學(xué)喜歡用@RestControllerAdvice來代替@ControllerAdvice和@ResponseBody。
例如如下示例!
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 處理所有異常,以json方式響應(yīng)
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
public Object exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("未知異常,請求地址:{}, 錯誤信息:{}", request.getRequestURI(), e.getMessage());
return ResultMsg.fail(999, e.getMessage());
}
}
實現(xiàn)效果,與上文等價。
打開@RestControllerAdvice的源碼,你會發(fā)現(xiàn)它將@ControllerAdvice和@ResponseBody注解組合在一起了,因此同時具備兩者效果,部分源碼如下:
package org.springframework.web.bind.annotation;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
2.2、全局異常處理方式二
在 Spring Boot 中,除了通過@ControllerAdvice和@ExceptionHandler注解實現(xiàn)全局異常處理外,還有一種通過實現(xiàn)HandlerExceptionResolver接口來完成全局異常的處理。
具體實現(xiàn)示例如下:
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionResolver.class);
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
LOGGER.error("接口請求出現(xiàn)異常,請求地址:{},錯誤信息:{}", request.getRequestURI(), e.getMessage());
if(e instanceof RuntimeException){
// 設(shè)置響應(yīng)類型為json格式
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("code", 500);
mv.addObject("msg", e.getMessage());
return mv;
} else {
// 設(shè)置響應(yīng)類型為錯誤頁面
ModelAndView mv = new ModelAndView();
mv.addObject("message", e.getMessage());
mv.setViewName("error");
return mv;
}
}
}
當(dāng)出現(xiàn)異常的時候,結(jié)果會以json格式響應(yīng)給客戶端。
啟動服務(wù)后,發(fā)起地址請求,結(jié)果如下:
這種思路的實現(xiàn)原理,主要是通過 SpringMVC 的異常處理鏈路器來完成異常的全局處理。
SpringMVC 支持用戶自定義異常處理類(需要實現(xiàn)HandlerExceptionResolver),當(dāng)發(fā)生異常時,默認(rèn)異常處理類無法處理時,就會交給自定義異常處理類來完成。實現(xiàn)方面比較靈活,即可以實現(xiàn)以json格式響應(yīng),也可以以頁面視圖的方式響應(yīng)。
雖然這種方式能夠處理全局異常,但是 Spring 官方不推薦使用它;同時實測過程中發(fā)現(xiàn)它無法攔截 404 錯誤,當(dāng)請求錯誤地址時,會優(yōu)先被DefaultHandlerExceptionResolver默認(rèn)異常處理類攔截,自定義的異常處理類無法捕捉。
03、小結(jié)
最后總結(jié)一下,雖然方式一和方式二都可以實現(xiàn)controller層接口請求異常的全局處理,但是在實際使用中,推薦方式一,簡單好維護。
示例代碼地址:
https://gitee.com/pzblogs/spring-boot-example-demo