在项目中非常完美的全局异常处理

通用异常处理

1 场景预设

​ 项目应用场景:当客户端执行某个业务,服务端进行处理业务-Controller-Service-Dao 过程中可能由于用户,代码问题导致业务执行失败,给用户一个提示,告知用户执行结果,如果失败告知原因。

我们预设这样一个场景,假如我们做新增用户,需要接收下面的参数:

name:名称
age:年龄

然后对数据做简单校验:

  • 年龄不能为空

需求:新增时,自动生成ID,然后随用户对象一起返回

开发业务功能步骤:

  • 实体类 PO DTO
  • 持久层Mapper
  • 业务层Service
  • 控制层Controller
  • 完成三层注入
  • 按照接口文档开发业务功能

8.1.1 代码

在nc_sys_pojo中增加用户实体类:

package com.itheima.sys.entity;

import lombok.Data;

/**
 * @author heima
 */
@Data
public class UserDomain {

    private Long id;
    private String name;
    private Integer age;
}

在nc_sys_service中增加用户业务类:

package com.itheima.sys.service;

import com.itheima.sys.entity.UserDomain;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class UserDominService {


    /**
     * 模拟测试保存用户记录
     * @param user
     * @return
     */
    public UserDomain saveUser(UserDomain user) {
        Random random = new Random(1000);
        user.setId(random.nextLong());
        return user;
    }
}

在nc_sys_service中增加用户控制器类:

package com.itheima.sys.controller;

import com.itheima.common.vo.Result;
import com.itheima.sys.entity.UserDomain;
import com.itheima.sys.service.UserDominService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试控制器
 * @author itheima
 */
@RestController
public class TestController {

    @Autowired
    private UserDominService userDominService;
    
    @GetMapping("/test")
    public Result<String> hello(){
        return new Result<>(true, 200, "请求成功!", "hello nineclock!");
    }

    /**
     * 全局异常场景模拟方法
     * @param user
     * @return
     */
    @PostMapping("/test/user")
    public Result saveUser(@RequestBody UserDomain user){
        //如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
        if (user.getAge() == null) {
            throw new RuntimeException("用户年龄为必填项!");
        }
        return new Result(true, 200, "保存成功", userDominService.saveUser(user));
    }

}

8.1.2 测试异常

使用POSTMAN发送请求进行测试,注意提交参数格式为Json.

8.1.3 问题分析

​ 刚才的处理看似完美,但是仔细想想,我们在异常处理中,如果抛出RuntimeException交给框架处理返回状态码为固定的500了,仅凭用户抛出的异常,我们根本无法判断到底该返回怎样的状态码,可能是参数有误、数据库异常、没有权限,等等各种情况。

​ 解决问题的思路为,所有的异常全部抛出,然后统一的拦截到异常处理,给前端返回结果。

8.2 统一异常处理

接下来,我们使用SpringMVC提供的统一异常拦截器,因为是统一处理,我们放到nc_common项目中:

新建一个全局异常处理类,名为:BasicExceptionAdvice 注意:必须保证被启动类能够扫描到。

具体代码如下:

package com.itheima.common.exception.advice;

import com.itheima.common.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author: itheima
 * @create: 2021-07-23 12:07
 */
@ResponseBody
@ControllerAdvice  //对controller层进行AOP增强,异常通知
public class BasicExceptionAdvice {


    /**
     * 处理RuntimeException类型异常
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public Result<String> handlerRuntimeErr(RuntimeException e){
        return new Result<>(false, 500, "执行业务失败!"+e.getMessage());
    }
}

解读:

  • @ControllerAdvice:默认情况下,会拦截所有加了@Controller的类

  • @ExceptionHandler(RuntimeException.class):作用在方法上,声明要处理的异常类型,可以有多个,这里指定的是RuntimeException。被声明的方法可以看做是一个SpringMVC的Handler

    • 参数是要处理的异常,类型必须要匹配
    • 返回结果可以是ModelAndViewResponseEntity等,基本与handler类似

重启系统微服务项目进行测试:

成功返回了错误信息!

8.2.1 异常信息及状态

刚才的处理看似完美,但是仔细想想,我们在通用的异常处理中,把返回状态码写死为400了:

这样显然不太合理。

因此,用户抛出异常时,就必须传递两个内容:

  • 异常信息
  • 异常状态码

但是RuntimeException是无法接受状态码的,只能接受异常的消息,所以我们需要做两件事情:

  • 自定义异常,来接受状态码、异常消息
  • 状态码与异常消息可能会重复使用,我们通过枚举来把这些信息变为常量

8.2.2 自定义异常枚举类型

枚举:把一件事情的所有可能性列举出来。在计算机中,枚举也可以叫多例,单例是多例的一种情况 。

单例:一个类只能有一个实例。

多例:一个类只能有有限个数的实例。

我们定义一个枚举,用于封装异常状态码和异常信息:

package com.itheima.common.enums;

import lombok.Getter;

@Getter
public enum ResponseEnum {

    //一定要将选择项放在最上
    AGE_NOT_NULL(400, "年龄不能为空!"),
    SUCCESS(200, "操作成功!"),
    ERROR(500, "操作失败!"),
    ;

    private Integer code;
    private String message;

    //提供私有无参构造
    ResponseEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}


8.2.3 自定义异常

然后自定义异常,来获取枚举对象。

在nc_common中定义自定义异常类:

package com.itheima.common.exception;

import com.itheima.common.exception.enums.ResponseEnum;
import lombok.Getter;

/**
 * 增加业务执行状态码属性
 */
@Getter
public class NcException extends RuntimeException {


