likes
comments
collection
share

全局异常处理

作者站长头像
站长
· 阅读数 29

本节视频教程链接

还记得前面我们的实现逻辑,在service层如果有校验失败我们会抛出异常,而controller中我们对其并没有处理,实际上spring boot有自己全局的错误处理形式。我们可以基于spring boot提供的切面特性,来很轻松的实现全局异常的处理,现在我们约定,只要后台逻辑处理失败的情况,我们都将抛出异常,包括controller中的处理逻辑。

实现全局异常处理器

现在我们就来编写全局异常处理器。

package com.xiaojuan.boot.common.web.support;

import ...

import static com.xiaojuan.boot.common.enums.BusinessError.*;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /** 维护一个错误码与http状态码的映射 */
    private Map<Integer, HttpStatus> httpStatusMap;

    @PostConstruct
    private void init() {
        httpStatusMap = new HashMap<>();
        httpStatusMap.put(NO_LOGIN.getValue(), HttpStatus.UNAUTHORIZED);
        httpStatusMap.put(HAS_NO_ROLE.getValue(), HttpStatus.FORBIDDEN);
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Response<?>> handleException(BusinessException ex) {
        log.error(ex.getMessage(), ex);
        // 默认服务器端错误,返回500状态码
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        if (!StringUtils.isEmpty(ex.getErrCode()) && httpStatusMap.containsKey(ex.getErrCode())) {
            status = httpStatusMap.get(ex.getErrCode());
        }
        return ResponseEntity.status(status).body(Response.fail(ex.getMessage(), ex.getErrCode(), ex.getData()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Response<?>> handleException(Exception ex) {
        log.error(ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Response.fail("小卷生鲜电商系统异常:" + ex.getMessage()));
    }

}

代码详解

这里我们用了@RestControllerAdvice注解对所有的@RestController修饰的controller都进行拦截,只要抛出了异常,就交给这个处理器来处理。

这里要说下响应的http状态码,一般都返回200,某些情况下我们将返回一个可以让前端特别处理的状态码,比如用户未登录的请求被拦截了我们可以返回401,用户登录了却没有权限的操作,我们返回403,这样前端对这块直接从http状态就能识别请求结果的状态。而对于我们业务开发来说,关注的是业务处理的错误码,在这里我们和请求http状态码之间做了一个映射转换,维护在httpStatusMap成员变量中。

接下来,我们写了两个异常处理方法,它们都必须用@ExceptionHandler注解来指定我们要处理的异常的类型,这里我们只考虑两种异常:我们自定义的业务异常BusinessException和最大的Exception

对于BusinessException,我们要考虑HttpStatus的判断逻辑,如果我们将某些业务错误码和http状态码做了映射,那么对于这些业务错误码我们就取映射到的http状态码来返回,否则我们返回服务器端错误的500状态码。

注意处理方法最后的返回结果,我们使用的是ResponseEntity<Response<?>>,spring web模块提供的ResponseEntity可以帮助我们按照指定的http状态码并解析指定格式的内容体(Spring Boot默认配置json形式)进行前端响应。而要响应的对象就是我们之前定义的Response,只不过我们将字段errCode的类型从原来的String改成了Integer,因为业务错误码我们就定义为数值型,同样的还有BusinessException类中的errCode字段类型的调整。

除了BusinessException,我们只处理最大的Exception,http状态码固定为500,错误消息也统一以固定的形式开头。

错误码枚举类

将应用中业务错误码我们定义在一个枚举中进行维护:

package com.xiaojuan.boot.common.enums;

import ...

@Getter
@AllArgsConstructor
public enum BusinessError {

    PARAM_INVALID("参数校验失败", 10001),
    RECORD_EXISTS("后台记录已存在", 10002),
    HAS_NO_ROLE("用户未授权,不能访问", 403),
    NO_LOGIN("请先登录再操作", 401);

    private final String label;
    private final Integer value;
}

这里我们给出默认的说明,一般在抛出业务异常时我们可以指定更具体的错误,而不会使用这里默认的错误消息。

service层抛出异常调整

现在我们对service层抛出异常做下调整,一般我们只要传入错误信息来构造BusinessException。有些时候我们还可以传入错误码来进一步区分这些错误。

全局异常处理

全局异常处理

实现自己的Assert工具

既然我们先前定义了自己的BusinessException,并且在其中维护了业务错误码errCode,就没必要用spring的Assert工具了,因为它抛出来的是IllegalStateException异常,最终被我们全局异常处理器以最大的Exception的处理逻辑进行包装响应结果,自然“参数校验失败”的错误码类型就丢了,因此这里我们基于spring对Assert的实现,我们包装为自己的Assert

package com.xiaojuan.boot.util;

import ...

public class Assert {
    public static void hasText(@Nullable String text, String message) {
        if (!StringUtils.hasText(text)) {
            throw new BusinessException(message, BusinessError.PARAM_INVALID.getValue());
        }
    }
}

controller层抛出异常

最后我们将原来预留todo注释的地方改为抛出异常的形式:

全局异常处理

最后,我们再基于test.http对抛出异常的测试场景进行测试,看是否达到预期的http状态码和json结构,这个大家自行测试。