【SpringMVC(十二)】@ControllerAdvice 异常处理 使用 及 原理

32 篇文章 15 订阅

我们在写web应用时,有一个很常见的需求就是:业务层可能会抛出各种各样的异常,我们希望可以在最终接口返回时,根据异常类型自动映射为一个errorCode以及errorMsg,下发给客户端。

具体看下:

以登录为例,我们有一个userService,可以通过客户端的token反解出user信息。如果token不合法,则抛出一个业务异常,终止当前请求:

@Service
public class UserService {

    public Object getUserInfo(String token) {
        if (token == null || token.length() > 3) {
            return "userInfo";
        }
        throw new BizException(TOKEN_NOT_EXIST, "用户token不存在");
    }
}

当然,这里作为例子,就省去了查询token的过程,如果token不为空且长度大于3,就认为其存在。

public class BizException extends RuntimeException {

    private int code;
    private String errorMsg;

    public BizException(int code, String errorMsg) {
        this.code = code;
        this.errorMsg = errorMsg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

义务异常类,所有的义务都抛这个异常,通过code来表明不同的异常类型,可以避免类爆炸。

public class ErrorCode {

    public static final int SUCCESS = 1;
    public static final int TOKEN_NOT_EXIST = 2;

}

code常量类。

最后看下controller:
 

    @RequestMapping("/login")
    public Object login(@RequestParam String token) {
        Map<String, Object> result = new HashMap<>();
        Object userInfo = userService.getUserInfo(token);
        result.put("code", 1);
        result.put("user", userInfo);
        return result;
    }

这里如果不处理异常,那么就会直接将异常信息返回给客户端:

 这样客户端也不知道如何处理,可能导致崩溃,所以还是需要try-catch住,做一层转换。

变成这样:

    @RequestMapping("/login")
    public Object login(@RequestParam String token) {
        Map<String, Object> result = new HashMap<>();
        try {
            Object userInfo = userService.getUserInfo(token);
            result.put("code", 1);
        } catch (BizException e) {
            result.put("code", e.getCode());
            result.put("errorMsg", e.getErrorMsg());
        } catch (Exception e) {
            result.put("code", 999);
            result.put("errorMsg", "serverError");
        }
        return result;
    }

如果用户不存在,捕获BizException,转为对应的errorCode,下发给客户端:

{"code":2,"errorMsg":"用户token不存在"}

这样客户端就可以根据不同的code来做不同的处理,比如说提醒用户登录等等。

 这样看上去已经好了很多,但是,我们会有很多控制器,每一个都在控制器层面try-catch,转为code,这其实是一个重复的过程,而且,过多的try-catch样板代码使得这个类结构很臃肿,代码可读性较差,显然需要寻找更好的办法,可以将这个行为定义到一个抽象的框架层面里,业务控制器就不需要关心这个转换了。

这个办法就是使用今天的主角@ControllerAdvice注解。

这个注解可以用在任意的类上,被这个注解标注的类会被spring扫包扫到,这个类的实例会被作为控制器的增强类,有点类似动态代理的感觉。我们可以在这个类里定义各种控制器的增强行为,比如全局异常处理。

看下例子:
 

@ControllerAdvice(basePackages = "com.liyao.controller")
public class GlobalExceptionHandler {

    @ResponseBody
    @ExceptionHandler(value = BizException.class)
    public Map<String, Object> generalProcess(BizException e) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", e.getCode());
        result.put("errorMsg", e.getErrorMsg());
        return result;
    }
}

这里我们定义了一个全局异常处理增强类,会自动将BizException转为对应的code以及msg,下发给客户端。每一种异常对需要一个@Exceptionhandler注解,如果有多个,会做最近匹配,根据异常类型选出最合适的处理器。

另外,对于json类型的返回值(非modelandview),需要加@Responsebody注解序列化为json,否则会报错。

这时,我们的控制器就不需要try-catch了:

    @RequestMapping("/login")
    public Object login(@RequestParam String token) {
        Map<String, Object> result = new HashMap<>();
        Object userInfo = userService.getUserInfo(token);
        result.put("code", 1);
        result.put("user", userInfo);
        return result;
    }

如果不存在,会自动捕获转为code:

{"code":2,"errorMsg":"用户token不存在"}

是不是解放了很多??

 

下面看下原理,主要分为两步,一是初始化,二是异常处理。

初始化:

在DispatcherServlet的init方法中,调用了一个initHandlerExceptionResolvers方法,来加载全局异常处理器:

protected void initStrategies(ApplicationContext context) {
	initMultipartResolver(context);
	initLocaleResolver(context);
	initThemeResolver(context);
	initHandlerMappings(context);
	initHandlerAdapters(context);
	initHandlerExceptionResolvers(context);
	initRequestToViewNameTranslator(context);
	initViewResolvers(context);
	initFlashMapManager(context);
}

在这个init方法内部,有一段代码是这样的:

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<HandlerExceptionResolver>(matchingBeans.values());
		// We keep HandlerExceptionResolvers in sorted order.
		OrderComparator.sort(this.handlerExceptionResolvers);
	}
}

查找classpath下所有的HandlerExceptionResolver类型的bean,存在DispatcherServlet的一个实例变量中:

