互聯網大廠是如何處理全局異常的?

1、爲什麼要處理全局異常?

在日常項目開發過程當中,程序不免會出現運行時異常,或者業務異常。難道要針對每一處可能出現的異常進行編寫代碼進行處理?或者直接不處理異常,將一大屏堆滿英文的異常信息顯示給用戶?那用戶體驗性是何等極差。

因此,當程序拋異常時,爲了日誌的可讀性排查 Bug簡單,以及更好的用戶體驗性,因此咱們要對全局異常進行處理。前端

2、開發環境
  1. JDK 1.8 或者1.8以上
  2. Springboot (此演示版本爲 Springboot 2.1.18.RELEASE)
  3. Gradle (固然也可用Maven,其實目的都是爲構建項目,管理依賴等)
3、添加依賴
plugins {
    id "org.springframework.boot" version "2.1.18.RELEASE"
    id "io.spring.dependency-management" version "1.0.10.RELEASE"
    id "java"
}

group = 'com.nobody'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenLocal()
    maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 添加lombok,主要爲程序中經過註解,不用編寫getter和setter等代碼
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
}
4、自定義異常錯誤類

在咱們項目開發中,確定會有跟業務相關的異常,例如添加用戶的業務,系統要求用戶名不能爲空,可是添加用戶的請求接口,用戶名值爲空,這時咱們程序要報用戶名不能爲空的異常錯誤;或者查詢用戶信息的接口,可能會報用戶不存在的錯誤異常等等。java

4.1 自定義異常基礎接口類

由於要作成通用性,因此咱們定義一個異常基礎接口類,自定義的異常枚舉類需實現該接口。web

package com.nobody.exception;

/**
 * @Description 自定義異常基礎接口類,自定義的異常信息枚舉類需實現該接口。
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
public interface BaseErrorInfo {

    /**
     * 獲取錯誤碼
     * 
     * @return 錯誤碼
     */
    String getErrorCode();

    /**
     * 獲取錯誤信息
     * 
     * @return 錯誤信息
     */
    String getErrorMsg();

}

4.2 通用異常信息枚舉類

通用異常信息枚舉類,這裏定義的全部異常信息是整個程序通用的。spring

package com.nobody.exception;

import lombok.Getter;

/**
 * @Description 自定義通用異常信息枚舉類
 * @Author Mr.nobody
 * @Date 2020/10/23
 * @Version 1.0
 */
@Getter
public enum CommonErrorEnum implements BaseErrorInfo {

    /**
     * 成功
     */
    SUCCESS("200", "成功!"),
    /**
     * 請求的數據格式不符!
     */
    BODY_NOT_MATCH("400", "請求的數據格式不符!"),
    /**
     * 未找到該資源!
     */
    NOT_FOUND("404", "未找到該資源!"),
    /**
     * 服務器內部錯誤!
     */
    INTERNAL_SERVER_ERROR("500", "服務器內部錯誤!"),
    /**
     * 服務器正忙,請稍後再試!
     */
    SERVER_BUSY("503", "服務器正忙,請稍後再試!");

    private String errorCode;
    private String errorMsg;

    CommonErrorEnum(String errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
}

4.3 業務異常信息枚舉類

若是程序中異常信息太多,能夠針對每一個模塊功能定義業務異常枚舉類,方便維護,例如和用戶相關的異常信息枚舉類以下。數組

package com.nobody.exception;

import lombok.Getter;

/**
 * @Description 自定義用戶相關異常信息枚舉類
 * @Author Mr.nobody
 * @Date 2020/10/23
 * @Version 1.0
 */
@Getter
public enum UserErrorEnum implements BaseErrorInfo {

    /**
     * 用戶不存在
     */
    USER_NOT_FOUND("1001", "用戶不存在!");

    private String errorCode;
    private String errorMsg;

    UserErrorEnum(String errorCode, String errorMsg) {
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
}

4.4 自定義業務異常類

業務異常類,主要用於業務錯誤,或者異常時手動拋出的異常。服務器

package com.nobody.exception;

import lombok.Getter;
import lombok.Setter;
import org.slf4j.MDC;

/**
 * @Description 自定義業務異常類
 * @Author Mr.nobody
 * @Date 2020/10/23
 * @Version 1.0
 */
@Getter
@Setter
public class BizException extends RuntimeException {

    private static final long serialVersionUID = 5564446583860234738L;

    // 錯誤碼
    private String errorCode;
    // 錯誤信息
    private String errorMsg;
    // 日誌追蹤ID
    private String traceId = MDC.get("traceId");

