全局异常处理
还记得前面我们的实现逻辑,在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结构,这个大家自行测试。
转载自:https://juejin.cn/post/7276675247596634123