Spring Cloud:統一異常處理

在啓動應用時會發如今控制檯打印的日誌中出現了兩個路徑爲 {[/error]} 的訪問地址,當系統中發送異常錯誤時,Spring Boot 會根據請求方式分別跳轉到以 JSON 格式或以界面顯示的 /error 地址中顯示錯誤信息。html

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 是 JSON 序列化依賴, spring-boot-starter-freemarker 是一個模板引擎,用於咱們設置錯誤輸出模板。git

增長配置

propertiesgithub

# 出現錯誤時, 直接拋出異常(便於異常統一處理,不然捕獲不到404)
spring.mvc.throw-exception-if-no-handler-found=true
# 不要爲工程中的資源文件創建映射
spring.resources.add-mappings=false

ymlspring

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 則是在業務中具體使用的自定義異常類,起到了對不一樣的異常信息進行分類的做用。json

新建 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 會首先執行在繼承樹中靠前的異常類型。mvc

BindException

該異常來自於表單驗證框架 Hibernate calidation,當字段驗證未經過時會拋出此異常。app

編寫測試 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

相關文章
相關標籤/搜索