SpringBoot中全局異常處理的5種實現(xiàn)方式小結
前言
在實際開發(fā)中,異常處理是一個非常重要的環(huán)節(jié)。合理的異常處理機制不僅能提高系統(tǒng)的健壯性,還能大大提升用戶體驗。本文將詳細介紹SpringBoot中全局異常處理的幾種實現(xiàn)方式。
為什么需要全局異常處理?
如果沒有統(tǒng)一的異常處理機制,當系統(tǒng)發(fā)生異常時,可能會導致以下問題
- 用戶體驗差:用戶可能看到一些技術性的錯誤信息,如堆棧跟蹤
- 安全隱患:暴露系統(tǒng)內部錯誤詳情可能會被攻擊者利用
- 維護困難:分散在各處的異常處理代碼增加了維護難度
- 響應格式不一致:不同接口返回的錯誤格式不統(tǒng)一,增加前端處理難度
下面,來看幾種在SpringBoot中實現(xiàn)全局異常處理的方式。
方式一:@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler
這是SpringBoot中最常用的全局異常處理方式。
首先,定義一個統(tǒng)一的返回結果類:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } // getter和setter方法省略 }
然后,定義自定義異常:
public class BusinessException extends RuntimeException { private Integer code; public BusinessException(String message) { super(message); this.code = 500; } public BusinessException(Integer code, String message) { super(message); this.code = code; } public Integer getCode() { return code; } }
創(chuàng)建全局異常處理類:
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; @RestControllerAdvice public class GlobalExceptionHandler { /** * 處理自定義業(yè)務異常 */ @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } /** * 處理參數校驗異常 */ @ExceptionHandler(ConstraintViolationException.class) public Result<Void> handleValidationException(ConstraintViolationException e) { return Result.error(400, "參數校驗失敗:" + e.getMessage()); } /** * 處理資源找不到異常 */ @ExceptionHandler(NoHandlerFoundException.class) public Result<Void> handleNotFoundException(NoHandlerFoundException e) { return Result.error(404, "請求的資源不存在"); } /** * 處理其他所有未捕獲的異常 */ @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { return Result.error(500, "服務器內部錯誤:" + e.getMessage()); } }
優(yōu)點
- 代碼簡潔清晰,易于維護
- 可以針對不同的異常類型定義不同的處理方法
- 與Spring MVC結合緊密,可以獲取請求和響應上下文
方式二:實現(xiàn)HandlerExceptionResolver接口
HandlerExceptionResolver是Spring MVC中用于解析異常的接口,我們可以通過實現(xiàn)此接口來自定義異常處理邏輯。
import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import com.fasterxml.jackson.databind.ObjectMapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class CustomExceptionResolver implements HandlerExceptionResolver { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Result<?> result; // 處理不同類型的異常 if (ex instanceof BusinessException) { BusinessException businessException = (BusinessException) ex; result = Result.error(businessException.getCode(), businessException.getMessage()); } else { result = Result.error(500, "服務器內部錯誤:" + ex.getMessage()); } // 設置響應類型和狀態(tài)碼 response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_OK); try { // 將錯誤信息寫入響應 PrintWriter writer = response.getWriter(); writer.write(objectMapper.writeValueAsString(result)); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } // 返回空的ModelAndView表示異常已經處理完成 return new ModelAndView(); } }
優(yōu)點
- 可以完全控制異常處理的過程
- 可以直接操作HttpServletRequest和HttpServletResponse
- 適合需要特殊處理的場景
缺點
- 代碼相對復雜
- 無法利用Spring MVC的注解優(yōu)勢
方式三:使用SimpleMappingExceptionResolver
SimpleMappingExceptionResolver是HandlerExceptionResolver的一個簡單實現(xiàn),適用于返回錯誤視圖的場景。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.Properties; @Configuration public class ExceptionConfig { @Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver(); // 設置默認錯誤頁面 resolver.setDefaultErrorView("error/default"); // 設置異常映射 Properties mappings = new Properties(); mappings.setProperty(BusinessException.class.getName(), "error/business"); mappings.setProperty(RuntimeException.class.getName(), "error/runtime"); resolver.setExceptionMappings(mappings); // 設置異常屬性名,默認為"exception" resolver.setExceptionAttribute("ex"); return resolver; } }
對應的錯誤頁面模板(使用Thymeleaf):
<!-- templates/error/business.html --> <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>業(yè)務錯誤</title> </head> <body> <h1>業(yè)務錯誤</h1> <p th:text="${ex.message}">錯誤信息</p> </body> </html>
優(yōu)點
- 配置簡單
- 適合返回錯誤頁面的場景
缺點
- 主要適用于返回視圖,不適合RESTful API
- 靈活性有限
方式四:自定義ErrorController
Spring Boot提供了BasicErrorController來處理應用中的錯誤,我們可以通過繼承或替換它來自定義錯誤處理邏輯。
import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.Map; @Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class CustomErrorController implements ErrorController { @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public ResponseEntity<Result<Void>> handleError(HttpServletRequest request) { HttpStatus status = getStatus(request); Integer statusCode = status.value(); String message = "未知錯誤"; switch (statusCode) { case 404: message = "請求的資源不存在"; break; case 403: message = "沒有權限訪問該資源"; break; case 500: message = "服務器內部錯誤"; break; default: break; } return new ResponseEntity<>(Result.error(statusCode, message), status); } @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public String handleErrorHtml(HttpServletRequest request, Map<String, Object> model) { HttpStatus status = getStatus(request); // 添加錯誤信息到模型 model.put("status", status.value()); model.put("message", status.getReasonPhrase()); // 返回錯誤頁面 return "error/error"; } private HttpStatus getStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } try { return HttpStatus.valueOf(statusCode); } catch (Exception ex) { return HttpStatus.INTERNAL_SERVER_ERROR; } } }
優(yōu)點
- 可以同時處理HTML和JSON響應
- 對所有未處理的錯誤提供統(tǒng)一的處理方式
- 可以獲取錯誤的狀態(tài)碼和詳細信息
缺點
- 只能處理已經發(fā)生的HTTP錯誤,無法攔截自定義異常
- 一般作為兜底方案使用
方式五:使用錯誤頁面模板
Spring Boot支持通過靜態(tài)HTML頁面或模板來展示特定狀態(tài)碼的錯誤。只需要在templates/error/目錄下創(chuàng)建對應狀態(tài)碼的頁面即可。
目錄結構:
src/
main/
resources/
templates/
error/
404.html
500.html
error.html # 默認錯誤頁面
例如,404.html的內容:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>頁面不存在</title> </head> <body> <h1>404 - 頁面不存在</h1> <p>您請求的頁面不存在,請檢查URL是否正確。</p> <a href="/" rel="external nofollow" >返回首頁</a> </body> </html>
優(yōu)點
- 配置極其簡單,只需創(chuàng)建對應的頁面即可
- 適合簡單的Web應用
缺點
- 靈活性有限
- 不適合RESTful API
- 無法處理自定義異常
實戰(zhàn)示例:完整的異常處理體系
下面提供一個完整的異常處理體系示例,組合了多種方式:
首先,創(chuàng)建異常體系:
// 基礎異常類 public abstract class BaseException extends RuntimeException { private final int code; public BaseException(int code, String message) { super(message); this.code = code; } public int getCode() { return code; } } // 業(yè)務異常 public class BusinessException extends BaseException { public BusinessException(String message) { super(400, message); } public BusinessException(int code, String message) { super(code, message); } } // 系統(tǒng)異常 public class SystemException extends BaseException { public SystemException(String message) { super(500, message); } public SystemException(int code, String message) { super(code, message); } } // 權限異常 public class PermissionException extends BaseException { public PermissionException(String message) { super(403, message); } }
創(chuàng)建全局異常處理器:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 處理自定義基礎異常 */ @ExceptionHandler(BaseException.class) public Result<?> handleBaseException(BaseException e) { logger.error("業(yè)務異常:{}", e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } /** * 處理參數校驗異常(@Valid) */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); String errorMsg = fieldErrors.stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); logger.error("參數校驗錯誤:{}", errorMsg); return Result.error(400, "參數校驗錯誤: " + errorMsg); } /** * 處理所有其他異常 */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<?> handleException(Exception e) { logger.error("系統(tǒng)異常:", e); return Result.error(500, "服務器內部錯誤,請聯(lián)系管理員"); } }
使用示例:
@RestController @RequestMapping("/api") public class UserController { @GetMapping("/{id}") public Result<User> getUser(@PathVariable Long id) { if (id <= 0) { throw new BusinessException("用戶ID必須大于0"); } if (id > 100) { throw new SystemException("系統(tǒng)維護中"); } if (id == 10) { throw new PermissionException("沒有權限查看此用戶"); } // 模擬查詢用戶 User user = new User(id, "用戶" + id, "user" + id + "@example.com"); return Result.success(user); } }
各方式對比與使用建議
實現(xiàn)方式 | 適用場景 | 靈活性 | 復雜度 |
---|---|---|---|
@ControllerAdvice + @ExceptionHandler | RESTful API、前后端分離項目 | 高 | 低 |
HandlerExceptionResolver | 需要精細控制異常處理過程的場景 | 高 | 中 |
SimpleMappingExceptionResolver | 傳統(tǒng)Web應用,需要返回錯誤頁面 | 中 | 低 |
自定義ErrorController | 需要自定義錯誤頁面和錯誤響應的場景 | 中 | 中 |
錯誤頁面模板 | 簡單的Web應用,只需自定義錯誤頁面 | 低 | 低 |
建議
對于RESTful API或前后端分離項目:優(yōu)先選擇@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler方式,它提供了良好的靈活性和簡潔的代碼結構。
對于需要返回錯誤頁面的傳統(tǒng)Web應用:可以使用SimpleMappingExceptionResolver或錯誤頁面模板方式。
對于復雜系統(tǒng):可以組合使用多種方式,例如
- 使用@ControllerAdvice處理業(yè)務異常
- 使用自定義ErrorController處理未捕獲的HTTP錯誤
- 使用錯誤頁面模板提供友好的錯誤頁面
最佳實踐總結
分層異常處理:根據業(yè)務需求,設計合理的異常繼承體系,便于分類處理。
統(tǒng)一返回格式:無論成功還是失敗,都使用統(tǒng)一的返回格式,便于前端處理。
合理記錄日志:在異常處理中記錄日志,可以幫助排查問題。不同級別的異常使用不同級別的日志。
區(qū)分開發(fā)和生產環(huán)境:在開發(fā)環(huán)境可以返回詳細的錯誤信息,而在生產環(huán)境則應該隱藏敏感信息。
異常分類
- 業(yè)務異常:用戶操作引起的可預期異常
- 系統(tǒng)異常:系統(tǒng)內部錯誤
- 第三方服務異常:調用外部服務失敗等
不要忽略異常:即使是捕獲異常后不需要處理,也應該至少記錄日志。
結語
在Spring Boot應用中,全局異常處理是提高系統(tǒng)健壯性和用戶體驗的重要環(huán)節(jié)。通過本文介紹的幾種實現(xiàn)方式,開發(fā)者可以根據實際需求選擇合適的實現(xiàn)方案。在實際項目中,往往需要結合多種方式,構建一個完整的異常處理體系。
以上就是SpringBoot中全局異常處理的5種實現(xiàn)方式小結的詳細內容,更多關于SpringBoot全局異常處理的資料請關注腳本之家其它相關文章!
相關文章
Mybatis自定義TypeHandler解決特殊類型轉換問題詳解
這篇文章主要介紹了Mybatis自定義TypeHandler解決特殊類型轉換問題詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11