    //业务状态码
    private Integer code;


    /**
     * 通过构造注入值
     * @param code
     * @param message
     */
    public NcException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    /**
     * 自定义异常状态码提示消息,通过枚举类型
     * @param enums
     */
    public NcException(ResponseEnum enums) {
        super(enums.getMessage());
        this.code = enums.getCode();
    }
}

修改Controller中代码:

package com.itheima.sys.controller;

import com.itheima.common.enums.ResponseEnum;
import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import com.itheima.sys.entity.UserDomain;
import com.itheima.sys.service.UserDominService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private UserDominService userDominService;

   /**
     * 全局异常场景模拟方法
     * @param user
     * @return
     */
    @PostMapping("/test/user")
    public Result saveUser(@RequestBody UserDomain user){
        //如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
        if (user.getAge() == null) {
            //throw new RuntimeException("用户年龄为必填项!");
            throw new NcException(ResponseEnum.AGE_NOT_NULL);
        }
        return new Result(true, 200, "保存成功", userDominService.saveUser(user));
    }
}

修改全局异常处理逻辑:

package com.itheima.common.exception.advice;

import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @Author: 黑马程序员
 * @description:
 **/
@Slf4j
@ResponseBody
@ControllerAdvice
public class BasicExceptionAdvice {

    @ExceptionHandler(RuntimeException.class)
    public Result handleException(RuntimeException e) {
        // 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
        return new Result(false, 400, e.getMessage());
    }

    /**
     * 处理自定义异常
     * @return
     */
    @ExceptionHandler(NcException.class)
    public Result handlerNcException(NcException e){
        return new Result(false, e.getCode(), e.getMessage());
    }

    /**
     * 处理其他异常
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Result handlerNcException(Exception e){
        return new Result(false, 500, e.getMessage());
    }
}

测试结果如下:

8.2.4 封装返回结果

将来返回结果给前端,不管成功还是失败都需要返回结果对象Result,目前每次返回都必修手动New一个对象,使用起来比较繁琐。改为将成功,以及失败的Result对象返回的静态方法封装在Result类中。

package com.itheima.common.vo;

import com.itheima.common.exception.enums.ResponseEnum;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

/**
 * @program: 黑马程序员
 * @description: 服务端提供前端返回的结果
 **/
@Data
public class Result<T> implements Serializable {

    /**
     * 是否成功
     */
    private Boolean success;
    /**
     * 返回状态码:执行业务状态
     */
    private int code;
    /**
     * 提示消息
     */
    private String message;
    /**
     * 返回数据
     */
    private T data;

    public Result() {
    }

    public Result(Boolean success, int code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }

    public Result(Boolean success, int code) {
        this.success = success;
        this.code = code;
    }

    public Result(Boolean success, int code, T data) {
        this.success = success;
        this.code = code;
        this.data = data;
    }

    public Result(Boolean success, int code, String message, T data) {
        this.success = success;
        this.code = code;
        this.message = message;
        this.data = data;
    }



    //TODO 问题:响应结果在controller方法中需要每次都new 解决:抽取静态方法代表成功 或者 失败结果

    public static <T> Result<T> success() {
        return new Result<T>(true, ResponseEnum.SUCCESS.getCode());
    }

    public static <T> Result<T> successMessage(String message) {
        return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), message);
    }

    public static <T> Result<T> success(T data) {
        return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), data);
    }

    public static <T> Result<T> success(String message, T data) {
        return new Result<T>(true, ResponseEnum.SUCCESS.getCode(), message, data);
    }

    public static <T> Result<T> error() {
        return new Result<T>(false, ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
    }

    public static <T> Result<T> errorMessage(String errorMessage) {
        return new Result<T>(false, ResponseEnum.ERROR.getCode(), errorMessage);
    }

    public static <T> Result<T> errorCodeMessage(int errorCode, String errorMessage) {
        return new Result<T>(false, errorCode, errorMessage);
    }
}

修改项目中正常以及异常处理方法返回结果:

package com.itheima.common.exception.advice;

import com.itheima.common.exception.NcException;
import com.itheima.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @Author: 黑马程序员
 * @description:
 **/
