互联网大厂是如何处理全局异常的? 一、为何要处理全局异常? 二、开发环境 三、添加依赖 四、自定义异常错误类 五、接口返回统一格式 六、全局异常处理 七、测试

关注微信公众号【Java之言】,更多干货文章学习资料,助你放弃编程之路!


一、为何要处理全局异常?

在平常项目开发过程中,程序难免会出现运行时异常,或者业务异常。难道要针对每一处可能出现的异常进行编写代码进行处理?或者直接不处理异常,将一大屏堆满英文的异常信息显示给用户?那用户体验性是何等极差。
所以,当程序抛异常时,为了日志的可读性排查 Bug简单,以及更好的用户体验性,所以我们要对全局异常进行处理。

二、开发环境

  1. JDK 1.8 或者1.8以上
  2. Springboot (此演示版本为 Springboot 2.1.18.RELEASE)
  3. Gradle (当然也可用Maven,其实目的都是为构建项目,管理依赖等)

三、添加依赖

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.1 自定义异常基础接口类

因为要做成通用性,所以我们定义一个异常基础接口类,自定义的异常枚举类需实现该接口。

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 通用异常信息枚举类

通用异常信息枚举类,这里定义的所有异常信息是整个程序通用的。

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;
    }
}
复制代码

五、接口返回统一格式

为方便前端对接口返回的数据进行处理,也是规范问题,所以我们要定义接口返回统一格式。

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> genSuccessResult(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> genSuccessResult() {
        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;
    }

}
复制代码

六、全局异常处理

此类是对全局异常的处理,根据自己情况,是否对不同种类的异常进行处理。例如以下是单独对业务异常,接口参数异常,以及剩余的所有异常进行处理,并生成接口统一格式信息,返回给调用接口的客户端,进行展示。
首先我们需要在处理全局异常的类上面,加上 @ControllerAdvice 或者 @RestControllerAdvice注解。@ControllerAdvice 注解能处理 @Controller@RestController 类型的接口调用时产生的异常,而 @RestControllerAdvice 注解只能处理 @RestController 类型接口调用时产生的异常。我们一般用 @ControllerAdvice 注解。
@ExceptionHandler 只能注解在方法上,表示这是一个处理异常的方法,value 属性可以填写需要处理的异常类,可以是数组。
@ResponseBody 注解表示我们返回的信息是响应体数据。

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.1 辅助类

测试会针对不同情况进行验证,以下是一些测试需要用到的类。

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 相关业务,然后产生不同的异常。

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.genSuccessResult(user);
    }

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

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

}
复制代码

7.2 测试结果

启动服务,进行接口调用,本此演示用的 IDEA 自带的 HTTP Client 工具进行调用,当然你也可以使用 Postman 进行调用。

在这里插入图片描述

首先演示正常的接口调用,服务没有报错,接口也返回正常数据。

在这里插入图片描述

还是调用查询用户接口,演示用户不存在情况,服务报错打印日志,接口也返回错误信息。

在这里插入图片描述
在这里插入图片描述

再演示添加用户操作,用户名不填值,程序报错打印日志,接口也返回错误信息。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

再演示其他异常情况,例如解析数字出错。

在这里插入图片描述
在这里插入图片描述

此演示项目已上传到Github,如有需要可自行下载,欢迎 Star
github.com/LucioChn/sp…

关注微信公众号【Java之言】,更多干货文章学习资料,助你放弃编程之路!

在这里插入图片描述