1.概述
在日常开发中,如何快速定位异常并处理异常,体现了一个研发人员解决问题的能力,需要不断在业务发展中精进自己的业务水平,提升对原理的理解。快速解决的前提是快速定位问题,合理的异常处理能够帮助我们快速抛出问题。在SpringBoot中,全局异常处理能够有效降低异常代码数量,提供统一的异常处理模式,帮助我们从代码层面优雅地抛出异常。本文将演示SpringBoot中全局异常使用,并进行原理分析。
2.全局异常处理器
2.1 定义错误枚举类
随着业务的不断发展,系统可能会变得越来约庞大,可能产生和处理的异常就会逐渐增多,统一的异常文档说明必不可少,它能有效提供异常描述,进而进行异常定位。在JAVA语言中,通常采用枚举类统一定义异常编码与说明,如下图所示:
public enum ExceptionEnum {
// 数据操作错误定义
SUCCESS("2000", "成功!"),
CODE_8001("8001", "Service unavailable"),
CODE_8002("8002", "JSON exception"),
CODE_8003("8003", "Parameter is invalid"),
CODE_8004("8004", "Db error"),
CODE_8005("8005", "{0}参数不能为空"),
BODY_NOT_MATCH("4000", "请求的数据格式异常:{0}"),
SIGNATURE_NOT_MATCH("4001", "请求的数字签名不匹配:{0}"),
NOT_FOUND("4004", "未找到该资源:{0}"),
INTERNAL_SERVER_ERROR("5000", "服务器内部错误:{0}"),
SERVER_BUSY("5003", "服务器正忙,请稍后再试:{0}");
private final String errorCode;
private final String errorMsg;
ExceptionEnum(String errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public String getCode() {
return this.errorCode;
}
public String getErrorMsg() {
return this.errorMsg;
}
}
2.2 自定义异常类
自定义异常类可以定义异常返回码与返回内容,方便在处理异常时能够快速理解异常内容,定位异常原因,代码如下:
@Slf4j
public class BaseException extends RuntimeException {
private Object[] args;
private ExceptionEnum exceptionEnum;
public BaseException() {
}
public BaseException(String message, Throwable t) {
super(message, t);
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
public ExceptionEnum getErrorCode() {
return exceptionEnum;
}
public void setErrorCode(ExceptionEnum errorCode) {
this.exceptionEnum = errorCode;
}
public BaseException(ExceptionEnum errorCode, Object... args) {
this.exceptionEnum = errorCode;
this.args = args;
}
public BaseException(ExceptionEnum errorCode, Throwable t, Object... args) {
super(t);
this.exceptionEnum = errorCode;
this.args = args;
}
@Override
public String getMessage() {
String message;
if (exceptionEnum != null) {
message = MessageFormat.format(exceptionEnum.getErrorMsg(), args);
} else {
message = super.getMessage();
}
return message;
}
}
2.3 自定义全局异常处理
SpringBoot 中的全局异常处理主要起作用的两个注解是 @ControllerAdvice 和 @ExceptionHandler ,其中 @ControllerAdvice 是组件注解,添加了这个注解的类能够拦截 Controller 的请求,而 ExceptionHandler 注解可以设置全局处理控制里的异常类型来拦截要处理的异常。@RestControllerAdvice是@ControllerAdvice与@ResponseBody整合,是对@ControllerAdvice的一个增强。通过@RestControllerAdvice和@ExceptionHandler的组合,在业务中就可以减少大量不必要try-catch代码的编写,@RestControllerAdvice会自动进行catch并匹配对应的@ExceptionHandler,然后重新封装异常信息和返回值,统一返回给前端。
/**
* @Author: ChengLiang
* @CreateTime: 2024-04-15 17:25
* @Description: 新增全局异常处理类
* @Version: 1.0
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义的业务异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = BaseException.class)
public ResultData baseExceptionHandler(HttpServletRequest req, BaseException e) {
log.error("发生业务异常!原因是:{}", e.getMessage());
return new ResultData(e.getErrorCode().getCode(), e.getMessage());
}
/**
* 处理空指针的异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = NullPointerException.class)
public ResultData exceptionHandler(HttpServletRequest req, NullPointerException e) {
log.error("发生空指针异常!原因是:", e);
return new ResultData(ExceptionEnum.BODY_NOT_MATCH.getCode(), ExceptionEnum.BODY_NOT_MATCH.getErrorMsg());
}
/**
* 处理其他异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
public ResultData exceptionHandler(HttpServletRequest req, Exception e) {
log.error("未知异常!原因是:", e);
return new ResultData(ExceptionEnum.INTERNAL_SERVER_ERROR.getCode(), ExceptionEnum.INTERNAL_SERVER_ERROR.getErrorMsg());
}
}
2.4 测试结果
业务代码如下:
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public ResultData addUser(User user) {
if (StringUtils.isEmpty(user.getUserName())) {
log.error("userName不能为空!");
throw new BaseException(ExceptionEnum.CODE_8005, "userName");
}
if (user.getAge() <= 0) {
log.error("age不能为负数");
throw new BaseException(ExceptionEnum.CODE_8005, "age");
}
//处理入库
log.info("数据入库成功:{}", JSON.toJSONString(user));
return ResultData.success();
}
@Override
public ResultData queryById(Long id) {
if (id == null || id <= 0) {
log.error("id不能为空!");
throw new BaseException(ExceptionEnum.CODE_8005, "id不能为空");
}
final User user = new User();
user.setAddress("浙江省杭州市拱墅区朝晖路3号");
user.setAge(18);
user.setGender("男");
user.setUserName("马张飞");
return ResultData.success(JSON.toJSONString(user));
}
}
测试结果如下:
2.5 原理分析
@ExceptionHandler结合@ControllerAdvice注解,定义的异常处理器方法可用于@ControllerAdvice注解覆盖的所有控制器方法内所发生的这类异常。单独的@ExceptionHandler在某个控制器类,所定义的异常处理只适用于当前控制器类所有方法所发生的这类异常。@ExceptionHandler结合@ControllerAdvice的处理原理如下图所示:
1.在初始化时,ExceptionHandlerExceptionResolver会被作为一个组合模式HandlerExceptionResolver bean注入到容器中,它实现了HandlerExceptionResolver接口,同时实现了InitializingBean,所以在容器初始化时就会创建对应bean。
@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers);
}
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
在创建ExceptionHandlerExceptionResolver bean过程中,它会查找所有被标注@ControllerAdvice类中使用@ExceptionHandler定义的异常处理控制器方法,供后续处理异常使用。
2.DispatcherServlet初始化时,会搜集所有HandlerExceptionResolver bean并放入对应List中,
public class DispatcherServlet extends FrameworkServlet {
// ......
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
//请求处理的adapter
initHandlerAdapters(context);
// 异常响应处理的resolver
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
// ......
}
/**
* Initialize the HandlerExceptionResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* we default to no exception resolver.
*/
//准备DispatcherServlet处理请求时所使用的HandlerExceptionResolver对象
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
try {
HandlerExceptionResolver her =
context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// Ensure we have at least some HandlerExceptionResolvers, by registering
// default HandlerExceptionResolvers if no other resolvers are found.
if (this.handlerExceptionResolvers == null) {
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
3.异常发生时,DispatcherServlet会遍历handlerExceptionResolvers中每个HandlerExceptionResolver对象试图对该异常进行处理。
/**
* Handle the result of handler selection and handler invocation, which is
* either a ModelAndView or an Exception to be resolved to a ModelAndView.
*/
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
//处理或者返回一个视图(携带异常数据)
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
//......
}
/**
* Determine an error ModelAndView via the registered HandlerExceptionResolvers.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen at the time of the exception
* (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding ModelAndView to forward to
* @throws Exception if no error ModelAndView found
*/
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
//遍历handlerExceptionResolvers中的每个HandlerExceptionResolver对象
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
//......
return exMv;
}
throw ex;
}
在上述代码中,ExceptionHandlerExceptionResolver处理异常时,resolveException方法最终会调用doResolveHandlerMethodException方法进行处理,处理流程如下所示:
(1)先从发生异常控制器方法所在的类查找是否存在使用注解ExceptionHandler并能处理该异常的方法;
(2)若第一步没找到,则从所有@ControllerAdvice注解类中查找使用注解@ExceptionHandler并能处理该异常的方法。
上述调用过程流程流转图如下所示(图来源于参考文献2):
3.小结
1.@ExceptionHandler结合@ControllerAdvice注解能够有效针对同类异常进行处理,返回指定格式至前端,编程友好;
2.在了解上述流程的原理中,需要对spring 的DispatcherServlet部分内容有一定的了解;
3.如果要实现上述的效果,其实还有很多方法,比如自定义拦截器也可以,具体根据业务情况进行使用。
4.参考文献
1.https://docs.spring.io/spring-framework/docs/5.0.0.RELEASE/javadoc-api
2.https://juejin.cn/post/7263123940154523703
3.https://juejin.cn/post/7237369525970616357
5.附录
https://gitee.com/Marinc/nacos/tree/master/exceptionHandler-service