使用@RestControllerAdvice注解进行统一异常拦截

使用@RestControllerAdvice注解进行统一异常拦截

在业务编写过程中总会出现各种异常,我们通常会使用try-catch包裹,并return一个友好的错误信息,但这样会让我们的代码可读性变得很差,对于没有捕获到的异常则可能会暴露敏感信息。

那有没有什么方法可以忽略掉异常,甚至主动抛出异常以代替return终止代码的执行,而不需要担心未经捕获的异常被一路抛到客户端呢?

@ControllerAdvice以及@RestControllerAdvice是Spring提供的两个对Controller进行增强操作的注解。

两个注解的区别在于@RestControllerAdvice是基于Restful风格实现的,默认返回JSON格式数据且不需要使用@ResponseBody,不可以做视图跳转,一般用于接口服务。若项目没有前后端分离需要Java实现页面跳转则需要使用@ControllerAdvice。

其中包含一下三个方法级注解:

  • 全局数据绑定:@ModelAttribute
  • 全局数据处理:@InitBinder
  • 全局异常处理:@ExceptionHandler

本文将对@ExceptionHandler的使用进行介绍。

当异常被抛到Controller时,会被以下代码拦截,并返回500状态码。

@RestControllerAdvice
public class ExceptionHandel {
    //对Exception类进行拦截
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> baseExceptionHandel(Exception ex) {
        ex.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器异常");
    }
}

@ExceptionHandler(Exception.class)表示拦截所有Exception及它的子类,实际使用中一般是作为兜底的处理,拦截那些意料之外的异常,避免暴露敏感信息。所以可以搭配自定义异常进行更新精度的异常处理。

比如,可以新增一个Token校验的异常,区别于Exception的500状态码,我们不需要打印异常堆栈信息,而需要返回302状态码和一个有好的错误信息,以便web及时识别登录状态的失效并跳转至登录页面。

自定义异常:

public class TokenException extends RuntimeException {
    public TokenException(String message) {
        super(message);
    }
}

异常拦截:

@ExceptionHandler(TokenException.class)
public ResponseEntity<String> noAuthorityExceptionHandel(TokenException ex) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage());
}

这样我们就可以在代码随意抛出异常,在并发量不是特别大的项目中甚至可以代替return语句返回错误信息:

Map<String, Object> userInfo = loginTokenCache.getIfPresent(token);
if (userInfo == null || userInfo.isEmpty()) {
    log.error("{}缓存中无token信息", request.getRequestURI());
    throw new TokenException("缓存中无token信息");
}

对于dao操作,可以搭配@Transactional实现自动回滚,只要抛出的异常继承自RuntimeException,Spring都可以自动进行事务回滚,而不需要手动操作事务管理器。

@Transactional(rollbackFor = Exception.class)
public void deleteLabel(Map<String, String> params) {
    //删除操作
    int deleteSuccess = labelManageDao.deleteLabel(params);
    //更新操作
    int updateSuccess = labelManageDao.updateUserLabelRel(params);
    //任一操作未成功,则抛出异常进行回滚
    if (deleteSuccess == 0 || updateSuccess == 0){
        throw new UpdateException("删除失败");
    }
}