Spring中的全局异常处理(实用)

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

相信大家在平时写代码的时候都使用过 try catch 来处理异常,特别是在前端调用的后端接口中,如果我们没做异常处理,后端直接返回错误信息给前端,前端直接把程序员才看懂的错误信息展示给了用户,想必会造成很不好的用户体验。

为了防止发生这种情况,我们就需要在后端接口中 try catch 处理好异常,将更友好的错误信息返回给用户,比如:服务器内部异常、校验异常等等。而我们一个个的在接口中加上 try catch 有些麻烦,此时我们就可以使用全局异常处理机制

@ControllerAdvice 注解实现全局异常处理

在 Spring 3.2 中,新增了 @ControllerAdvice@RestControllerAdvice 注解。这两个注解的功能呢其实是一样的。区别就像 @Controller@RestController 之间的区别。

@ControllerAdvice 作用:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

@ControllerAdvice 用法:

  1. @ExceptionHandler 注解标注的方法:用于捕获 Controller 中抛出的不同类型的异常,从而达到异常全局处理的目的;
  2. @InitBinder 注解标注的方法:用于请求中注册自定义参数的解析,从而达到自定义请求参数格式的目的;
  3. @ModelAttribute 注解标注的方法:表示此方法会在执行目标 Controller 方法之前执行 。

全局异常处理具体用法:

今天呐,我们主要来探讨的是 @ControllerAdvice 结合 @ExceptionHandler 注解用于全局异常的处理。

至于 @ControllerAdvice 的其他作用,本文不做探讨。有兴趣的小伙伴可以自己学习下。

代码实现:

创建一个 GlobalExceptionHandler 类,添加上 @RestControllerAdvice 注解

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler
{
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 基础异常
     */
    @ExceptionHandler(BaseException.class)
    public AjaxResult baseException(BaseException e)
    {
        return AjaxResult.error(e.getCode(),e.getDefaultMessage());
    }

    /**
     * 业务异常
     */
    @ExceptionHandler(CustomException.class)
    public AjaxResult businessException(CustomException e)
    {
        if (StringUtils.isNull(e.getCode()))
        {
            return AjaxResult.error(e.getMessage());
        }
        return AjaxResult.error(e.getCode(), e.getMessage());
    }

    /**
     * 其他异常
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e)
    {
        log.error(e.getMessage(), e);
        return AjaxResult.error(HttpStatus.ERROR,"系统内部错误");
    }

    /**
     * 自定义验证异常
     */
    @ExceptionHandler(BindException.class)
    public AjaxResult validatedBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return AjaxResult.error(message);
    }

    /**
     * 权限异常
     */
    @ExceptionHandler(PreAuthorizeException.class)
    public AjaxResult preAuthorizeException(PreAuthorizeException e)
    {
        return AjaxResult.error("没有权限,请联系管理员授权");
    }
}
复制代码

在该类中,可以定义多个方法,不同的方法处理不同的异常,例如你还可以定义专门处理空指针的方法、专门处理数组越界的方法等等。Controller 中的不同的异常就会进入这个类中的对应异常的方法中。

来看一下我们自定义的BaseException异常:

/**
 * 基础异常
 */
public class BaseException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    /**
     * 所属模块
     */
    private String module;

    /**
     * 错误码
     */
    private String code = HttpStatus.ERROR;

    /**
     * 错误码对应的参数
     */
    private Object[] args;

    /**
     * 错误消息
     */
    private String defaultMessage;

    public BaseException(String module, String code, Object[] args, String defaultMessage)
    {
        this.module = module;
        this.code = code;
        this.args = args;
        this.defaultMessage = defaultMessage;
    }

    public BaseException(String module, String code, Object[] args)
    {
        this(module, code, args, null);
    }

    public BaseException(String code, String defaultMessage)
    {
        this(null, code, null, defaultMessage);
    }

    public BaseException(String code, Object[] args)
    {
        this(null, code, args, null);
    }

    public BaseException(String defaultMessage)
    {
        this(null, HttpStatus.ERROR, null, defaultMessage);
    }

    public String getModule()
    {
        return module;
    }

    public String getCode()
    {
        return code;
    }

    public Object[] getArgs()
    {
        return args;
    }

    public String getDefaultMessage()
    {
        return defaultMessage;
    }
}
复制代码

再贴出 AjaxResult.java 的代码(来自开源项目若依管理系统):

public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /** 状态码 */
    public static final String CODE_TAG = "code";

    /** 返回内容 */
    public static final String MSG_TAG = "msg";

    /** 数据对象 */
    public static final String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
     */
    public AjaxResult()
    {
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     */
    public AjaxResult(String code, String msg)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一个新创建的 AjaxResult 对象
     *
     * @param code 状态码
     * @param msg 返回内容
     * @param data 数据对象
     */
    public AjaxResult(String code, String msg, Object data)
    {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (StringUtils.isNotNull(data))
        {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 方便链式调用
     *
     * @param key
     * @param value
     * @return
     */
    @Override
    public AjaxResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success()
    {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功数据
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data)
    {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @return
     */
    public static AjaxResult error()
    {
        return AjaxResult.error("操作失败");
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String msg)
    {
        return AjaxResult.error(HttpStatus.ERROR, msg);
    }

    /**
     * 返回错误消息
     *
     * @param msg 返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data)
    {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String code, String msg)
    {
        return new AjaxResult(code, msg, null);
    }

    /**
     * 返回错误消息
     *
     * @param code 状态码
     * @param msg 返回内容
     * @return 警告消息
     */
    public static AjaxResult error(String code, String msg, Object data)
    {
        return new AjaxResult(code, msg, data);
    }
}
复制代码

总结

除了使用 @ControllerAdvice 之外,还可以通过实现 HandlerExceptionResolver 接口类实现全局异常处理机制。这里就不在叙述了,感兴趣的小伙伴自己去查。

基于 @ControllerAdvice 实现,我们平时写代码的时候,就会在 Service 层抛出异常到 Controller 层,而不是直接 try catch 处理。如果自己 try catch 处理了,那么就不会再进入到 GlobalExceptionHandler 类中了。

当然除了返回规范的错误信息给用户,你也可以在 GlobalExceptionHandler 方法中做其他的工作,比如异常统计什么的。

还有一点想要写一下:

在 service 方法里面如果对异常进行了捕获并处理的话,该事务是不会进行回滚的,@Transactional会失效

或者在 catch 语句中最后增加 throw new RuntimeException() 语句,以便让 aop 捕获异常再去回滚。

又或者在 catch 语句中增加下面代码来手动回滚:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
复制代码