Spring MVC ControllerAdvice深入解析
Spring 在3.2版本後面增加了一個ControllerAdvice註解。網上的資料說的都是ControllerAdvice配合ExceptionHandler註解可以統一處理異常。而Spring MVC是如何做到的資料卻比較少,下面會先給出使用的例子和踩過的一個坑。然後進行相應的原始碼分析,之後再介始ControllerAdvice另外的兩種使用方式。
ControllerAdvice的簡單使用
- ControllerAdvice配合ExceptionHandler可以統一處理系統的異常,我們先定義一個ExceptionAdvice類用於處理系統的兩種型別的異常。程式碼如下:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.pptv.frame.dto.common.ResponseDTO;
import com.pptv.frame.dto.common.ServiceCodeEnum;
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({
ArrayIndexOutOfBoundsException.class
})
@ResponseBody
public ResponseDTO handleArrayIndexOutOfBoundsException(ArrayIndexOutOfBoundsException e) {
// TODO 記錄log日誌
e.printStackTrace();
ResponseDTO responseDTO = new ResponseDTO();
responseDTO.wrapResponse(ServiceCodeEnum.E999998, "陣列越界異常");
return responseDTO;
}
@ExceptionHandler({
Exception.class
})
@ResponseBody
public ResponseDTO handleException(Exception e) {
// TODO 記錄log日誌
e.printStackTrace();
ResponseDTO responseDTO = new ResponseDTO();
responseDTO.wrapResponse(ServiceCodeEnum.E999998, "未知異常");
return responseDTO;
}
}
- Spring mvc 的配置如下(這裡用到了mvc:annotation-driven):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cxf="http://cxf.apache.org/core"
xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan
base-package="frame.web.controller;frame.web.advice" />
<!--===================== view resovler ===================== -->
<bean id="jstlViewResolver"
class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="order" value="1" />
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
</bean>
<!-- 配置Fastjson支援 -->
<mvc:annotation-driven conversion-service="conversionService">
<mvc:message-converters register-defaults="true">
<bean
class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes" value="text/html;charset=UTF-8" />
<property name="features">
<array>
<value>WriteMapNullValue</value>
<value>WriteNullStringAsEmpty</value>
</array>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 自定義引數轉換 -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
</bean>
</beans>
遇到的一個坑是當spring mvc配置檔案不用<mvc:annotation-drive>這個標籤而是手動將RequestMappingHandlerMapping與RequestMappingHandlerAdapter這兩個類讓spring容器管理,上面的ControllerAdvice將不起作用
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="webBindingInitializer">
<bean
class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
<property name="conversionService" ref="conversionService" />
</bean>
</property>
<property name="messageConverters">
<list>
<ref bean="mappingJackson2HttpMessageConverter" />
<ref bean="stringHttpMessageConverter" />
</list>
</property>
</bean>
Spring MVC是如何處理異常的
下面來看看Spring MVC是如何處理異常的,為什麼我手動配置了RequestMappingHandlerMapping和RequestMappingHandlerAdapter ControllerAdvice就不會對異常進行攔截呢而通過<mvc:annotation-drive>這個標籤就可以呢?我們從Spring MVC的入口看一下異常是如何處理的。下面是關鍵程式碼(關鍵程式碼都有相應的註釋):
public class DispatcherServlet extends FrameworkServlet {
/**
*這個方法是Spring MVC的入口方法,可以看到Spring MVC人具體處理流程
**/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
//這裡有個try,下面的catch就是用於處理異常的
try {
//檢查是否是上傳檔案的請求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
//根據請求的request得到HandlerExecutionChain 物件,裡面有Inceptor和相應的Controller
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
//根據配置的HandlerAdapter 對handler進行適配
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
//這裡會呼叫具體的Handler也就是我們寫的Controller
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
//上面處理的邏輯有任何的異常,都將會落到這裡,用dispatchException 這個變數接住異常引用
dispatchException = ex;
}
catch (Throwable err) {
//如果拋的是error, 這裡也會把異常給接住
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//具體處理異常的邏輯看來是在這個方法裡了,具體的邏輯看下面的原始碼
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//這是最外面的try,這裡需要處理Inteceptor裡After的邏輯
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//這是最外面的try,這裡需要處理Inteceptor裡After的邏輯
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
/**
*這個方法裡processHandlerException用於處理各種不同的Exception
**/
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
boolean errorView = false;
//當controller丟擲異常後,就會執行下面的邏輯啦
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
//各種不同的異常會走到這裡來處理,processHandlerException的原始碼在下面有詳細的註釋
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");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
/**
**這裡是用於處理Spring MVC異常的入口
**/
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// Check registered HandlerExceptionResolvers...
//通過注入的handlerExceptionResolvers來處得具體的Exception,這也就找到了我上面踩坑的原因了。
ModelAndView exMv = null;
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
if (exMv != null) {
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()) {
exMv.setViewName(getDefaultViewName(request));
}
if (logger.isDebugEnabled()) {
logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
}
通過上面的原始碼,我們一步步可以跟蹤到processHandlerException這個方法,這個方法裡通過HandlerExceptionResolver 來處理具體的異常,而當我們手動只配置RequestMappingHandlerMapping和RequestMappingHandlerAdapter時,並沒有配置任何的HandlerExceptionResolver 。也就是為什麼ControllerAdvice不會對異常進行處理了,我們同時也可以想到<mvc:annotation-drive>一定是幫助我們注入了一個HandlerExceptionResolver 類。下面我們通過分析AnnotationDrivenBeanDefinitionParser這個類來看看到底給我們注入的是那個HandlerExceptionResolver,AnnotationDrivenBeanDefinitionParser類就是用於解析<mvc:annotation-drive>標籤的。下面是AnnotationDrivenBeanDefinitionParser的部分原始碼:
package org.springframework.web.servlet.config;
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
/**
*parse是這個類的核心方法,它用於解析 annotation-drive標籤裡的內容,根據標籤裡的內容往spring ioc容器裡注入具體的物件。
**/
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
XmlReaderContext readerContext = parserContext.getReaderContext();
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
parserContext.pushContainingComponent(compDefinition);
RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);
//這裡有我們熟悉的RequestMappingHandlerMapping,
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("order", 0);
handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
if (element.hasAttribute("enable-matrix-variables")) {
Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enable-matrix-variables"));
handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
}
else if (element.hasAttribute("enableMatrixVariables")) {
Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enableMatrixVariables"));
handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
}
configurePathMatchingProperties(handlerMappingDef, element, parserContext);
readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef);
RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source);
handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
//這裡會注入具體的ConversionService用於將json,xml轉成Spring mvc裡的請求和返回物件
RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);
RuntimeBeanReference validator = getValidator(element, source, parserContext);
RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element);
RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class);
bindingDef.setSource(source);
bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
bindingDef.getPropertyValues().add("conversionService", conversionService);
bindingDef.getPropertyValues().add("validator", validator);
bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver);
ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext);
ManagedList<?> argumentResolvers = getArgumentResolvers(element, parserContext);
ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, parserContext);
String asyncTimeout = getAsyncTimeout(element);
RuntimeBeanReference asyncExecutor = getAsyncExecutor(element);
ManagedList<?> callableInterceptors = getCallableInterceptors(element, source, parserContext);
ManagedList<?> deferredResultInterceptors = getDeferredResultInterceptors(element, source, parserContext);
//RequestMappingHandlerAdapter也會在這裡注入
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.setSource(source);
handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
addRequestBodyAdvice(handlerAdapterDef);
addResponseBodyAdvice(handlerAdapterDef);
if (element.hasAttribute("ignore-default-model-on-redirect")) {
Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignore-default-model-on-redirect"));
handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
}
else if (element.hasAttribute("ignoreDefaultModelOnRedirect")) {
// "ignoreDefaultModelOnRedirect" spelling is deprecated
Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute("ignoreDefaultModelOnRedirect"));
handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
}
if (argumentResolvers != null) {
handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
}
if (returnValueHandlers != null) {
handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
}
if (asyncTimeout != null) {
handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout);
}
if (asyncExecutor != null) {
handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor);
}
handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors);
handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors);
readerContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME , handlerAdapterDef);
String uriCompContribName = MvcUriComponentsBuilder.MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME;
RootBeanDefinition uriCompContribDef = new RootBeanDefinition(CompositeUriComponentsContributorFactoryBean.class);
uriCompContribDef.setSource(source);
uriCompContribDef.getPropertyValues().addPropertyValue("handlerAdapter", handlerAdapterDef);
uriCompContribDef.getPropertyValues().addPropertyValue("conversionService", conversionService);
readerContext.getRegistry().registerBeanDefinition(uriCompContribName, uriCompContribDef);
RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);
csInterceptorDef.setSource(source);
csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService);
RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class);
mappedCsInterceptorDef.setSource(source);
mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null);
mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef);
String mappedInterceptorName = readerContext.registerWithGeneratedName(mappedCsInterceptorDef);
//這裡有我們需要找的ExceptionHandlerExceptionResolver,
RootBeanDefinition exceptionHandlerExceptionResolver = new RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
exceptionHandlerExceptionResolver.setSource(source);
exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);
exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0);
addResponseBodyAdvice(exceptionHandlerExceptionResolver);
if (argumentResolvers != null) {
exceptionHandlerExceptionResolver.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
}
if (returnValueHandlers != null) {
exceptionHandlerExceptionResolver.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
}
String methodExceptionResolverName = readerContext.registerWithGeneratedName(exceptionHandlerExceptionResolver);
RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class);
responseStatusExceptionResolver.setSource(source);
responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
responseStatusExceptionResolver.getPropertyValues().add("order", 1);
String responseStatusExceptionResolverName =
readerContext.registerWithGeneratedName(responseStatusExceptionResolver);
RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class);
defaultExceptionResolver.setSource(source);
defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
defaultExceptionResolver.getPropertyValues().add("order", 2);
String defaultExceptionResolverName =
readerContext.registerWithGeneratedName(defaultExceptionResolver);
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME));
parserContext.registerComponent(new BeanComponentDefinition(uriCompContribDef, uriCompContribName));
parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName));
// Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
MvcNamespaceUtils.registerDefaultComponents(parserContext, source);
parserContext.popAndRegisterContainingComponent();
return null;
}
}
通過上面程式碼的分析, 我們可以找到ExceptionHandlerExceptionResolver這個類來用於處理Spring MVC的各種異常,那ExceptionHandlerExceptionResolver具體又是如何跟ControllerAdvice配合使用來處理各種異常的呢?我們來看看ExceptionHandlerExceptionResolver裡的關鍵程式碼:
package org.springframework.web.servlet.mvc.method.annotation;
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
//這裡有個map用於儲存ControllerAdviceBean
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>();
//這個方法是由spring 容器呼叫的
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
//這個方法裡會處理ExceptionHandler
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);
}
}
/**
*這個方法裡會在spring ioc容器裡找出標註了@ControllerAdvice的類,如果有方法標註了@ExceptionHandler會生成一個ExceptionHandlerMethodResolver類用於處理異常並放到exceptionHandlerAdviceCache這個map快取類裡。
**/
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for exception mappings: " + getApplicationContext());
}
//這裡會找到容器裡標註了ControllerAdvice標籤的類
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
//這個構造方法裡會檢查ControllerAdvice類裡是否有@ExceptionHandler標註的方法,在ExceptionHandlerMethodResolver 有個異常的map。
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
//如果有@ExceptionHandler方法,會執行下面的邏輯
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
if (logger.isInfoEnabled()) {
logger.info("Detected @ExceptionHandler methods in " + adviceBean);
}
}
if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {
this.responseBodyAdvice.add(adviceBean);
if (logger.isInfoEnabled()) {
logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
}
}
}
}
/**
** 這個方法會根據exceptionHandlerAdviceCache這個找到具體需要處理異常的方法
*/
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;
}
}
ExceptionHandlerExceptionResolver這個類首先會掃描容器裡所有的ControllerAdvice,如果ControllerAdvice標註了@ExceptionHandler會加到一個map快取裡。在處理具體的異常的時候,會去這個快取裡一個個找是否有ControllerAdvice能夠處理這個異常。整個流程我們就分析到這裡,下面看看ControllerAdvice的另外兩個用法。
RequestBodyAdvice與ResponseBodyAdvice
Spring在4.2的版本給我們提供了RequestBodyAdvice與ResponseBodyAdvice這兩個介面,而ControllerAdvice是在3.2這個版本里的。那RequestBodyAdvice和ResponseBodyAdvice能夠幫我們做些什麼事性呢?假如現在有個需求,正常介面返回的是json,但傳入的請求頭裡有callback引數需要返回jsonp格式的資料需要如何做呢?下面我們來看看RequestBodyAdvice和ResponseBodyAdvice這兩個類的具體定義,RequestBodyAdvice程式碼如下:
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
public interface RequestBodyAdvice {
//supports方法用於決定是否呼叫下面的方法,
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
//處理空引數據情況
Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
//在引數讀取之前處理的邏輯
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
//在引數讀取之後處理的邏輯
Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
從RequestBodyAdvice的定義我們可以清楚的看出他主要用於處理Spring MVC請求引數相關的邏輯,首先定義了support方法用於判斷是否能夠對請求引數做進一步的處理,然後定義了在讀取引數前後方法分別用於處理請求引數。這裡的讀取前後是在Spring MVC呼叫了HttpMessageConverter對引數進行了轉義,所以使用起來還是很方便的。下面來看看ResponseBodyAdvice的定義:
package org.springframework.web.servlet.mvc.method.annotation;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
public interface ResponseBodyAdvice<T> {
//這個方法用於判斷是否需要呼叫beforeBodyWrite方法
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
//這裡在寫入的時候就可以修改要寫入的值啦
T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
ResponseBodyAdvice用於對寫入的資料進行修改,通過ResponseBodyAdvice我們可以很方便的將json資料改成jsonp進行返回。下面我自定義了一個JsonpAdvice用於處理根據header引數返回jsonp格式的資料。程式碼如下:
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice;
/**
* 處理需要返回jsonp的Advice 功能描述:
*
* @version 2.0.0
* @author zhiminchen
*/
@ControllerAdvice
public class JsonpAdvice extends AbstractMappingJacksonResponseBodyAdvice {
@Override
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
MediaType contentType,
MethodParameter returnType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
// 根據 header是否有callback引數決定是否返回jsonp格式的資料
String callback = servletRequest.getHeader("callback");
if (StringUtils.isNotBlank(callback)) {
MediaType contentTypeToUse = getContentType(contentType, request, response);
response.getHeaders().setContentType(contentTypeToUse);
bodyContainer.setJsonpFunction(callback);
}
}
protected MediaType getContentType(MediaType contentType,
ServerHttpRequest request,
ServerHttpResponse response) {
return new MediaType("application", "javascript");
}
}
如果採用的是FastJsonHttpMessageConverter作為型別轉換器。上面的JsonpAdvice 不起作用, 我們可以再自定義一個爭對FastJsonHttpMessageConverter的Jsonp攔截器,程式碼如下:
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.alibaba.fastjson.JSONPObject;
import com.pptv.frame.dto.common.ResponseDTO;
/**
* 處理需要返回jsonp的Advice 功能描述:
*
* @version 2.0.0
* @author zhiminchen
*/
@ControllerAdvice
public class JsonpAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType,
Class converterType) {
return returnType.getMethod().getReturnType().equals(ResponseDTO.class);
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String callback = servletRequest.getHeader("callback");
if (StringUtils.isNotBlank(callback)) {
MediaType contentTypeToUse = getContentType(request, response);
response.getHeaders().setContentType(contentTypeToUse);
JSONPObject jsonpObject = new JSONPObject(callback);
jsonpObject.addParameter(body);
return jsonpObject;
} else {
return body;
}
}
protected MediaType getContentType(ServerHttpRequest request,
ServerHttpResponse response) {
return new MediaType("application", "javascript");
}
}
總結:
- Spring MVC通過@ControllerAdvice配合@ExceptionHandler能夠統一處理系統的異常資訊。
- ControllerAdvice配合RequestBodyAdvice與ResponseBodyAdvice可以方便的對請求引數與返回值進行修改。
相關文章
- 深入淺出Spring MVCSpringMVC
- Spring MVC 解析之 DispatcherServletSpringMVCServlet
- 深入理解:Spring MVC工作原理SpringMVC
- Spring原始碼深度解析(郝佳)-學習-原始碼解析-Spring MVCSpring原始碼MVC
- Spring @ControllerAdvice+@ExceptionHandler統一異常處理SpringControllerException
- spring - mvcSpringMVC
- spring、spring MVC、spring BootMVCSpring Boot
- Spring MVC原始碼(三) ----- @RequestBody和@ResponseBody原理解析SpringMVC原始碼
- 深入理解MVCMVC
- 深入淺出——MVCMVC
- 深入理解Spring系列之十三:IntrospectorCleanupListener解析SpringROS
- spring mvc interceptorsSpringMVC
- spring - mvc - @ScheduledSpringMVC
- spring - mvc - @ValidSpringMVC
- Spring系列(一):Spring MVC bean 解析、註冊、例項化流程原始碼剖析SpringMVCBean原始碼
- 深入解析ASP.NET Core MVC的模組化設計[下篇]ASP.NETMVC
- Spring進階之@ControllerAdvice與統一異常處理SpringController
- Spring Boot + Mybatis + Spring MVC環境配置(四):MVC框架搭建Spring BootMyBatisMVC框架
- 深入Spring官網系列(十八):AOP詳細解析!Spring
- 深入Spring官網系列(十四):BeanWrapper詳細解析SpringBeanAPP
- Spring MVC學習SpringMVC
- Spring5 MVCSpringMVC
- Spring MVC基礎SpringMVC
- Spring MVC1SpringMVC
- Spring MVC 簡述SpringMVC
- Spring MVC應用SpringMVC
- tomcat + spring mvc原理外傳:spring mvc與前端的糾葛TomcatSpringMVC前端
- Spring MVC原始碼(四) ----- 統一異常處理原理解析SpringMVC原始碼
- Spring MVC 入門程式SpringMVC
- Spring MVC for beginners 筆記SpringMVC筆記
- spring mvc 快速入門SpringMVC
- Spring Mvc原理分析(一)SpringMVC
- Spring MVC 面試題SpringMVC面試題
- Spring 系列(二):Spring MVC的父子容器SpringMVC
- Spring 學習筆記(3)Spring MVCSpring筆記MVC
- 深入解析 Spring 配置檔案:從基礎到高階Spring
- MVC設計模式深入理解MVC設計模式
- Spring MVC 到 Spring Boot 的簡化之路MVCSpring Boot