    public BizException(BaseErrorInfo errorInfo) {
        super(errorInfo.getErrorMsg());
        this.errorCode = errorInfo.getErrorCode();
        this.errorMsg = errorInfo.getErrorMsg();
    }

    public BizException(BaseErrorInfo errorInfo, String errorMsg) {
        super(errorMsg);
        this.errorCode = errorInfo.getErrorCode();
        this.errorMsg = errorMsg;
    }

    public BizException(BaseErrorInfo errorInfo, Throwable cause) {
        super(errorInfo.getErrorMsg(), cause);
        this.errorCode = errorInfo.getErrorCode();
        this.errorMsg = errorInfo.getErrorMsg();
    }

    public BizException(String errorCode, String errorMsg) {
        super(errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public BizException(String errorCode, String errorMsg, Throwable cause) {
        super(errorMsg, cause);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }
}
5、接口返回統一格式

爲方便前端對接口返回的數據進行處理,也是規範問題,因此咱們要定義接口返回統一格式。app

package com.nobody.pojo.vo;

import lombok.Getter;
import lombok.Setter;

/**
 * @Description 接口返回統一格式
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
@Getter
@Setter
public class GeneralResult<T> {

    private boolean success;
    private String errorCode;
    private String message;
    private T data;
    private String traceId;

    private GeneralResult(boolean success, T data, String message, String errorCode) {
        this.success = success;
        this.data = data;
        this.message = message;
        this.errorCode = errorCode;
    }

    public static <T> GeneralResult<T> genResult(boolean success, T data, String message) {
        return genResult(success, data, message, null);
    }

    public static <T> GeneralResult<T> genSucce***esult(T data) {
        return genResult(true, data, null, null);
    }

    public static <T> GeneralResult<T> genErrorResult(String message) {
        return genResult(false, null, message, null);
    }

    public static <T> GeneralResult<T> genSucce***esult() {
        return genResult(true, null, null, null);
    }

    public static <T> GeneralResult<T> genErrorResult(String message, String errorCode) {
        return genResult(false, null, message, errorCode);
    }

    public static <T> GeneralResult<T> genResult(boolean success, T data, String message,
            String errorCode) {
        return new GeneralResult<>(success, data, message, errorCode);
    }

    public static <T> GeneralResult<T> genErrorResult(String message, String errorCode,
            String traceId) {
        GeneralResult<T> result = genResult(false, null, message, errorCode);
        result.setTraceId(traceId);
        return result;
    }

}
6、全局異常處理

此類是對全局異常的處理,根據本身狀況,是否對不一樣種類的異常進行處理。例如如下是單獨對業務異常,接口參數異常,以及剩餘的全部異常進行處理,並生成接口統一格式信息,返回給調用接口的客戶端,進行展現。

首先咱們須要在處理全局異常的類上面,加上 @ControllerAdvice 或者 @RestControllerAdvice註解。@ControllerAdvice 註解能處理 @Controller@RestController 類型的接口調用時產生的異常,而 @RestControllerAdvice 註解只能處理 @RestController 類型接口調用時產生的異常。咱們通常用 @ControllerAdvice 註解。

@ExceptionHandler 只能註解在方法上,表示這是一個處理異常的方法,value 屬性能夠填寫須要處理的異常類,能夠是數組。

@ResponseBody 註解表示咱們返回的信息是響應體數據。dom

package com.nobody.exception;

import javax.servlet.http.HttpServletRequest;

import com.nobody.pojo.vo.GeneralResult;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @Description 統一異常處理
 * @Author Mr.nobody
 * @Date 2020/10/23
 * @Version 1.0
 */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 處理自定義的業務異常
    @ExceptionHandler(value = BizException.class)
    @ResponseBody
    public GeneralResult<Object> restErrorHandler(HttpServletRequest request, BizException e) {
        String err = "requestURI:" + request.getRequestURI() + ",errorCode:" + e.getErrorCode()
                + ",errorMsg:" + e.getErrorMsg();
        log.error(err, e);
        return GeneralResult.genErrorResult(e.getMessage(), e.getErrorCode(), e.getTraceId());
    }

    // 處理接口參數數據格式錯誤異常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public GeneralResult<Object> errorHandler(HttpServletRequest request,
            MethodArgumentNotValidException e) {
        StringBuilder message = new StringBuilder();
        String err = null;
        e.getBindingResult().getAllErrors()
                .forEach(error -> message.append(error.getDefaultMessage()).append(";"));
        String des = message.toString();
        if (!StringUtils.isEmpty(des)) {
            err = des.substring(0, des.length() - 1);
        }
        log.error(err + ",requestURI:" + request.getRequestURI(), e);
        return GeneralResult.genErrorResult(CommonErrorEnum.BODY_NOT_MATCH.getErrorMsg(),
                CommonErrorEnum.BODY_NOT_MATCH.getErrorCode(), MDC.get("traceId"));
    }

