Spring Cloud:統一異常處理
阿新 • • 發佈:2018-12-18
在啟動應用時會發現在控制檯列印的日誌中出現了兩個路徑為 {[/error]} 的訪問地址,當系統中傳送異常錯誤時,Spring Boot 會根據請求方式分別跳轉到以 JSON 格式或以介面顯示的 /error 地址中顯示錯誤資訊。
2018-12-18 09:36:24.627 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" ... 2018-12-18 09:36:24.632 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" ...
預設異常處理
使用 AJAX 方式請求時返回的 JSON 格式錯誤資訊。
{ "timestamp": "2018-12-18T01:50:51.196+0000", "status": 404, "error": "Not Found", "message": "No handler found for GET /err404", "path": "/err404" }
使用瀏覽器請求時返回的錯誤資訊介面。
自定義異常處理
引入依賴
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.54</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
fastjson
增加配置
properties
# 出現錯誤時, 直接丟擲異常(便於異常統一處理,否則捕獲不到404)
spring.mvc.throw-exception-if-no-handler-found=true
# 不要為工程中的資原始檔建立對映
spring.resources.add-mappings=false
yml
spring:
# 出現錯誤時, 直接丟擲異常(便於異常統一處理,否則捕獲不到404)
mvc:
throw-exception-if-no-handler-found: true
# 不要為工程中的資原始檔建立對映
resources:
add-mappings: false
新建錯誤資訊實體
/** * 資訊實體 */ public class ExceptionEntity implements Serializable { private static final long serialVersionUID = 1L; private String message; private int code; private String error; private String path; @JSONField(format = "yyyy-MM-dd hh:mm:ss") private Date timestamp = new Date(); public static long getSerialVersionUID() { return serialVersionUID; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getError() { return error; } public void setError(String error) { this.error = error; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public Date getTimestamp() { return timestamp; } public void setTimestamp(Date timestamp) { this.timestamp = timestamp; } }
新建自定義異常
/** * 自定義異常 */ public class BasicException extends RuntimeException { private static final long serialVersionUID = 1L; private int code = 0; public BasicException(int code, String message) { super(message); this.code = code; } public int getCode() { return this.code; } }
/** * 業務異常 */ public class BusinessException extends BasicException { private static final long serialVersionUID = 1L; public BusinessException(int code, String message) { super(code, message); } }
BasicException 繼承了 RuntimeException ,並在原有的 Message 基礎上增加了錯誤碼 code 的內容。而 BusinessException 則是在業務中具體使用的自定義異常類,起到了對不同的異常資訊進行分類的作用。
新建 error.ftl 模板檔案
位置:/src/main/resources/templates/ 用於顯示錯誤資訊
<!DOCTYPE html> <html> <head> <meta name="robots" content="noindex,nofollow" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <style> h2{ color: #4288ce; font-weight: 400; padding: 6px 0; margin: 6px 0 0; font-size: 18px; border-bottom: 1px solid #eee; } /* Exception Variables */ .exception-var table{ width: 100%; max-width: 500px; margin: 12px 0; box-sizing: border-box; table-layout:fixed; word-wrap:break-word; } .exception-var table caption{ text-align: left; font-size: 16px; font-weight: bold; padding: 6px 0; } .exception-var table caption small{ font-weight: 300; display: inline-block; margin-left: 10px; color: #ccc; } .exception-var table tbody{ font-size: 13px; font-family: Consolas,"Liberation Mono",Courier,"微軟雅黑"; } .exception-var table td{ padding: 0 6px; vertical-align: top; word-break: break-all; } .exception-var table td:first-child{ width: 28%; font-weight: bold; white-space: nowrap; } .exception-var table td pre{ margin: 0; } </style> </head> <body> <div class="exception-var"> <h2>Exception Datas</h2> <table> <tbody> <tr> <td>Code</td> <td> ${(exception.code)!} </td> </tr> <tr> <td>Time</td> <td> ${(exception.timestamp?datetime)!} </td> </tr> <tr> <td>Path</td> <td> ${(exception.path)!} </td> </tr> <tr> <td>Exception</td> <td> ${(exception.error)!} </td> </tr> <tr> <td>Message</td> <td> ${(exception.message)!} </td> </tr> </tbody> </table> </div> </body> </html>
編寫全域性異常控制類
/** * 全域性異常控制類 */ @ControllerAdvice public class GlobalExceptionHandler { /** * 404異常處理 */ @ExceptionHandler(value = NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.NOT_FOUND.value(), exception.getMessage()); } /** * 405異常處理 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.METHOD_NOT_ALLOWED.value(), exception.getMessage()); } /** * 415異常處理 */ @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), exception.getMessage()); } /** * 500異常處理 */ @ExceptionHandler(value = Exception.class) public ModelAndView errorHandler (HttpServletRequest request, Exception exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage()); } /** * 業務異常處理 */ @ExceptionHandler(value = BasicException.class) private ModelAndView errorHandler (HttpServletRequest request, BasicException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), exception.getCode(), exception.getMessage()); } /** * 表單驗證異常處理 */ @ExceptionHandler(value = BindException.class) @ResponseBody public ExceptionEntity validExceptionHandler(BindException exception, HttpServletRequest request, HttpServletResponse response) { List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors(); Map<String,String> errors = new HashMap<>(); for (FieldError error:fieldErrors) { errors.put(error.getField(), error.getDefaultMessage()); } ExceptionEntity entity = new ExceptionEntity(); entity.setMessage(JSON.toJSONString(errors)); entity.setPath(request.getRequestURI()); entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); entity.setError(exception.getClass().getSimpleName()); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); return entity; } /** * 異常處理資料處理 */ private ModelAndView commonHandler (HttpServletRequest request, HttpServletResponse response, String error, int httpCode, String message) { ExceptionEntity entity = new ExceptionEntity(); entity.setPath(request.getRequestURI()); entity.setError(error); entity.setCode(httpCode); entity.setMessage(message); return determineOutput(request, response, entity); } /** * 異常輸出處理 */ private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) { if (!( request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")) )) { ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("exception", entity); return modelAndView; } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setCharacterEncoding("UTF8"); response.setHeader("Content-Type", "application/json"); try { response.getWriter().write(ResultJsonTools.build( ResponseCodeConstant.SYSTEM_ERROR, ResponseMessageConstant.APP_EXCEPTION, JSONObject.parseObject(JSON.toJSONString(entity)) )); } catch (IOException e) { e.printStackTrace(); } return null; } } }
@ControllerAdvice
作用於類上,用於標識該類用於處理全域性異常。
@ExceptionHandler
作用於方法上,用於對攔截的異常型別進行處理。value 屬性用於指定具體的攔截異常型別,如果有多個 ExceptionHandler 存在,則需要指定不同的 value 型別,由於異常類擁有繼承關係,所以 ExceptionHandler 會首先執行在繼承樹中靠前的異常型別。
BindException
該異常來自於表單驗證框架 Hibernate calidation,當欄位驗證未通過時會丟擲此異常。
編寫測試 Controller
@RestController public class TestController { @RequestMapping(value = "err") public void error(){ throw new BusinessException(400, "業務異常錯誤資訊"); } @RequestMapping(value = "err2") public void error2(){ throw new NullPointerException("手動丟擲異常資訊"); } @RequestMapping(value = "err3") public int error3(){ int a = 10 / 0; return a; } }
使用 AJAX 方式請求時返回的 JSON 格式錯誤資訊。
# /err { "msg": "應用程式異常", "code": -1, "status_code": 0, "data": { "path": "/err", "code": 400, "error": "BusinessException", "message": "業務異常錯誤資訊", "timestamp": "2018-12-18 11:09:00" } } # /err2 { "msg": "應用程式異常", "code": -1, "status_code": 0, "data": { "path": "/err2", "code": 500, "error": "NullPointerException", "message": "手動丟擲異常資訊", "timestamp": "2018-12-18 11:15:15" } } # /err3 { "msg": "應用程式異常", "code": -1, "status_code": 0, "data": { "path": "/err3", "code": 500, "error": "ArithmeticException", "message": "/ by zero", "timestamp": "2018-12-18 11:15:46" } } # /err404 { "msg": "應用程式異常", "code": -1, "status_code": 0, "data": { "path": "/err404", "code": 404, "error": "NoHandlerFoundException", "message": "No handler found for GET /err404", "timestamp": "2018-12-18 11:16:11" } }
使用瀏覽器請求時返回的錯誤資訊介面。
示例程式碼:https://github.com/BNDong/spring-cloud-examples/tree/master/spring-cloud-zuul/cloud-zuul
參考資料
《微服務 分散式架構開發實戰》 龔鵬 著
https://www.jianshu.com/p/1a49fa436623