試想一下我們一般怎麼統一處理異常呢,答:切面。但拋開切面不講,如果對每一個controller方法丟擲的異常做專門處理,那麼著實太費勁了,有沒有更好的方法呢?當然有,就是本篇文章接下來要介紹的springmvc的例外處理機制,用到了ControllerAdvice和ExceptionHandler註解,有點切面的感覺哈哈。
首先從springmvc的例外處理解析器開始講,當執行完controller方法後,不管有沒有異常產生都會呼叫DispatcherServlet#doDispatch()
方法中的processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
方法,接著會判斷是否有異常,若無異常則走正常流程,若有異常則需要進行處理 mv = processHandlerException(request, response, handler, exception);
再接著就是遍歷spring已經註冊的例外處理解析器直到有處理器返回mav
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
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);
}
}
// Did the handler return a view to render? 處理程式是否返回要渲染的檢視
if (mv != null && !mv.wasCleared()) {
// 渲染檢視
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
}
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
// 無檢視view
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
其中最重要也是最常使用的一個處理器就是ExceptionHandlerExceptionResolver,下面將著重介紹它,先來看看這個類的繼承結構圖,實現了InitializingBean介面,在這個bean建立完成之前會呼叫生命週期初始化方法afterPropertiesSet()
,這裡麵包含了對@ControllerAdvice
註解的解析,初始化完後的資訊供後續解析異常使用。
實現HandlerExceptionResolver
介面,實現解析方法resolveException()
public interface HandlerExceptionResolver {
/**
* Try to resolve the given exception that got thrown during handler execution,
* returning a {@link ModelAndView} that represents a specific error page if appropriate.
* <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
* to indicate that the exception has been resolved successfully but that no view
* should be rendered, for instance by setting a status code.
* @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 {@code ModelAndView} to forward to,
* or {@code null} for default processing in the resolution chain
*/
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
// 初始化異常註解 @ControllerAdvice
initExceptionHandlerAdviceCache();
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for exception mappings: " + getApplicationContext());
}
// 解析有@ControllerAdvice註解的bean,並將這個bean構建成ControllerAdviceBean物件
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 將ControllerAdviceBean根據order排序
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
// mappedMethods 對映不為空
if (resolver.hasExceptionMappings()) {
// 新增到快取中
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
if (logger.isInfoEnabled()) {
logger.info("Detected @ExceptionHandler methods in " + adviceBean);
}
}
// 若實現了ResponseBodyAdvice介面(暫不介紹)
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
if (logger.isInfoEnabled()) {
logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
}
}
}
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
這行程式碼會解析擁有@ControllerAdvice
註解的class,並且會遍歷class中帶有 @ExceptionHandler
註解的方法,獲取方法註解帶有的異常型別,將異常型別和方法放入到mappedMethods
中供後面獲取,獲取的時候若對應處理此異常型別的method有多個,則需要進行排序,選取一個異常型別與method ExceptionHandler註解異常型別最近的一個(深度最小的那個也即是繼承關係最少的那個)具體程式碼如下:
public class ExceptionHandlerMethodResolver {
/**
* A filter for selecting {@code @ExceptionHandler} methods.
*/
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
(AnnotationUtils.findAnnotation(method, ExceptionHandler.class) != null);
/**
* 異常型別與方法的對映map
*/
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
/**
* 快取,用來儲存先前碰到過的異常型別與處理方法的對映
*/
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
/**
* A constructor that finds {@link ExceptionHandler} methods in the given type.
* @param handlerType the type to introspect
*/
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
// 獲取並遍歷@ExceptionHandler註解的方法
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
/**
* Extract exception mappings from the {@code @ExceptionHandler} annotation first,
* and then as a fallback from the method signature itself.
*/
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<>();
// 將註解ExceptionHandler value值異常新增到result中
detectAnnotationExceptionMappings(method, result);
// 註解值為空的話再去獲取引數的異常型別
if (result.isEmpty()) {
for (Class<?> paramType : method.getParameterTypes()) {
if (Throwable.class.isAssignableFrom(paramType)) {
result.add((Class<? extends Throwable>) paramType);
}
}
}
if (result.isEmpty()) {
throw new IllegalStateException("No exception types mapped to " + method);
}
return result;
}
protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
ExceptionHandler ann = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
Assert.state(ann != null, "No ExceptionHandler annotation");
result.addAll(Arrays.asList(ann.value()));
}
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
// 將異常型別以及對應的method新增到map中,且異常型別不能有重複否則會報錯
Method oldMethod = this.mappedMethods.put(exceptionType, method);
if (oldMethod != null && !oldMethod.equals(method)) {
throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" +
exceptionType + "]: {" + oldMethod + ", " + method + "}");
}
}
/**
* Whether the contained type has any exception mappings.
*/
public boolean hasExceptionMappings() {
return !this.mappedMethods.isEmpty();
}
/**
* Find a {@link Method} to handle the given exception.
* Use {@link ExceptionDepthComparator} if more than one match is found.
* @param exception the exception
* @return a Method to handle the exception, or {@code null} if none found
*/
@Nullable
public Method resolveMethod(Exception exception) {
return resolveMethodByThrowable(exception);
}
/**
* Find a {@link Method} to handle the given Throwable.
* Use {@link ExceptionDepthComparator} if more than one match is found.
* @param exception the exception
* @return a Method to handle the exception, or {@code null} if none found
* @since 5.0
*/
@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = resolveMethodByExceptionType(cause.getClass());
}
}
return method;
}
/**
* Find a {@link Method} to handle the given exception type. This can be
* useful if an {@link Exception} instance is not available (e.g. for tools).
* @param exceptionType the exception type
* @return a Method to handle the exception, or {@code null} if none found
*/
@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, method);
}
return method;
}
/**
* Return the {@link Method} mapped to the given exception type, or {@code null} if none.
*/
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
// exceptionType 到matchs父類別異常型別的深度
matches.sort(new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
}
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// exception為controller方法丟擲的異常
// 根據異常及其型別從上述的mappedMethods中獲取對應的方法,再獲取方法所在的物件 封裝成ServletInvocableHandlerMethod
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
// 設定引數解析器,主要用來獲取方法的引數值的,供後續反射呼叫方法
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
// 設定返回值解析器,當執行完方法後獲取返回值,對返回值進行處理 或返回檢視或將結果寫入到response
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
Throwable cause = exception.getCause();
if (cause != null) {
// Expose cause as provided argument as well
// 執行例外處理方法,也就是我們的自定義的例外處理方法
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
}
else {
// Otherwise, just the given exception as-is
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
// Any other than the original exception is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (invocationEx != exception && logger.isWarnEnabled()) {
logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
}
// Continue with default processing of the original exception...
return null;
}
// 根據後續的返回值解析器設定的,將返回值寫入到response中了直接返回空的mav
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
// (this.view instanceof String)
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
此方法執行完成後已經完成了例外處理方法的呼叫,若方法返回值為檢視ModelAndView或其他檢視型別,則還需要藉助檢視解析器如InternalResourceViewResolver
對檢視進行解析渲染,若為其他型別的值則將值寫入到response響應中。
Controller類方法:
@Controller
@RequestMapping(value = "test")
public class HelloWorldController{
@Data
public static class User {
private String username;
private Integer age;
private String address;
}
@RequestMapping(value = "user/get", method = RequestMethod.POST)
@ResponseBody
public Object testObject(@RequestBody @Valid User user, @RequestParam String address) {
user.setAddress(address);
// 這裡特意丟擲RuntimeException異常
throw new RuntimeException("this is a exception");
}
}
ExceptionHandlerController例外處理類
@ControllerAdvice
@ResponseBody
public class ExceptionHandlerController {
@ExceptionHandler(value = Exception.class)
public Object handleException(Exception e) {
return CommonResult.fail("Exception:" + e.getMessage());
}
@ExceptionHandler(value = RuntimeException.class)
public Object handlerRuntimeException(Exception e) {
return CommonResult.fail("handlerRuntimeException:" + e.getMessage());
}
}
ExceptionHandlerController類中定義了兩個例外處理方法,一個處理Exception異常,一個處理RuntimeException異常,那個根據controller方法丟擲的異常RuntimeException再結合上面的分析(RuntimeException到RuntimeException深度為0,RuntimeException到Exception中間繼承了一次深度為1)可以得出丟擲異常型別的處理方法為handlerRuntimeException
方法。 執行程式結果如下:
初步解析ExceptionHandlerExceptionResolver原始碼,若寫的有誤或者有不理解的地方,歡迎指出討論~