    // 處理其餘異常
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public GeneralResult<Object> errorHandler(HttpServletRequest request, Exception e) {
        log.error("internal server error,requestURI:" + request.getRequestURI(), e);
        return GeneralResult.genErrorResult(CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorMsg(),
                CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorCode(), MDC.get("traceId"));
    }
}
7、測試

7.1 輔助類

測試會針對不一樣狀況進行驗證,如下是一些測試須要用到的類。maven

package com.nobody.pojo.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

/**
 * @Description 用戶實體類
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
@AllArgsConstructor
@Getter
@Setter
public class UserEntity implements Serializable {

    private static final long serialVersionUID = 5564446583860234738L;

    private String id;
    private String name;
    private int age;

}
package com.nobody.pojo.dto;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;

/**
 * @Description 添加用戶時參數類
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
@Getter
@Setter
public class UserDTO {
    @NotEmpty(message = "用戶名不能爲空")
    private String name;
    @Min(value = 0, message = "年齡最小不能低於0")
    private int age;
}

如下簡單模擬 User 相關業務,而後產生不一樣的異常。ide

package com.nobody.service;

import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
public interface UserService {
    UserEntity add(UserDTO userDTO);

    UserEntity getById(String id);

    void marry(String age);
}
package com.nobody.service.impl;

import com.nobody.exception.BizException;
import com.nobody.exception.UserErrorEnum;
import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;
import com.nobody.service.UserService;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.UUID;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
@Service
public class UserServiceImpl implements UserService {
    @Override
    public UserEntity add(UserDTO userDTO) {
        String userId = UUID.randomUUID().toString();
        return new UserEntity(userId, userDTO.getName(), userDTO.getAge());
    }

    @Override
    public UserEntity getById(String id) {
        // 模擬業務異常 
        if (Objects.equals(id, "000")) {
            throw new BizException(UserErrorEnum.USER_NOT_FOUND);
        }
        return new UserEntity(id, "Mr.nobody", 18);
    }

    @Override
    public void marry(String age) {
        // 當age不是數字字符串時,拋出異常
        Integer integerAge = Integer.valueOf(age);
        System.out.println(integerAge);
    }
}

接口類定義,根據不一樣參數調用接口,可產生不一樣的異常錯誤。

package com.nobody.controller;

import com.nobody.pojo.dto.UserDTO;
import com.nobody.pojo.entity.UserEntity;
import com.nobody.pojo.vo.GeneralResult;
import com.nobody.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/2/6
 * @Version 1.0
 */
@RestController
@RequestMapping("user")
public class UserController {

    private UserService userService;

    public UserController(final UserService userService) {
        this.userService = userService;
    }

    @PostMapping("add")
    public GeneralResult<UserEntity> add(@RequestBody @Valid UserDTO userDTO) {
        UserEntity user = userService.add(userDTO);
        return GeneralResult.genSucce***esult(user);
    }

    @GetMapping("find/{userId}")
    public GeneralResult<UserEntity> find(@PathVariable String userId) {
        UserEntity user = userService.getById(userId);
        return GeneralResult.genSucce***esult(user);
    }

    @GetMapping("marry/{age}")
    public GeneralResult<UserEntity> marry(@PathVariable String age) {
        userService.marry(age);
        return GeneralResult.genSucce***esult();
    }

}

7.2 測試結果

啓動服務,進行接口調用,本此演示用的 IDEA 自帶的 HTTP Client 工具進行調用,固然你也可使用 Postman 進行調用。

在這裏插入圖片描述

首先演示正常的接口調用,服務沒有報錯,接口也返回正常數據。

在這裏插入圖片描述

仍是調用查詢用戶接口,演示用戶不存在狀況,服務報錯打印日誌,接口也返回錯誤信息。

在這裏插入圖片描述
在這裏插入圖片描述

再演示添加用戶操做,用戶名不填值,程序報錯打印日誌,接口也返回錯誤信息。

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

再演示其餘異常狀況,例如解析數字出錯。

在這裏插入圖片描述
在這裏插入圖片描述

相關文章
相關標籤/搜索