/** List of HandlerExceptionResolvers used by this servlet */
private List<HandlerExceptionResolver> handlerExceptionResolvers;

所以说,在springmvc中,全局异常的处理是由HandlerExceptionResolver来完成的。

简单看下这个接口的定义:

public interface HandlerExceptionResolver {

	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

}

和普通的handler比较像,只是多了exception参数。

debug代码看下实际上会有哪些实现类被加载类:

我们用到的一般是是第一个(debug代码发现的),这里我们重点看下ExceptionHandlerExceptionResolver类:

这个类实例化时,这个方法会被spring回调:

@Override
public void afterPropertiesSet() {
	// Do this first, it may add ResponseBodyAdvice beans
	initExceptionHandlerAdviceCache();

	if (this.argumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	if (this.returnValueHandlers == null) {
		List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
		this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
	}
}

第一步就是初始化一些Advice,后面两步比较熟悉,加载argumentResolver和returnValueHandler,与普通的控制器一样。

看下Advice相关的:

List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
Collections.sort(adviceBeans, new OrderComparator());

for (ControllerAdviceBean adviceBean : adviceBeans) {
	ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
	if (resolver.hasExceptionMappings()) {
		this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
		logger.info("Detected @ExceptionHandler methods in " + adviceBean);
	}
	if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {
		this.responseBodyAdvice.add(adviceBean);
		logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
	}
}

public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext applicationContext) {
List<ControllerAdviceBean> beans = new ArrayList<ControllerAdviceBean>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class)) {
	if (applicationContext.findAnnotationOnBean(name, ControllerAdvice.class) != null) {
		beans.add(new ControllerAdviceBean(name, applicationContext));
	}
}
return beans;
}

核心代码如上,会将容器中所有的被注解ControllerAdvice标注的bean拿出来,然后构建一个ExceptionHandlerMethodResolver类型的实例,这个ExceptionHandlerMethodResolver类的作用就是处理一个Advice类中各种ExceptionHandler注解。会将解析后的adviceBean与resovler放到一个map的cache中。这里补充一点,为什么可以扫描到ControllerAdvice注解的bean,是因为该注解默认被@Component注解标记了,所以会被扫包:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {

好,至此,ControllerAdvice以及内部的ExceptionHandler已经被加载完毕。

 

再看下处理过程:

入口还是在DispatcherServlet中的doDispatch方法中:

try {
	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
	mv = ha.handle(processedRequest, response, 
} catch (Exception ex) {
	dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

可以看到,整个doDispatch方法其实是放在一个大的try-catch块中的,所以我们编写的controller的各种异常才能在这里被捕捉到,并得到统一的处理。catch块所做的事情就是记录异常,然后有异常和无异常的处理都会在后面的processDispatcheResult方法中执行:


		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);
			}
		}

这个方法中,如果exception不为空,那么就调用processHandlerException方法处理异常:

for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
	exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
	if (exMv != null) {
		break;
	}
}

这里是不是比较熟悉?调用了之前加载的handlerExceptionResolver链来依次处理。通过debug可以看到,是被ExceptionHandlerExceptionResolver来处理的:

	@Override
	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {

		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
		if (exceptionHandlerMethod == null) {
			return null;
		}

		exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
		exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);

		ServletWebRequest webRequest = new ServletWebRequest(request, response);
		ModelAndViewContainer mavContainer = new ModelAndViewContainer();

		try {
			if (logger.isDebugEnabled()) {
				logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
			}
			exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
		}
		catch (Exception invocationEx) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
			}
			return null;
		}

		if (mavContainer.isRequestHandled()) {
			return new ModelAndView();
		}
		else {
			ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel());
			mav.setViewName(mavContainer.getViewName());
			if (!mavContainer.isViewReference()) {
				mav.setView((View) mavContainer.getView());
			}
			return mav;
		}
	}

首先第一步就是查找合适的exceptionHandler:

protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
		Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);

		if (handlerMethod != null) {
			ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
			if (resolver == null) {
				resolver = new ExceptionHandlerMethodResolver(handlerType);
				this.exceptionHandlerCache.put(handlerType, resolver);
			}
			Method method = resolver.resolveMethod(exception);
			if (method != null) {
				return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
			}
		}

		for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
			if (entry.getKey().isApplicableToBeanType(handlerType)) {
				ExceptionHandlerMethodResolver resolver = entry.getValue();
				Method method = resolver.resolveMethod(exception);
				if (method != null) {
					return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
				}
			}
		}

		return null;
	}

这里会从之前的advice缓存cache中根据异常类型查找出最合适的handler,里面的匹配规则和普通的handler路径匹配类似,先是直接匹配然后是最优匹配。

匹配到以后,就将handler配置上argumentResolver和returnValueHandler,来处理入参和返回值,因为异常处理的返回值也可能被@Responsebody之类的注解标注,所以需要处理。

整个处理流程和普通的handler几乎一模一样。

所以整个springmvc的dispatcherServlet处理流程是放在一个大的try-catch块中的,有异常和无异常的处理几乎类似,都是匹配出对应的handler,设置argumentResolver和returnValueHandler,然后invoke。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值