springmvc例外處理解析#ExceptionHandlerExceptionResolver

2022-07-27 21:01:52

開頭

試想一下我們一般怎麼統一處理異常呢,答:切面。但拋開切面不講,如果對每一個controller方法丟擲的異常做專門處理,那麼著實太費勁了,有沒有更好的方法呢?當然有,就是本篇文章接下來要介紹的springmvc的例外處理機制,用到了ControllerAdvice和ExceptionHandler註解,有點切面的感覺哈哈。

 

1.ExceptionHandlerExceptionResolver

首先從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註解異常型別最近的一個(深度最小的那個也即是繼承關係最少的那個)具體程式碼如下:

ExceptionHandlerMethodResolver
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響應中。

 

2. demo

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原始碼,若寫的有誤或者有不理解的地方,歡迎指出討論~