@Slf4j
@ResponseBody
@ControllerAdvice
public class BasicExceptionAdvice {

    @ExceptionHandler(RuntimeException.class)
    public Result handleException(RuntimeException e) {
        // 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
        //return new Result(false, 500, e.getMessage());
        return Result.errorCodeMessage(500, e.getMessage());
    }

    /**
     * 处理自定义异常
     * @return
     */
    @ExceptionHandler(NcException.class)
    public Result handlerNcException(NcException e){
        //return new Result(false, exception.getCode(), exception.getMessage());
        return Result.errorCodeMessage(e.getCode(), e.getMessage());
    }

    /**
     * 处理其他异常
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Result handlerNcException(Exception e){
        //return new Result(false, 500, e.getMessage());
        return Result.errorCodeMessage(500, e.getMessage());
    }

}

再次修改TestController中代码:

8.3 返回异常枚举类

如果还有其他异常提示信息,只需要增加即可!

package com.itheima.common.exception.enums;

import lombok.Getter;

@Getter
public enum ResponseEnum {

    //一定要将选择项放在最上
    AGE_NOT_NULL(400, "年龄不能为空!"),
    SUCCESS(200, "操作成功!"),
    ERROR(500, "操作失败!"),

    INVALID_FILE_TYPE(400, "无效的文件类型!"),
    INVALID_PARAM_ERROR(400, "无效的请求参数!"),
    INVALID_PHONE_NUMBER(400, "无效的手机号码"),
    INVALID_VERIFY_CODE(400, "验证码错误!"),
    INVALID_USERNAME_PASSWORD(400, "无效的用户名和密码!"),
    INVALID_SERVER_ID_SECRET(400, "无效的服务id和密钥!"),
    INVALID_NOTIFY_PARAM(400, "回调参数有误!"),
    INVALID_NOTIFY_SIGN(400, "回调签名有误!"),

    DATA_TRANSFER_ERROR(500, "数据转换异常!"),
    INSERT_OPERATION_FAIL(500, "新增操作失败!"),
    UPDATE_OPERATION_FAIL(500, "更新操作失败!"),
    DELETE_OPERATION_FAIL(500, "删除操作失败!"),
    FILE_UPLOAD_ERROR(500, "文件上传失败!"),
    DIRECTORY_WRITER_ERROR(500, "目录写入失败!"),
    FILE_WRITER_ERROR(500, "文件写入失败!"),
    SEND_MESSAGE_ERROR(500, "短信发送失败!"),
    CODE_IMAGE_ERROR(500, "验证码错误!"),

    USER_MOBILE_EXISTS(500, "该手机号已经被注册!"),
    USER_NOT_REGISTER(500, "当前用户还未进行注册!"),
    USER_NOT_JOIN_COMPANY(500, "请加入企业后再使用该功能!"),
    USER_NOT_COMPANY_ADMIN(500, "您不是企业管理员!"),
    USER_NOT_MATCH_ATTGROUP(500, "未查询到用户考勤组,请先配置!"),
    USER_NOT_FOUND(500, "没有查询到用户!"),

    COMPANY_ADMIN_NOT_EXISTS(500, "没有找到对应企业管理员!"),
    COMPANY_NOT_FOUND(500, "企业不存在!"),
    WROK_NUM_EXISTS(500, "当前工号已经存在!"),

    COMPANY_USER_NOT_FOUND(404, "企业员工不存在!"),
    SMS_CODE_TIMEOUT(404, "验证码超时,请重新发送!"),

    PUNCH_INVALID_AREA(500, "打卡地点不在考勤点范围内!"),
    PUNCH_INVALID_DAY(500, "非工作日无需打卡!"),
    PUNCH_ALREADY(500, "已经打卡,无需重复打卡!"),
    SIGN_DATA_NULL(404, "未检索到签到数据!"),

    MESSAGE_PARAMS_LOST(500, "查询参数缺失!"),

    UNAUTHORIZED(401, "登录失效或未登录!"),
    FOBIDDEN(403, "禁止访问!"),
    SIGNDATA_NOT_FOUND(500, "当前检索条件没有签到数据!"),

    ROLE_NOT_FOUND(403, "角色列表不存在!"),
    ROLE_SYS_NOT_FOUND(403, "系统管理员角色不存在!"),
    SYS_PERMISSSION_NOT_FOUND(403, "当前企业无权限"),


    DOC_NOT_FOUND(403, "文档不存在或者已经删除!"),
    DOC_NOT_ALLOWED(403, "只有作者才能设置协作者!"),
    FILE_NOT_ALLOWED_MODIFY(403, "您没有权限修改文档!"),
    FILE_NOT_BELONG_YOU(403, "您不是该文档拥有者,无法设置权限!"),
    ;

    private Integer code;
    private String message;

    //提供私有无参构造
    ResponseEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值