SpingMVC 子產品
簡介
Spring MVC是一種基于MVC架構模式的輕量級Web架構。
SpringMVC處理過程
Spring MVC的處理過程:
-
接收使用者的請求DispatcherServlet
- 找到用于處理request的 handler 和Interceptors,構造成 HandlerExecutionChain執行鍊
- 找到 handler 相對應的 HandlerAdapter
- 執行所有注冊攔截器的preHandler方法
- 調用 HandlerAdapter 的 handle() 方法處理請求,傳回 ModelAndView
- 倒序執行所有注冊攔截器的postHandler方法
- 請求視圖解析和視圖渲染
處理流程中各個元件的功能:
- 前端控制器(DispatcherServlet):接收使用者請求,給使用者傳回結果。
- 處理器映射器(HandlerMapping):根據請求的url路徑,通過注解或xml配置,尋找比對的Handler。
- 處理器擴充卡(HandlerAdapter):Handler的擴充卡,調用 handler 的方法處理請求。
- 處理器(Handler):執行相關的請求處理邏輯,并傳回響應的資料和視圖資訊,将其封裝到ModelAndView對象中。
- 視圖解析器(ViewResolver):将邏輯視圖名解析成真正的視圖View。
- 視圖(View):接口類,實作類可支援不同的View類型(JSP、FreeMarker、Excel等)。
SpringMVC web.xml配置:
<!-- 配置applicationContext.xml
如果不寫任何參數,預設位置在WEB-INF下,且預設名字為applicationContext.xml
如果想自定義檔案名,需要在web.xml中加入contextConfigLocation這個context參數
<context-param>:該元素用來聲明應用範圍(整個WEB項目)内的上下文初始化參數。
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 預設位置在WEB-INF下 預設名字為:springDispacherServlet-servlet -->
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!--監聽器
作用:配置檔案加載監聽器;
ContextLoaderListener:作用是啟動web容器,自動裝配applicationContext.xml配置檔案
-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--前端控制器-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定SpringMVC配置檔案 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-conf.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--處理請求編碼 該過濾器一定要配置在其他過濾器之前-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--開啟Spring支援REST風格-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
前端控制器:
前端控制器(DispatcherServlet)類最核心的方法是 doDispatch()
在web.xml中配置了名為 dispatcher 的Servlet,攔截所有的請求。當Web應用接收到這種請求時,會調用前端控制器。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//檢測request是否包含多媒體類型(File檔案),并将request轉化為processedRequest
processedRequest = this.checkMultipart(request);
//判斷processedRequest是否為原始的request
multipartRequestParsed = processedRequest != request;
//确定目前請求的處理程式(處理器)。(就是判斷目前請求可以被哪個Controller處理)
mappedHandler = this.getHandler(processedRequest);
//如果沒有找到哪個處理器(控制器)能處理目前請求就會報404或者抛出異常
if (mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//确定目前請求的處理程式(處理器)的擴充卡。
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
//如果處理程式支援,則處理最後修改的标頭。
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.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;
}
//實際上調用處理程式。(擴充卡來執行目标方法,将目标方法執行完的傳回值作為視圖名,儲存到ModelAndView中。是以說,無論目标方法怎麼寫,擴充卡都會将最終傳回值封裝成ModelAndView )
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//使用預設視圖名
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var19) {
dispatchException = var19;
}
//處理ModelAndView,包含render()方法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception var20) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var20);
} catch (Error var21) {
this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var21);
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
詳解:
- 所有請求過來
攔截請求DispatcherServlet
- 調用doDispatch() 方法進行處理
- getHandler():根據目前請求位址找到能處理這個請求的目标處理器類
- 根據目前請求在HandlerMapping中找到目前request的映射資訊,擷取到handler(目标處理器類)
- getHandlerAdapter():根據目前處理器類擷取到能執行這個處理器方法的擴充卡
- 根據目前處理器類找到目前的HandlerAdapter(擴充卡)
- 使用剛才擷取到的擴充卡(AnnotationMethodHandlerAdapter)執行目标方法
- 目标方法執行後會傳回一個ModelAndView對象
- 根據ModelAndView的資訊轉發到具體的頁面,并可以在請求域中取出ModelAndView中的模型資料
- getHandler():根據目前請求位址找到能處理這個請求的目标處理器類
那麼getHandler()是如何根據目前請求就能找到哪個類能來處理目前請求?
首先,該方法回返目标處理器的執行鍊 HandlerExecutionChain 。然後周遊 handlerMappings(處理器映射),它裡面儲存了每一個處理器能處理哪些請求的映射資訊。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
**getHandlerAdapter()又是怎麼找到目标處理器類的擴充卡的呢?**與上邊一樣,周遊所有 handlerAdapters ,如果目前周遊的 adapter 支援目前處理器就傳回該擴充卡(擴充卡本質上就是反射工具,要拿擴充卡才去執行目标方法)。其實是由HandlerExecutionChain HandlerMapping.getHandler(HttpServletRequest)方法擷取到的。從這裡可以看出,HandlerMapping的作用主要是根據目前request請求擷取能夠處理目前request的handler,而HandlerAdapter的作用在于将request中的各個屬性适配為handler能夠處理的形式。
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
關于HandlerAdapter.supports()方法,有這個方法的主要原因是HandlerMapping是可以有多中實作的,Spring會周遊這些具體的實作類,判斷哪一個能夠根據目前request産生一個handler。對HandlerAdapter而言,其實不知道目前擷取到的handler具體是什麼形式的,不同的HandlerMapping産生的handler形式是不一樣的,比如,RequestMappingHandlerMapping産生的handler則是封裝在HandlerMethod對象中的,因而這裡HandlerAdapter需要一個方法能夠快速過濾掉目前産生的handler是否為其能夠進行适配的,這個方法就是HandlerAdapter.supports()方法。如下是該方法的具體實作:
// AbstractHandlerMethodAdapter
@Override
public final boolean supports(Object handler) {
// 判斷目前handler是否為HandlerMethod類型,并且判斷supportsInternal()方法傳回值是否為true,
// 這裡supportsInternal()方法是提供給子類實作的一個方法,對于RequestMappingHandlerAdapter
// 而言,其傳回值始終是true,因為其隻需要處理的handler是HandlerMethod類型的即可
return (handler instanceof HandlerMethod
&& supportsInternal((HandlerMethod) handler));
}
// RequestMappingHandlerAdapter
@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
// 這裡RequestMappingHandlerAdapter隻是對supportsInternal()傳回true,因為其隻需要
// 處理的handler類型是HandlerMethod類型即可
return true;
}
在supports()方法判斷了所處理的handler是HandlerMethod類型之後,RequestMappingHandlerAdapter就會調用handle() 方法處理目前請求。該方法主要作用在于有五點:
- 擷取目前Spring容器在方法上配置的标注了@ModelAttribute但是沒有标注@RequestMapping注解的方法,在真正調用具體的handler之前會将這些方法依次進行調用
- 擷取目前Spring容器中标注了@InitBinder注解的方法,調用這些方法以對一些使用者自定義的參數進行轉換并且綁定
- 根據目前handler的方法參數标注的注解類型,如@RequestParam,@ModelAttribute等,擷取對應的ArgumentResolver,以将request中的參數轉換為目前方法中對應注解的類型
- 配合轉換而來的參數,通過反射調用具體的handler方法
- 通過ReturnValueHandler對傳回值進行适配,比如ModelAndView類型的傳回值就由ModelAndViewMethodReturnValueHandler處理,最終将所有的處理結果都統一封裝為一個ModelAndView類型的傳回值,這也是RequestMappingHandlerAdapter.handle()方法的傳回值類型。
這裡先看看RequestMappingHandlerAdapter.handle()方法的實作源碼:
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
// 檢查給定的請求中所支援的方法和所需的會話(如果有的話)。
checkRequest(request);
// 判斷目前是否需要支援在同一個session中隻能線性地處理請求
if (this.synchronizeOnSession) {
// 擷取目前請求的session對象
HttpSession session = request.getSession(false);
if (session != null) {
// 為目前session生成一個唯一的可以用于鎖定的key
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
// 對HandlerMethod進行參數等的适配處理,并調用目标handler
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 沒有HttpSession可用->不需要互斥鎖
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// 如果目前不需要對session進行同步處理,則直接對HandlerMethod進行适配
mav = invokeHandlerMethod(request, response, handlerMethod);
}
// 判斷目前請求頭中是否包含Cache-Control請求頭,如果不包含,則對目前response進行處理,
// 為其設定過期時間
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
// 如果目前SessionAttribute中存在配置的attributes,則為其設定過期時間。
// 這裡SessionAttribute主要是通過@SessionAttribute注解生成的
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
// 如果目前不存在SessionAttributes,則判斷目前是否存在Cache-Control設定,
// 如果存在,則按照該設定進行response處理,如果不存在,則設定response中的
// Cache的過期時間為-1,即立即失效
prepareResponse(response);
}
}
return mav;
}
上述代碼主要做了兩部分處理:
- 判斷目前是否對session進行同步處理,如果需要,則對其調用進行加鎖,不需要則直接調用
- 判斷請求頭中是否辦函Cache-Control請求頭,如果不包含,則設定其Cache立即失效。可以看到,對于HandlerMethod的具體處理是在
方法中進行的,如下是該方法的具體實作:invokeHandlerMethod()
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
// 擷取容器中全局配置的InitBinder和目前HandlerMethod所對應的Controller中配置的InitBinder,用于進行參數的綁定
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 擷取容器中全局配置的ModelAttribute和目前HandlerMethod所對應的Controller中配置的ModelAttribute,這些配置的方法将會在目标方法調用之前進行調用
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 将handlerMethod 封裝為一個ServletInvocableHandlerMethod對象,該對象用于對目前request的整體調用流程進行了封裝
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
// 設定目前容器中配置的所有ArgumentResolver
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
// 設定目前容器中配置的所有ReturnValueHandler
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
//将前面建立的WebDataBinderFactory設定到ServletInvocableHandlerMethod中
invocableMethod.setDataBinderFactory(binderFactory);
// 設定ParameterNameDiscoverer,該對象将按照一定的規則擷取目前參數的名稱
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
// 這裡initModel()方法主要作用是調用前面擷取到的@ModelAttribute标注的方法,
// 進而達到@ModelAttribute标注的方法能夠在目标Handler調用之前調用的目的
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
// 擷取目前的AsyncWebRequest,這裡AsyncWebRequest的主要作用是用于判斷目标
// handler的傳回值是否為WebAsyncTask或DefferredResult,如果是這兩種中的一種,
// 則說明目前請求的處理應該是異步的。所謂的異步,指的是目前請求會将Controller中
// 封裝的業務邏輯放到一個線程池中進行調用,待該調用有傳回結果之後再傳回到response中。
// 這種處理的優點在于用于請求分發的線程能夠解放出來,進而處理更多的請求,隻有待目标任務
// 完成之後才會回來将該異步任務的結果傳回。
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
// 封裝異步任務的線程池,request和interceptors到WebAsyncManager中
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// 這裡就是用于判斷目前請求是否有異步任務結果的,如果存在,則對異步任務結果進行封裝
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
// 封裝異步任務的處理結果,雖然封裝的是一個HandlerMethod,但隻是Spring簡單的封裝
// 的一個Callable對象,該對象中直接将調用結果傳回了。這樣封裝的目的在于能夠統一的
// 進行右面的ServletInvocableHandlerMethod.invokeAndHandle()方法的調用
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
// 對請求參數進行處理,調用目标HandlerMethod,并且将傳回值封裝為一個ModelAndView對象
invocableMethod.invokeAndHandle(webRequest, mavContainer);//真正執行目标方法
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
// 對封裝的ModelAndView進行處理,主要是判斷目前請求是否進行了重定向,如果進行了重定向,
// 還會判斷是否需要将FlashAttributes封裝到新的請求中
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
// 調用request destruction callbacks和對SessionAttributes進行處理
webRequest.requestCompleted();
}
}
上述代碼是RequestMappingHandlerAdapter處理請求的主要流程,其主要包含四個部分:
- 擷取目前容器中使用@InitBinder注解注冊的屬性轉換器
- 擷取目前容器中使用@ModelAttribute标注但沒有使用@RequestMapping标注的方法,并且在調用目标方法之前調用這些方法
- 判斷目标handler傳回值是否使用了WebAsyncTask或DefferredResult封裝,如果封裝了,則按照異步任務的方式進行執行
- 處理請求參數,調用目标方法和處理傳回值。這裡我們首先看RequestMappingHandlerAdapter是如何處理标注@InitBinder的方法的,如下是getDataBinderFactory()方法的源碼:
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod)
throws Exception {
// 判斷目前緩存中是否緩存了目前bean所需要裝配的InitBinder方法,如果存在,則直接從緩存中取,
// 如果不存在,則在目前bean中進行掃描擷取
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 在目前bean中查找所有标注了@InitBinder注解的方法,這裡INIT_BINDER_METHODS就是一個
// 選擇器,表示隻擷取使用@InitBinder标注的方法
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
// 這裡initBinderAdviceCache是在RequestMappingHandlerAdapter初始化時同步初始化的,
// 其内包含的方法有如下兩個特點:①目前方法所在類使用@ControllerAdvice進行标注了;
// ②目前方法使用@InitBinder進行了标注。也就是說其内儲存的方法可以了解為是全局類型
// 的參數綁定方法
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
// 這裡判斷的是目前配置的全局類型的InitBinder是否能夠應用于目前bean,
// 判斷的方式主要在@ControllerAdvice注解中進行了聲明,包括通過包名,類所在的包,
// 接口或者注解的形式限定的範圍
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
// 這裡是将目前HandlerMethod所在bean中的InitBinder添加到需要執行的initBinderMethods中。
// 這裡從添加的順序可以看出,全局類型的InitBinder會在目前bean中的InitBinder之前執行
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
// 将需要執行的InitBinder封裝到InitBinderDataBinderFactory中
return createDataBinderFactory(initBinderMethods);
}
這裡擷取InitBinder的方式主要有兩種,一種是擷取全局配置的InitBinder,全局類型的InitBinder需要聲明的類上使用@ControllerAdvice進行标注,并且聲明方法上使用@InitBinder進行标注;另一種則是擷取目前handler所在類中的使用@InitBinder注解标注的方法。這兩種InitBinder都會執行,隻不過全局類型的InitBinder會先于局部類型的InitBinder執行。關于使用@InitBinder标注的方法執行時間點,需要說明的是,因為其與參數綁定有關,因而其隻會在參數綁定時才會執行。
這裡我們繼續看RequestMappingHandlerAdapter是如何擷取@ModelAttribute标注的方法并且執行的,如下是
getModelFactory()
方法的源碼:
private ModelFactory getModelFactory(HandlerMethod handlerMethod,
WebDataBinderFactory binderFactory) {
// 這裡SessionAttributeHandler的作用是聲明幾個屬性,使其能夠在多個請求之間共享,
// 并且其能夠保證目前request傳回的model中始終保有這些屬性
SessionAttributesHandler sessionAttrHandler =
getSessionAttributesHandler(handlerMethod);
// 判斷緩存中是否儲存有目前handler執行之前所需要執行的标注了@ModelAttribute的方法
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.modelAttributeCache.get(handlerType);
if (methods == null) {
// 如果緩存中沒有相關屬性,那麼就在目前bean中查找所有使用@ModelAttribute标注,但是
// 沒有使用@RequestMapping标注的方法,并将這些方法緩存起來
methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
}
// 擷取全局的使用@ModelAttribute标注,但是沒有使用@RequestMapping标注的方法,
// 這裡全局類型的方法的聲明方式需要注意的是,其所在的bean必須使用@ControllerAdvice進行标注
List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {
// 判斷@ControllerAdvice中指定的作用的bean範圍與目前bean是否比對,比對了才會對其應用
if (clazz.isApplicableToBeanType(handlerType)) {
Object bean = clazz.resolveBean();
for (Method method : methodSet) {
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
});
// 将目前方法中使用@ModelAttribute标注的方法添加到需要執行的attrMethods中。從這裡的添加順序
// 可以看出,全局類型的方法将會先于局部類型的方法執行
for (Method method : methods) {
Object bean = handlerMethod.getBean();
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
// 将需要執行的方法等資料封裝為ModelFactory對象
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}
上述
getModelFactory()
方法主要工作還是擷取目前需要先于目标handler執行的方法,并且擷取的方式與前面的InitBinder非常的相似,這裡就不再贅述。關于這裡擷取的方法,其具體的執行過程實際上是在後面的
ModelFactory.initModel()
方法中進行。這裡我們直接閱讀該方法的源碼:
public void initModel(NativeWebRequest request, ModelAndViewContainer container,
HandlerMethod handlerMethod) throws Exception {
// 在目前request中擷取使用@SessionAttribute注解聲明的參數
Map<String, ?> sessionAttributes =
this.sessionAttributesHandler.retrieveAttributes(request);
// 将@SessionAttribute聲明的參數封裝到ModelAndViewContainer中
container.mergeAttributes(sessionAttributes);
// 調用前面擷取的使用@ModelAttribute标注的方法
invokeModelAttributeMethods(request, container);
// 這裡首先擷取目标handler執行所需的參數中與@SessionAttribute同名或同類型的參數,
// 也就是handler想要直接從@SessionAttribute中聲明的參數中擷取的參數。然後對這些參數
// 進行周遊,首先判斷request中是否包含該屬性,如果不包含,則從之前的SessionAttribute緩存
// 中擷取,如果兩個都沒有,則直接抛出異常
for (String name : findSessionAttributeArguments(handlerMethod)) {
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '"
+ name + "'", name);
}
container.addAttribute(name, value);
}
}
}
這裡
initModel()
方法主要做了兩件事:
- 保證@SessionAttribute聲明的參數的存在
- 調用使用@ModelAttribute标注的方法。我們直接閱讀invokeModelAttributeMethods()方法的源碼:
private void invokeModelAttributeMethods(NativeWebRequest request,
ModelAndViewContainer container) throws Exception {
while (!this.modelMethods.isEmpty()) {
// 這裡getNextModelMethod()方法始終會擷取modelMethods中的第0号為的方法,
// 後續該方法執行完了之後則會将該方法從modelMethods移除掉,因而這裡while
// 循環隻需要判斷modelMethods是否為空即可
InvocableHandlerMethod modelMethod =
getNextModelMethod(container).getHandlerMethod();
// 擷取目前方法中标注的ModelAttribute屬性,然後判斷目前request中是否有與該屬性中name字段
// 标注的值相同的屬性,如果存在,并且目前ModelAttribute設定了不對該屬性進行綁定,那麼
// 就直接略過目前方法的執行
ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
Assert.state(ann != null, "No ModelAttribute annotation");
if (container.containsAttribute(ann.name())) {
if (!ann.binding()) {
container.setBindingDisabled(ann.name());
}
continue;
}
// 通過ArgumentResolver對方法參數進行處理,并且調用目标方法
Object returnValue = modelMethod.invokeForRequest(request, container);
// 如果目前方法的傳回值不為空,則判斷目前@ModelAttribute是否設定了需要綁定傳回值,
// 如果設定了,則将傳回值綁定到請求中,後續handler可以直接使用該參數
if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue,
modelMethod.getReturnType());
if (!ann.binding()) {
container.setBindingDisabled(returnValueName);
}
// 如果request中不包含該參數,則将該傳回值添加到ModelAndViewContainer中,
// 供handler使用
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
}
這裡調用使用@ModelAttribute标注的方法的方式比較簡單,主要需要注意的是,對于調用結果,如果目前request中沒有同名的參數,則會将調用結果添加到ModelAndViewContainer中,以供給後續handler使用。
在調用完使用上述方法之後,Spring會判斷目前handler的傳回值是否為WebAsyncTask或DefferedResult類型,如果是這兩種類型的一種,那麼就會講這些任務放入一個線程池中進行異步調用,而目前線程則可以繼續進行請求的分發。這裡這種設計的目的是,預設情況下Spring處理請求都是同步的,也就是說進行請求分發的線程是會調用使用者所聲明的handler方法的,那麼如果使用者聲明的handler執行時間較長,就可能導緻Spring用于請求處理的線程都耗在了處理這些業務代碼上,也就導緻後續的請求必須等待,這在高并發的場景中是不能被允許的,因而這裡Spring提供了一種異步任務處理的方式,也就是進行請求分發的線程隻需要将使用者的業務任務放到線程池中執行即可,其自身可以繼續進行其他的請求的分發。如果線程池中的任務處理完成,其會通知Spring将處理結果傳回給調用方。關于異步任務的處理流程,我們後面會使用專門的章節進行講解,這裡隻是簡單的講解其主要功能。
在進行了相關前置方法調用和異步任務的判斷之後,RequestMappingHandlerAdapter就會開始調用目标handler了。調用過程在
ServletInvocableHandlerMethod.invokeAndHandle()
方法中,如下是該方法的源碼:
public void invokeAndHandle(ServletWebRequest webRequest,
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 對目标handler的參數進行處理,并且調用目标handler
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 設定相關的傳回狀态
setResponseStatus(webRequest);
// 如果請求處理完成,則設定requestHandled屬性
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null
|| mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
// 如果請求失敗,但是有錯誤原因,那麼也會設定requestHandled屬性
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// 周遊目前容器中所有ReturnValueHandler,判斷哪種handler支援目前傳回值的處理,
// 如果支援,則使用該handler處理該傳回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getReturnValueHandlingErrorMessage("Error handling return value",
returnValue), ex);
}
throw ex;
}
}
對于handler的調用過程,這裡主要分為三個步驟:
- 處理請求參數進行處理,将request中的參數封裝為目前handler的參數的形式。
- 通過反射調用目前handler
- 對方法的傳回值進行處理,以将其封裝為一個ModelAndView對象。這裡第一步和第二步封裝在了
方法中,我們首先看該方法的源碼:invokeForRequest()
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 将request中的參數轉換為目前handler的參數形式
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(),
getBeanType()) + "' with arguments " + Arrays.toString(args));
}
// 這裡doInvoke()方法主要是結合處理後的參數,使用反射對目标方法進行調用
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(),
getBeanType()) + "] returned [" + returnValue + "]");
}
return returnValue;
}
// 本方法主要是通過目前容器中配置的ArgumentResolver對request中的參數進行轉化,
// 将其處理為目标handler的參數的形式
private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable
ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 擷取目前handler所聲明的所有參數,主要包括參數名,參數類型,參數位置,所标注的注解等等屬性
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// providedArgs是調用方提供的參數,這裡主要是判斷這些參數中是否有目前類型
// 或其子類型的參數,如果有,則直接使用調用方提供的參數,對于請求處理而言,預設情況下,
// 調用方提供的參數都是長度為0的數組
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 如果在調用方提供的參數中不能找到目前類型的參數值,則周遊Spring容器中所有的
// ArgumentResolver,判斷哪種類型的Resolver支援對目前參數的解析,這裡的判斷
// 方式比較簡單,比如RequestParamMethodArgumentResolver就是判斷目前參數
// 是否使用@RequestParam注解進行了标注
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
// 如果能夠找到對目前參數進行處理的ArgumentResolver,則調用其
// resolveArgument()方法從request中擷取對應的參數值,并且進行轉換
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
} catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug(getArgumentResolutionErrorMessage("Failed to resolve",
i), ex);
}
throw ex;
}
}
// 如果進行了參數處理之後目前參數還是為空,則抛出異常
if (args[i] == null) {
throw new IllegalStateException("Could not resolve method parameter at index "
+ parameter.getParameterIndex() + " in "
+ parameter.getExecutable().toGenericString()
+ ": " + getArgumentResolutionErrorMessage("No suitable resolver for",i));
}
}
return args;
}
關于handler的調用,可以看到,這裡的實作也是比較簡單的,首先是周遊所有的參數,并且查找哪種ArgumentResolver能夠處理目前參數,找到了則按照具體的Resolver定義的方式進行處理即可。在所有的參數處理完成之後,RequestMappingHandlerAdapter就會使用反射調用目标handler。
對于傳回值的處理,其形式與對參數的處理非常相似,都是對ReturnValueHandler進行周遊,判斷哪種Handler能夠支援目前傳回值的處理,如果找到了,則按照其規則進行處理即可。如下是該過程的主要流程代碼:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 擷取能夠處理目前傳回值的Handler,比如如果傳回值是ModelAndView類型,那麼這裡的handler就是
// ModelAndViewMethodReturnValueHandler
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: "
+ returnType.getParameterType().getName());
}
// 通過擷取到的handler處理傳回值,并将其封裝到ModelAndViewContainer中
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
// 本方法的主要作用是擷取能夠處理目前傳回值的ReturnValueHandler
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value,
MethodParameter returnType) {
// 判斷傳回值是否為異步類型的傳回值,即WebAsyncTask或DefferredResult
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
// 對所有的ReturnValueHandler進行周遊,判斷其是否支援目前傳回值的處理。這裡如果目前傳回值
// 是異步類型的傳回值,還會判斷目前ReturnValueHandler是否為
// AsyncHandlerMethodReturnValueHandler類型,如果不是,則會繼續查找
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
// 判斷是否支援傳回值處理的主要位置,比如ModelAndViewMethodReturnValueHandler就會
// 判斷傳回值是否為ModelAndView類型,如果是,則表示其是目前ReturnValuleHandler所支援的類型
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
總的來講,RequestMappingHandlerAdapter的主要作用就是調用RequestMappingHandlerMapping所擷取到的handler,然後将傳回值封裝為一個ModelAndView對象,該對象中儲存了所要渲染的視圖名稱和渲染視圖時所需要的參數值,而具體的渲染過程則是通過View對象進行的。
SpringMVC九大元件:
/** 檔案上傳解析器 */
@Nullable
private MultipartResolver multipartResolver;
/** 區域資訊解析器(國際化視圖) */
@Nullable
private LocaleResolver localeResolver;
/** 主題解析器 */
@Nullable
private ThemeResolver themeResolver;
/** Handler映射資訊 */
@Nullable
private List<HandlerMapping> handlerMappings;
/** List of Handler擴充卡 */
@Nullable
private List<HandlerAdapter> handlerAdapters;
/** 異常解析器 */
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;
/** RequestToViewNameTranslator used by this servlet. */
@Nullable
private RequestToViewNameTranslator viewNameTranslator;
/** FlashMap管理器:SpringMVC中運作重定向攜帶資料的功能 */
@Nullable
private FlashMapManager flashMapManager;
/** 視圖解析器 */
@Nullable
private List<ViewResolver> viewResolvers;
共同點:九大元件全都是接口;接口就是規範,提供了強大的擴充性。
淺談 SpringMVC 九大元件動作原理:
在DispatcherServlet中有一個
initStrategies()
方法,該方法是一個初始化政策方法,而這個方法中就是九大元件的初始化方法。如下:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
這裡以
initHandlerMappings(context)
方法為例進行說明SpringMVC是如何對九大元件進行初始化的。進入到 initHandlerMappings(context)方法,代碼如下:
/**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
首先,先看這個方法上的一段注釋,意思是:初始化HandlerMapping 所使用的的這個類,如果在BeanFactory中沒有為這個名稱空間定義HandlerMapping bean将使用預設的BeanNameUrlHandlerMapping。
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
detectAllHandlerMappings
是DispatcherServlet的一個屬性,預設值為true,在這裡做了判斷條件。
這段代碼是檢測容器中所有的HandlerMapping,如果找到進行指派并排序。但如果沒有找到,就去容器中找id為HANDLER_MAPPING_BEAN_NAME 的 HandlerMapping。
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
如果還沒有找到,就會注冊一個預設的HandlerMapping。
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
那麼這個預設的HandlerMapping是如何注冊的呢?有上邊代碼知道,通過
getDefaultStrategies()
方法來注冊的。
getDefaultStrategies()
方法實作:
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Unresolvable class definition for DispatcherServlet's default strategy class [" +
className + "] for interface [" + key + "]", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
defaultStrategies:實作
static {
// 從屬性檔案加載預設政策實作。這目前是嚴格的内部設定,不允許應用程式開發人員定制。
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}
}
為給定的政策接口建立一個預設政策對象的清單,通過
defaultStrategies.getProperty(key)
實作使用DispatcherServlet.properties屬性檔案(在相同包下的DispatcherServlet類)來确定類名。它通過上下文的BeanFactory執行個體化政策對象。
總結後就一句話:元件的初始時去容器中找這個元件,如果沒有找到就是用預設的。
對靜态資源的處理:
1 :容器啟動時是否加載servlet,值大于0表示容器在應用啟動時就加載這個servlet,小于0或不指定,則在該servlet的第一個請求時才會去加載。正數的值越小,應用啟動是越先被加載,值相同則由容器選擇加載順序
*.form:也可以使用這種字尾比對,隻攔截字尾為 .form 的請求,不回攔截靜态資源的請求。
/:攔截 *.html,不攔截 *.jsp 請求。因為 *.jsp 請求會被Tomcat中預設的jsp Servlet處理,不會被dispatcherServlet攔截。
/*:攔截所有請求,包括 *.jsp 和 *.html。
“/” 和 “/" 的差別在于 "/” 的優先級高于 ”/“ 和 “*.字尾” 的路徑,而“/”在所有的比對路徑中,優先級最低。即當别的路徑都無法比對時,“/”所比對的Servlet才會進行相應的請求資源處理。
處理 *.jsp 是 Tomcat 做的事;所有項目的 web.xml 皆是繼承 Tomcat中的 web.xml
Tomcat中 web.xml :
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
Tomcat中的web.xml中有一個DefaultServlet是url-pattern = /
DefaultServlet 是 Tomcat 中處理靜态資源的,除了jsp和servlet外剩下的都是靜态資源;如html、圖檔等。tomcat會在伺服器下找到這個資源并傳回。
前端控制器的 / 相當于重寫了tomcat伺服器中的DefaultServlet。
靜态資源的處理:
方法一:激活Tomcat的default Servlet來處理靜态資源
<!--Tomcat, Jetty, JBoss, and GlassFish預設Servlet的名字為“default”-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.html</url-pattern>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
default Servlet無法解析jsp頁面,直接輸出html源碼。
方式二:使用
<mvc:default-servlet-handler/>
請求的url若是靜态資源請求,則轉由org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler 處理并傳回,否則才由DispatcherServlet處理。DefaultServletHttpRequestHandler使用的是各個Servlet容器自己預設的Servlet(如jsp servlet)。
方式三:Spring3.0.4以後版本
<mvc:resources/>
<mvc:annotation-driven />
<!--兩個*,它表示映射/resources/下所有的URL,包括子路徑-->
<mvc:resources mapping="/resources/**" location="/WEB-INF/, classpath:config, /resources/" cache-period="31536000"/>
<mvc:default-servlet-handler/>
将靜态資源的處理經由 Spring MVC 架構交回 Web 應用伺服器處理。而
<mvc:resources/>
更進一步,由 Spring MVC 架構自行處理靜态資源。
mvc:resources/>
允許靜态資源放在任何地方,如 WEB-INF 目錄下、類路徑下,甚至 JAR 包中。可通過 cache-period 設定用戶端資料緩存時間。
使用
mvc:resources/>
元素,把 mapping 的 URL 注冊到 SimpleURLHandlerMapping的urlMap中, key 為 mapping 的 URl pattern值,而 value為ResourceHttpRequestHandler 處理并傳回,是以就支援 classpath 目錄 和jar包内靜态資源的通路。
預設配置檔案
預設SpringMCVC配置檔案位置:WEB-INF\servletName-servlet.xml
使用 @RequestMapping 映射請求:
@RequestMapping
作用是為控制器指定可以處理那些 URL 請求,該注解可以放在類上或者方法上。
- 标注在類上:提供初步的請求請求映射資訊。相對于 WEB 應用的根目錄
- 标注在方法上:提供進一步的細分映射資訊。相對于類定義出的 URL。
若類上未标注 @RequestMapping ,則方法出标記的 URL 相對于 WEB 應用的根目錄
DispatcherServlet 截獲請求後,就通過控制器上@RequestMapping 提供的映射資訊确定請求所對應的處理方法。
@RequestMapping 的 method 屬性用于指定處理哪些HTTP方法。
@RequestMapping(value = "/hello" , method = {RequestMethod.GET,RequestMethod.POST} )
//共有這幾個 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE;常用的:GET,POST,PUT,DELETE
@RequestMapping的param屬性可以指定某一種參數名類型,當請求資料中包含該名稱的請求參數時,才能進行響應,否則拒絕此次請求。
// 發送請求時必須帶上一個名為 username 的參數,否則 404
@RequestMapping(value = "/hello" , params = {"username"})
@RequestMapping的headers屬性可以指定某一種請求頭類型。
consumes屬性表示處理請求的送出内容類型(Content-Type),例如"application/json,text/html"。
而produces表示傳回的内容類型,僅當request請求頭中的Accept包含該指定類型時才會傳回。
@RequestMapping(value = "/hello" , consumes = "application/json")
@RequestMapping(value = "/hello" , produces = "application/json")
參數綁定
當使用者發送請求時,前端控制器會請求處理映射器傳回一個處理器鍊,然後請求處理器擴充卡執行響應的Handler。此時,HandlerAdapter會調用SpringMVC提供的參數綁定元件将請求的key/value資料綁定到Controller處理器方法對應的形參上。
簡單類型參數綁定
通過RequestParam将某個請求參數綁定到方法的形參上。value屬性不指定時,則請求參數名稱要與形參名稱相同。required參數表示是否必須傳入。defaultValue參數可以指定參數的預設值。
路徑變量
@RequestMapping(value = "/hello/{id}")
public String test01(@PathVariable("id")String id ){
System.out.println("程式啟動..."+id);
return "success";
}
}
包裝類型參數綁定
<form action="/springmvcdemo/user/findUserByCondition.action" method="post">
id: <input type="int" name="id"/>
name: <input type="text" name="name"/>
<input type="submit"/>
</form>
name屬性名稱與User類屬性對應,Spring MVC的HandlerAdapter會解析請求參數生成具體的實體類,将相關的屬性值通過set方法綁定到實體類中。
@RequestMapping("/findUserByCondition")
@ResponseBody
public User findUserByCondition(User user) {
return user;
}
集合類型參數綁定
@RequestMapping("/findUsers")
@ResponseBody
public List<User> findUsers(UserList userList) {
List<User> users = userList.getUsers();
for(User user : users) {
LOGGER.info("user_id: " + user.getId() + " " + "user_name: " + user.getName());
}
return users;
}
<form action="/springmvcdemo/user/findUsers.action" method="post">
<input name="users[0].id" value="1"/>
<input name="users[0].name" value="tyson"/>
<input name="users[1].id" value="2"/>
<input name="users[1].name" value="sophia"/>
<input type="submit"/>
</form>
包裝類中定義的List屬性的名稱要與前端頁面的集合名一緻。
public class UserList {
private List<User> users;
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
REST風格的URL位址限制
REST是一種開發風格,有四種請求:GET、POST、PUT、DELETE。分别對應着四種請求操作:GET用來擷取資源,POST用來建立資源,PUT用來更新資源,DELETE用來删除資源。REST風格提倡URL位址使用統一的風格設計。
通常的浏覽器隻支援發送POST和GET請求,不支援DELETE和PUT,要想支援,則需要使用HiddenHttpMethodFilter過濾器來将post請求轉換為put跟delete請求。
<!--開啟Spring支援REST風格-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<form action="/hello" method="post">
<!-- Form 表單中加入此句-->
<input type="hidden" name="_method" value="DELETE">
</form>
源碼分析:
HiddenHttpMethodFilter首先繼承了OncePerRequestFilter,那麼就先看他的重寫方法
doFilterInternal
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//為替換原有的request做準備
HttpServletRequest requestToUse = request;
//如果請求方式為 post 且沒有異常
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
//擷取表單上_method的值(put、delete)
String paramValue = request.getParameter(this.methodParam);
//如果_method的值不為空
if (StringUtils.hasLength(paramValue)) {
//将擷取的這個值轉為大寫
String method = paramValue.toUpperCase(Locale.ENGLISH);
//判斷這個值是否包含(PUT、DELETE、PATCH等)這些常量之一
if (ALLOWED_METHODS.contains(method)) {
//開時替換request請求。 1.調用自己的内部類(HttpMethodRequestWrapper)的構造器,将request和method傳入;2.内部類(HttpMethodRequestWrapper)重寫了getMehod方法,傳回的是你設定傳入的method (public String getMethod() { return this.method;}),此時requestToUse已經是自己設定的method了
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
//放行 将新的requestToUse代替原有的那個request執行後面的代碼。
filterChain.doFilter((ServletRequest)requestToUse, response);
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
public String getMethod() {
return this.method;
}
}
小坑:
請求處理
擷取請求參數的值:
@RequestParam
擷取請求參數:
@RequestMapping("/demo02")
public String demo02(@RequestParam("user") String param){
System.out.println("預設擷取請求參數"+param);
return "success";
}
@RequestParam可以指定請求參數的名。@RequestParam(“user”)相當于 request.getParameter(“user”);
而@PathVariable 是路徑變量 ,如 demo01/{user} 。
預設擷取請求參數:
直接在方法上入參上寫一個和請求參數名相同的變量,這個變量就自動接收請求參數的值。如果這個參數在發送請求時沒有傳或者發送請求時的參數不是param,那麼預設值為null。
@RequestMapping("/demo02")
public String demo02(Srting param){
System.out.println("預設擷取請求參數"+param);
return "success";
}
擷取請求頭中的資訊:
浏覽器中的頭資訊:
擷取請求中帶的某個@Cookie:
以前擷取某個Cookie
Cookie[] cookies = request.getCookie();
for(Cookie cookie : cookies){
if(cookie.getName.equals("JSESSIONID")){
cookie.getValue();
}
}
使用@CookieValue:
這兩種都能擷取Cookie
傳入POJO,SpringMVC自動封裝
如果請求參數太多了,可以封裝成一個POJO,SpringMVC會自動為這個POJO指派。
- 将POJO中的每一個屬性,從request請求中擷取出來并封裝。
- 還支援級聯屬性(屬性的屬性)。
- 請求參數的參數名和對象中的屬性一一對應。
CharacterEncodingFilter解決亂碼問題:
送出的資料可能有亂碼
請求亂碼:
- GET請求:在Tomcat檔案中改server.xml的8080處加入URIEncoding=“utf-8”。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"
URIEncoding="utf-8" />
- POST請求:在第一次擷取參數之前設定CharacterEncodingFilter過濾器(該過濾器一定要設定在其他過濾去之前)
web.xml
<!--字元編碼過濾器 該過濾器一定要配置在其他過濾器之前-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--解決POST請求亂碼-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--解決響應亂碼-->
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
為什麼要放在最前面呢?處理請求亂碼其實就是設定request.setCharacterEncoding(“UTF-8”);這句話要在第一次擷取請求參數之前調用。而CharacterEncodingFilter過濾器的本質就是這句話。要是其他過濾器(這裡舉例HiddenHttpMethodFilter)要是放在CharacterEncodingFilter,如下圖就已經擷取參數了。是以會導緻處理編碼的過濾器無效。
資料輸出
SpringMVC除過在方法上傳入原生的request和session外還能怎樣把資料帶給頁面?
方法參數:
可以在方法處傳入Map、或者Model或者ModelMap。在這些參數中儲存的資料都會被放在域中。可以在頁面擷取。
@RequestMapping("/demo04")
public String demo04(Map<String,Object> map){
map.put("msg", "Map");
return "success";
}
@RequestMapping("/demo05")
public String demo05(Model model){
model.addAttribute("msg", "Model");
return "success";
}
@RequestMapping("/demo06")
public String demo06(ModelMap modelMap){
modelMap.addAttribute("msg","ModelMap" );
return "success";
}
這三種參數的資料都是放在request域中。
Map、Model、ModelMap的關系:
在控制台輸出三種參數的類型
可以看到,他們都是 BindingAwareModelMap。也就是在 BindingAwareModelMap 中儲存的資料會被放在請求域中。
方法傳回值:ModelAndView
方法的傳回值可以變為ModelAndView類型。ModelAndView既包含視圖資訊(頁面位址)也包含模型資料(給頁面帶的資料),而且資料是放在請求域中。
@RequestMapping("/demo07")
public ModelAndView demeo07(){
ModelAndView view = new ModelAndView("success");
view.addObject("msg", "ModelAndView");
return view ;
}
SessionAttributes給session域暫存資料:
@SessionAttributes
隻能标注在類上。給BindingAwareModelMap中儲存的資料,或者ModelAndView中的資料同時給Session中存方一份。該注解的三個屬性如圖。
value指定儲存資料時要給session中放的資料的key。types指定改資料的類型。
總之該注解不推薦使用,原因是,如果使用該注解,就必須確定Session中存有資料,否則會抛出異常。如果要在session中存放資料 推薦使用原生API。
視圖解析
視圖和視圖解析:
- 請求處理方法執行完成後,最終傳回一個 ModelAndView對象。對于那些傳回 String ,View或ModelMap等類型的處理方法,SpringMVC 也會在内部将他們裝配成一個ModelAndView對象,它包含了邏輯名和模型對象的視圖。
- SpringMVC 借助視圖解析器(ViewResolver)得到最終的視圖對象(View),最終的視圖可以是JSP,也可能是Excel、JFreeChart等各種表現形式的視圖
- 對于最終究竟采取何種視圖對象對模型資料進行渲染,處理器并不關心,處理器工作重電聚焦在生産模型資料的工作上,進而實作MVC的充分解耦
視圖:
- 視圖的作用是渲染模型資料,将模型裡的資料以某種形式呈現給客戶。
- 為了實作視圖模型和具體實作技術的解耦,Spring在 servlet 包中定義了一個高度抽象的 View接口:
- 視圖對象有視圖解析器負責執行個體化。由于視圖是無狀态的,是以他們不會有線程安全的問題
常用的視圖實作類:
視圖解析器:
- SpringMVC 為邏輯視圖名的解析提供了不同的政策,可以在 Spring WEB上下文中配置一種或多種解析政策,并指定他麼之間的先後順序。每一種映射政策對應一個具體的視圖解析器實作類。
- 視圖解析器的作用比較單一:将邏輯視圖解析為一個具體的視圖對象。
- 所有的視圖解析器都必須實作ViewResolver接口
常用的視圖解析器實作類:
- 程式員可以選擇一種視圖解析器或混用多種視圖解析器
- 每個視圖解析器都實作了Ordered接口并開放出一個order屬性,可以通過 order屬性指定解析器的優先順序,order越小優先級越高。
- SpringMVC會按視圖解析器順序的優先順序對邏輯視圖名進行解析,直到解析成功并傳回視圖對象,否則将抛出ServletException異常。
forward字首指定一個轉發操作:
以前轉發頁面是通過視圖解析器進行拼串。
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程式啟動...");
return "success";
}
<!--配置視圖解析器-->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="suffix" value=".jsp"></property>
</bean>
通過 給 success 拼接字尾 .jsp。
除了這種方法外也有以下方式進行頁面轉發
- 使用相對路徑:
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程式啟動...");
return "/success";
}
- 使用 forward 字首轉發到目前項目下的一個頁面。這種方法但不會使用視圖解析器拼接。
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程式啟動...");
return "forward:/success.jsp";
}
并且這種方法還可以實作請求之間的調用,如下:
@RequestMapping("/demo01")
public String demo01(){
System.out.println("程式啟動...");
return "forward:/demo06";
}
@RequestMapping("/demo06")
public String demo06(ModelMap modelMap){
modelMap.addAttribute("msg","ModelMap" );
System.out.println(modelMap.getClass());
return "success";
}
redirect字首指定重定向到頁面:
redirect:重定向的路徑
/success.jsp :代表就是目前項目下開始;SpringMVC會為路徑自動的拼接上項目名。
原生的Servlet重定向 / 路徑需要加上項目名才能成功:response.sendRedirect(“項目名/success.jsp”)
是以說,有字首的轉發和重定向操作,配置的視圖解析器就不會進行拼串。
視圖解析流程:
上面前端控制器中說到過,所有方法的傳回值都會被包裝成 ModelAndView 對象
該方法是進行
視圖渲染
(将域中的資料在頁面展示),下面來到
processDispatchResult
方法看視圖渲染流程:
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);
}
}
// 處理程式是否傳回要渲染的視圖?
if (mv != null && !mv.wasCleared()) {
// 真正渲染視圖的方法
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
首先,該方法是處理 處理程式選擇和處理程式調用的結果,它要麼是ModelAndView,要麼是要解析為ModelAndView的異常。如果ModelAndView不為空且沒有并清理掉,就調用
render
方法,接下來詳細看一下
render
方法:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 确定請求的語言環境,并将其應用于響應。
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// 解析視圖名。
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// 不需要查找:ModelAndView對象包含實際的View對象。
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// 委托給View對象進行渲染。
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
該方法是渲染給定的ModelAndView。這是處理請求的最後一個階段。它可能涉及按名稱解析視圖。這個方法有一個對象
View view;
那麼該對象與ViewResolver又有什麼關系呢?
public interface ViewResolver {
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
可以看到,ViewResolver是一個接口,隻定義了一個空方法
resolveViewName
,該方法根據視圖名(方法的傳回值)得到View對象。那麼,
resolveViewName
是如何根據視圖名得到View對象的呢?我們詳細來看
resolveViewName
方法:
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
該方法就是将給定的視圖名稱解析為一個視圖對象。它預設實作詢問這個排程程式的所有ViewResolvers(九大元件之一)。可以覆寫自定義解析政策,可能基于特定的模型屬性或請求參數。
這個 viewResolvers 可以是我們自己定義的,如果自己沒有定義,那麼預設也是使用的
InternalResourceViewResolver
可以在DispatcherServlet.properties中檢視九大元件在沒有自定義的情況下使用的預設值。
通過上面的代碼可以看到視圖的渲染是通過
resolveViewName
方法,細看該方法:
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 第一次沒有緩存
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
// 如果沒有view對象,
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// 要求子類建立View對象。
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
真正建立view對象:
protected View createView(String viewName, Locale locale) throws Exception {
// 如果這個解析器不應該處理給定的視圖,則傳回null以傳遞給鍊中的下一個解析器。
if (!canHandle(viewName, locale)) {
return null;
}
// 檢查特殊的“redirect:”字首。
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
// 檢查特殊的“forward:”字首。
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
// 沒有字首或字尾則調用父類實作:調用loadView。
return super.createView(viewName, locale);
}
執行完createView後,就會建立出一個
InternalResourceView
類型的View對象,并傳回出去。如果沒有建立就換下一個視圖解析器。
然後在調用
view.render()
方法。細看 render 方法:
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("View " + formatViewName() +
", model " + (model != null ? model : Collections.emptyMap()) +
(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
根據指定的模型準備視圖,并将其與靜态屬性和RequestContext屬性合并(如果需要的話)。委托renderMergedOutputModel進行實際渲染,也就是
renderMergedOutputModel()
方法要做的事情,而InternalResourceView重寫了該方法,細看該方法:
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将模型對象公開為請求屬性。
exposeModelAsRequestAttributes(model, request);
// 将幫助程式作為請求屬性公開(如果有的話)。
exposeHelpers(request);
// 确定請求配置設定器的路徑。
String dispatcherPath = prepareForRendering(request, response);
// 擷取目标資源(通常是JSP)的RequestDispatcher(請求轉發器)
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// 如果已經包含或響應已經送出,則執行include,否則轉發。
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
其中
exposeModelAsRequestAttributes(model, request);
是将給定映射中的模型對象公開為請求屬性。名稱将取自模型Map。這個方法适用于所有可以通過{@link javax.servlet.RequestDispatcher}通路的資源。細看該方法:
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
該方法将模型中所有資料取出來放在request域中。
看了這麼多,總結成一句話:
視圖解析器隻是為了得到視圖對象;視圖對象才能真正的轉發(将模型資料全部放在請求域中)或者重定向到頁面視圖對象才能真正的渲染視圖
JSTL支援國際化:
當我們導入這兩個包時,
可以發現,view對象自動變成了JstkView,這時候可以快速友善的支援國際化功能;
而如果沒有導入這兩個包,也可以使用配置指定使用的view視圖。在視圖解析器中有一個名為
viewClass
的屬性,指定他的Value值為自己想設定的View對象即可。
View對象依舊是JstlView。
國際化中的坑:
問題:發送一個請求直接來到一個頁面,除了做國際化的請求外,其它請求都不能通路。
分析:
- 一定要過SpringMVC的視圖解析流程,才會建立一個JSTLView來支援國際化功能;不能直接把請求交給Tomcat。
- 不能使用forwar、redirect字首(原因上邊建立view對象處提到)
解決:SpringMVC配置檔案中加入
<mvc:view-controller path="指定請求路徑" view-name="映射給那個視圖"></mvc:view-controller>
,但這樣做了之後,其他請求将不能通路,這時需要加上開啟MVC注解驅動模式:
<mvc:annotation-driven></mvc:annotation-driven>
,他會自動注冊 DefaultAnnotationHandlerMapping 與 AnnotationMethodHandlerAdapter 兩個 bean, 是 spring MVC 為 @Controllers 分發請求所必須的
自定義視圖和自定義視圖解析器:
步驟:
- 編寫自定義的視圖解析器,和視圖實作類
/**
* 自定義視圖解析器
* 視圖解析器必須放在 IOC 容器中
*/
@Component
public class MyViewResolver implements ViewResolver {
/**
*
* @param viewName 根據視圖名傳回視圖對象
* @param locale 國際化
* @return
* @throws Exception
*/
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 如果視圖名以customize開頭,如果不能處理則傳回null
if(viewName.startsWith("customize:")){
return new MyView();
}else{
return null;
}
}
}
/**
* 自定義視圖
*/
public class MyView implements View {
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws Exception {
// 從model中取出資料
System.out.println("model中的資料:"+model.get("msg"));
response.setContentType("text/html");
response.getWriter().write("<h1>自定義轉發視圖頁面</h1>");
}
}
注:視圖解析器必須放在IOC容器中
然後在SpringMVC的配置檔案中,配置如下:
調試項目:
可以看到viewResolvers中有兩個值,在Spring4中viewResolvers中的值還需要排序,而在Spring5中則不需要排序。
資料綁定
前邊提到過SpringMVC可以自動将POJO進行封裝,那麼是如何進行封裝的呢?原理又是什麼呢?下邊來探索一下。
SpringMVC封裝自定義類型的對象的時候,JavaBean要和頁面送出的資料進行一一綁定,而在進行綁定期間需要解決很多問題,如:頁面送出的所有資料都是字元串,而Javabean中的類型有Internet、Date、Double等資料類型,要進行綁定,需要将String類型轉換成Javabean中的資料類型;再者,資料綁定期間資料的格式化問題,如birth字段在前端是String類型,而在Javabean中是一個Date,且格式有xxxx-xx-xx、xxxx/xx/xx、xxxx.xx.xx等格式;另外還有如何進行資料校驗來確定使用者所送出的資料的合法性。而這個過程是如何實作的呢?
來看ModelAttributeMethodProcessor類中一段相關的源碼:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// 建立屬性執行個體
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// 無BindingResult參數-> fail with BindException
throw ex;
}
// 否則,公開null/空值和相關的BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean屬性綁定和驗證;
// 如果在構造時綁定失敗,則跳過。
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// 值類型适應,也包括java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// 在模型末尾添加解析屬性和BindingResult
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
看這段源碼的注釋意思是:
從模型中解析參數,如果未找到參數,則使用預設值執行個體化它。然後,模型屬性通過資料綁定填充請求值,如果{@code @java.validation則可選驗證。Valid}存在于實參中。
再來具體分析:
在這段源碼有一個
WebDataBinderFactory
:他是一個接口,其中有一個建立binder(綁定器)的方法,如圖:
他根據給定的對象建立一個`WebDataBinder`類型的Web資料綁定器,用于從web請求參數到JavaBean對象的資料綁定。專為web環境設計,但不依賴于Servlet API; 其中參數`webRequest`:目前請求;`target`:為其建立資料綁定器的對象,如果為簡單類型建立則傳入null;`objectName`:目标對象的名稱
資料的綁定則有它來完成。在這個綁定器中有許多元件,如下圖。處理綁定期間的一系列問題就是有這些元件來完成的。
比如,資料類型轉換、資料格式化的工作就是有ConversionService元件來完成的,資料校驗則是由Validator來完成的。而這個完成過程又是怎樣的呢?
SpringMVC通過反射機制對目标方法進行解析,将請求消息綁定到處理方法的入參中。資料綁定的核心部件是DataBinder,運作機制如下:
詳解 <mvc:annotation-driven/>
注解
<mvc:annotation-driven/>
首先,SpringMVC配置檔案中加入
<mvc:annotation-driver>
注解後,SpringMVC會自動注冊
RequestMappingHandlerMapping
、
RequestMappingHandlerAdapter
、
ExceptionHandlerExceptionResolver
三個元件。還将提供以下支援:
- 支援使用 ConversionService 執行個體對表單參數進行類型轉換
- 支援使用 @NumberFormat 、@DateTimeFormat注解完成資料類型的格式轉換
- 支援使用 @Valid 注解對 JavaBan 執行個體進行 JSR303驗證
- 支援使用 @RequestBody 和 @ResponseBody 注解
可以在
BeanDefinitionParser
接口中看到很多定義解析器,其中AnnotationDrivenBeanDefinitionParser就是解析
<mvc:annotation-driven/>
這個标簽的。
在開發中,如果請求報錯就簡單粗暴加上該注解驅動标簽
<mvc:default-servlet-handler/>
。而且必要時要配合另一個注解标簽
<mvc:default-servlet-handler/>
一起使用。具體分析:
①:如果這兩個标簽都沒有加:@RequestMapping映射資源可以通路,但靜态資源(.html、.js、.img等)不可通路
這種情況下,動态資源能通路是因為 RequestMappingHandlerMapping 中的handlerMap中儲存了每一個資源的映射資訊。而沒有儲存靜态資源的映射資訊,是以靜态資源不能通路。
②:如果隻加上
<mvc:default-servlet-handler/>
,靜态資源可以通路,但動态資源不可以通路
這種之是以靜态資源能通路,是因為 RequestMappingHandlerMapping 沒有了,是以動态資源不能通路,卻出現了 SimpleUrlHandlerMapping 替換了,起作用是将所有請求直接交給Tomcat。
③:如果隻加上
<mvc:annotation-driven/>
,@RequestMapping映射資源可以通路,但靜态資源不可通路
這種情況與兩者都不加的情況一樣,不多介紹。
④:隻有兩者都加上的情況下,既可以通路動态資源又可以通路靜态資源。
這種情況下可以看到既有RequestMappingHandlerMapping也有SimpleURlHandlerMapping,且前者中儲存的是動态請求的資訊,後者中儲存的是靜态請求的資訊。
日期格式化
資料格式化
FormattingConversionServiceFactroyBean内部已經注冊了:
- NumberFormatAnnotationFormatterFactroy:支援對數字類型的屬性使用 @NumberFormat注解
- JodaDateTimeFormatAnnotationFormatterFactory:支援對日期類型的屬性使用 @DateTimeForma 注解
日期格式化
@DateTimeFormat注解可對java.util.Date、java.util.Calendar、java.lang.Long時間類型進行标注:
- pattern 屬性:類型為字元串。指定解析/格式化字段資料的模式,如:“yyyy-MM-dd hh:mm:ss”
- iso 屬性:類型為DateTimeFormat.ISO。指定解析/格式化字段資料的ISO模式,包括四種:ISO.NONE(不使用)預設的、ISO.DATE(yyyy-MM-dd)、ISO.TIME(hh:mm:ss.SSSZ)、ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
- style屬性:字元串類型。通過樣式指定日期時間的格式,由兩位字元組成,第一位表示日期的格式,第二位表示時間的格式:S:短日期/時間格式、M:中日期/時間格式、L:長日期/時間格式、F:完整日期/時間格式、-:忽略日期或時間格式
數值格式化
@NumberFormat 可對類似數字類型的屬性進行标注,他擁有兩個互斥的屬性:
- style:類型為NumberFormat.Style。用于指定樣式類型,包括三種:Style.NUMBER(正常資料類型)、Style.CURRENCY(貨币類型)、Style.PERCENT(百分數類型)
- pattern:類型為String,自定義樣式,如patter=“#,###”
資料校驗
在開發中隻做前端資料安全性校驗是不夠的,因為有很多辦法能夠避開前端校驗,比如:浏覽器禁用js後,前端校驗就不能使用了。是以重要的資料一定要加上後端驗證。
SpringMVC 可以用JSR303來做資料校驗,JSR303是JAVAEE中的一項子規範,叫做Bean Validation ,Hibernate Validator 是 Bean Validation 的參考實作 . Hibernate Validator 提供了 JSR 303 規範中所有内置constraint 的實作。
如何快速的進行後端校驗呢?
- 導入Jar包
- hibernate-validator-annotation-processor-5.0.0.CR2.jar
- hibernate-validator-5.0.0.CR2.jar
- validation-api-1.1.0.CR1.jar
- jboss-logging-3.1.1.GA.jar
- classmate-0.8.0.jar
- 給 JavaBean 加上校驗注解
- 在 SpringMVC 封裝對象的時候,告訴SpringMVC這個 JavaBean需要校驗,隻需在這個對象參數前加上注解
@Valid
-
如何知道校驗結果呢?
隻需在這個對象參數後緊跟
參數。該參數封裝了校驗結果。BindingResult
SpringMVC與Ajax
後端傳回JSON資料
有時需要後端傳回Json資料時,SpringMVC要如何實作呢?
導包:(使用的5.+的SpringMVC要使用2.9+的版本)
- jackson-annotations-2.9.8.jar
- jackson-databind-2.9.8.jar
- jackson-core-2.9.8.jar
在請求的方法上加上注解
@ResponseBody
:将傳回資料放到響應體重,即可将傳回的資料轉換成 Json 格式。
@ResponseBody
@RequestMapping("/demo03")
public Book demo03(@Valid Book book, BindingResult bindingResult){
boolean hasErrors = bindingResult.hasErrors();
if(hasErrors){
// 如果校驗結果有錯,就不進行業務處理
}else{
// 進行業務處理
System.out.println("SpringMVC自動封裝對象"+book);
}
return book;
}
可以在浏覽器看到,資料已經被轉換成JSON格式了。
在jackson-annotations這個jar包下,有一些注解可以規定傳回格式,比如:JsonIgnore ,标注該注解的屬性,不會以json的格式傳回。
就是因為加了該注解,上邊的JSON格式的資料中才沒有bookName這個屬性的值。
前端頁面使用Ajax擷取JSON資料:
<script type="text/javascript" src="js/jquery-3.1.1.js"></script>
<script>
$("#btn_Ajax").click(function () {
$.ajax({
url: "/demo03",
type: "POST",
data:$("#form_Ajax").serialize(),
success: function (data) {
console.log(data);
//如果是一個集合 可以使用each函數周遊取值
$.each(data,function () {
})
}
});
//禁用掉預設行為
return false;
});
</script>
@ResponseBody
注解标注在方法上,方法向前端傳回JSON格式的資料。那麼與之對應的有一個注解
@RequestBody
是幹什麼的呢?
@RequestBody
從上圖可以看到,該注解能夠擷取一個請求的請求體,該請求必須是一個POST請求。那麼問題來了,他可不可以把一個表單中的資料封裝成一個對象呢?就像前邊提到的自定義的轉換器那種思想。我們來細看:
@RequestMapping("/demo01")
public String demo01(@RequestBody Book book){
System.out.println("請求體"+ book);
System.out.println("程式啟動...");
return "success";
}
$("#btn_requestBody").click(function () {
// js對象
var book = {
bookName:$("#bookName").val(),
author:$("#author").val(),
price:$("#price").val()
}
// 轉換成json字元串
var bookJson = JSON.stringify(book);
$.ajax({
url:"/demo01",
type:"POST",
data:bookJson,
contentType:"application/json"
});
//禁用預設行為
return false;
});
可以看到,前端傳入的 json格式的資料能被後端接收并轉換成一個對象。
**注意:**如果報錯:JSON parse error: Unrecognized token XXX: was expecting (‘true‘, ‘false‘ or ‘null‘);說明前端傳入的不是标準的json格式,我這裡之是以會出現這個錯誤,是應為傳的資料直接給序列化了。
這就是
@RequestBody
的作用:前端傳一個json字元串,它可以映射成成一個對象
補充:
HttpEntity 參數
以前我們想拿請求頭都是使用
@RequestHeader
注解,它有點雞肋,因為它隻能拿到某一個請求頭。如果我們想拿到整個請求頭,可以給請求方法增加一個參數
HttpEntity<String>
,該參數可以拿到整個請求頭。
ResponseEntity
請求方法傳回類型為ResponseEntity 意思是可以自定義一個響應體。(一般用不到)
@RequestMapping("/demoResponseEntity")
public ResponseEntity<String> demoResponseEntity(){
String body = "自定義響應體";
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add("Sec-Fetch-Dest", new Date().toString());
// 枚舉類型
HttpStatus status = HttpStatus.OK;
return new ResponseEntity<String>(body,headers,status);
}
檔案上傳
SpringMVC實作檔案上傳步驟:
1、檔案上傳表單
<form action="" method="post" enctype="multipart/form-data">
使用者頭像:<input type="file" name="fileName" >
使用者名:<input type="text" name="userName">
<input type="submit" value="上傳">
</form>
**注意:**檔案上傳的表單一定要有 enctype=“multipart/form-data”
2、導包
commons-fileupload-1.3.jar
commons-io-2.4.jar
3、在SpringMVC配置檔案中配置檔案上傳解析器
<!-- 檔案上傳解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--檔案上傳表單中可能會有普通項-->
<property name="defaultEncoding" value="utf-8"></property>
</bean>
4、檔案上傳請求處理
隻需在檔案上傳請求方法上加一個
MultipartFile
類型的參數,該參數封裝目前檔案的資訊,可以直接儲存
@RequestMapping("/uploadDemo")
public String uploadDemo(@RequestParam("fileName") MultipartFile file , String userName, Model model){
// SpringMVC自動把檔案流封裝給 file
System.out.println("檔案項name的值"+file.getName());
System.out.println("檔案名稱"+file.getOriginalFilename());
// 檔案儲存
try {
file.transferTo(new File("G:\\"+file.getOriginalFilename()));
model.addAttribute("msg", "檔案上傳成功");
} catch (IOException e) {
model.addAttribute("msg",e.getMessage() );
}
return "success";
}
多檔案上傳:
<form action="${path}/uploadDemo" method="post" enctype="multipart/form-data">
使用者頭像:<input type="file" name="fileName" ><br/>
使用者履歷:<input type="file" name="fileName" ><br/>
使用者名:<input type="text" name="userName">
<input type="submit" value="上傳">
</form>
@RequestMapping("/uploadDemo")
public String uploadDemo(@RequestParam("fileName") MultipartFile[] file , String userName, Model model){
// SpringMVC自動把檔案流封裝給 file
for (MultipartFile multipartFile : file) {
if(!multipartFile.isEmpty()){
try {
multipartFile.transferTo(new File("G:\\"+multipartFile.getOriginalFilename()));
} catch (IOException e) {
model.addAttribute("msg",e.getMessage() );
}
}
System.out.println("檔案項name的值"+multipartFile.getName());
System.out.println("檔案名稱"+multipartFile.getOriginalFilename());
}
return "success";
}
攔截器
SpringMVC提供了攔截器機制,允許運作目标方法之前進行一些攔截工作,或者目标方法運作之後進行一些其他處理;
SpringMVC中的攔截器是一個接口
HandlerInterceptor
preHandle:在目标方法運作之前調用;傳回 boolean ,如果是true 意味着放行,否則不放行。
postHandle:在目标方法運作之後調用;
afterCompletion:整個請求(來到頁面之後、chain.doFilter()放行、資源響應之後)完成後調用;
要實作攔截器就要實作該接口:
1、編寫攔截器(實作
HandlerInterceptor
接口)
public class InterceptorController implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle......");
// 傳回true則不攔截
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle......");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion......");
}
}
2、配置編寫的攔截器
<!-- 配置攔截器 -->
<mvc:interceptors>
<!-- 攔截所有請求 -->
<bean class="com.atcpl.controller.InterceptorController"></bean>
<!-- 自定義攔截請求 -->
<!--<mvc:interceptor>
<mvc:mapping path="/demo01"/>
<bean class="com.atcpl.controller.InterceptorController"></bean>
</mvc:interceptor>-->
</mvc:interceptors>
攔截器運作流程:
攔截器源碼分析:
以DeBug模式調試代碼,在執行到控制台列印出東西位置,就是要調試的源碼。如下:
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 {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
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 (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// 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) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
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);
}
}
}
在擷取處理器時可以看到有兩個攔截器(最後一個是自定義的)
這句話是執行目标方法,在這之前進行了判斷,判斷攔截器的傳回結果。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
進入
preHandle()
方法細看:
方法注釋:
應用注冊攔截器的preHandle方法。@return {@code true}如果執行鍊應該繼續下一個攔截器或處理程式本身。否則,DispatcherServlet假定這個攔截器已經處理了響應本身。
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
//拿到所有的攔截器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//順序周遊
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
//調用攔截器的preHandle方法 ,如果傳回false (即!false為true)
//執行攔截器的afterCompletion ,然後再傳回一個false
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
// 定義一個索引,用來記錄哪些攔截器已經放行了
this.interceptorIndex = i;
}
}
return true;
}
執行完上邊的方法會有一個傳回值,即
//上邊的方法傳回的是false,(!false即為true) 目标方法及以後的流程都不會執行,直接執行afterCompletion
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
如果傳回值是true,也就是目标方法正常執行完後會執行
mappedHandler.applyPostHandle()
方法,如果目标方法異常,會跳過
mappedHandler.applyPostHandle()
且被兩個catch捕獲,然後來到頁面渲染,這也映證了上邊說的攔截器的運作流程。
再來看
postHandle()
方法:
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
// 拿到所有的攔截器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
// 倒序周遊
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
運作期間有任何異常都會被捕獲,執行完 postHandle() 就會執行頁面渲染方法。
細看
processDispatchResult()
方法:
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);
}
}
// 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.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
render();
頁面渲染方法執行完,如果沒有異常則會執行afterCompletion()方法。
當然,如果有異常的話,也會被捕獲,所有說隻要目标方法正常執行,那麼afterCompletion總會執行,這也映證了上邊的攔截器的運作流程。
然後我們再來細看
afterCompletion()
方法:
方法注釋:
在映射的HandlerInterceptors上觸發completion後回調。将隻調用所有攔截器的afterCompletion,其預處理句柄調用已成功完成并傳回true。
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
//拿到所有處理器
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
//倒叙周遊
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
已經放行的攔截器的afterCompletion就執行,那麼他是如何知道那個攔截器已經放行了呢?
在這個倒叙周遊中,interceptorIndex就是代表着已經放行的攔截器。
這個interceptorIndex就是
preHandle()
方法中周遊時儲存已放行的攔截器的索引。從最後一個放行的攔截器開始,把該攔截器之前已經放行了的攔截器的afterCompletion方法都執行了。
異常處理
SpringMVC通過HandlerExceptionResolver處理程式的異常,包括Handler映射、資料綁定以及目标方法執行時發生的異常。
SpringMVC 提供的 HandlerExceptionReslover 的實作類如下:
下面來分析源碼:
先來看
initHandlerExceptionResolvers()
:
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// 在ApplicationContext中找到所有HandlerExceptionResolvers,包括祖先上下文。
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.
}
}
// 如果沒有找到其他解析器,請注冊預設的HandlerExceptionResolvers,
// 以確定我們至少有一些HandlerExceptionResolvers。
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");
}
}
}
預設的政策:(來到spring-webmvc.jar/DispatcherServlet.properties中)
可以看到 HandlerExceptionResolver 的預設值:
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
那麼當出現異常時,程式是如何做的呢?
DeBug模式調試代碼,如下圖中标注。
進入到processDispatchResult方法内:
繼續細看異常處理方法
processHandlerException()
:
這三個異常解析器輪流嘗試解析。如果這三個異常解析器都不能處理,就把異常抛出去。
而這三個異常都有對應使用環境:
- ExceptionHandlerExceptionResolver:@ExceptionHandler
- ResponseStatusExceptionResolver:@ResponseStatus
- DefaultHandlerExceptionResolver:判斷是否SpringMVC自帶的異常
第一種解析器(ExceptionHandlerExceptionResolver)所使用的場景:@ExceptionHandler
/**
* 告訴SpringMVC該方法專門處理異常的
* @return
*/
@ExceptionHandler(Exception.class)
public String customizeExceptionDemo(){
return "customizeError";
}
如果想要攜帶異常資訊可以在方發處加上參數 Exception ,SpringMVC隻認這一個參數,不能在參數位置寫Model、Map之類的參數。那麼還想将異常資訊攜帶導頁面可以将方法的傳回類型設定為 ModelAndView。
@ExceptionHandler(Exception.class)
public ModelAndView customizeExceptionDemo(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
結果如下圖:
如果有兩個或者多個這種處理異常的方法,如下圖,出現異常時精确優先。
抽取異常處理方法:
自定義一個專門處理異常的類 CustomizeException
/**
* 自定義專門處理異常的類
* 把該類加入到容器中使用的注解是 {@ControllerAdvice}而不是那四個
*/
@ControllerAdvice
public class CustomizeException {
@ExceptionHandler(Exception.class)
public ModelAndView customizeExceptionDemo(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
@ExceptionHandler(value = {ArithmeticException.class})
public ModelAndView customizeArithmeticException(Exception e){
ModelAndView mv = new ModelAndView("customizeError");
mv.addObject("exceptionMsg", e);
return mv;
}
}
那麼問題來了,我這個類中有異常處理的方法,而我某個具體類中也有異常處理的方法,如下兩張圖,當
exceptionDemo()
方法發生異常時,是具體類(ExceptionController)中的異常處理方法執行呢,還是專門處理異常的類(CustomizeException)中的異常處理方法執行呢?
答案是:具體類中的方法出現異常,具體類中的異常處理方法優先執行。也就是全局異常處理與本類異常處理同僚存在時,本類優先且具體異常優先。
第二類異常處了解析器使用場景:@ResponseStatus
@RequestMapping("/loginDemo")
public String loginDemo(@RequestParam("username") String param, Model model){
// 使用者不存在抛出自定義異常
if(!"admin".equals(param)){
model.addAttribute("msg","使用者名不存在" );
throw new UserNotFoundException();
}
return "success";
}
發送一個請求,且請求帶參數
username
,假設username不等admin,抛出自定義的異常;
//自定義異常
@ResponseStatus(reason = "使用者不存在",value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException{
}
第三類異常異常處了解析器(DefaultHandlerExceptionResolver)使用場景:SpringMVC自己的異常
for循環周遊三個異常解析器,當目前解析器是 DefaultHandlerExceptionResolver 進入resolverException方法,如下圖:
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
可以看到該方法中有很多判斷,判斷目前異常屬于哪一種,然後進行相應的處理。
處理以上三個異常解析器還有一個SimpleMappingExceptionResolver
他是通過配置的方式來進行異常處理。一般不常用,不在多做解釋。
總結:
r);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn(“Failure while trying to resolve exception [” + ex.getClass().getName() + “]”, handlerEx);
}
}
return null;
}
可以看到該方法中有很多判斷,判斷目前異常屬于哪一種,然後進行相應的處理。
處理以上三個異常解析器還有一個**SimpleMappingExceptionResolver**
[外鍊圖檔轉存中...(img-wSapeRTv-1665832263248)]
他是通過配置的方式來進行異常處理。一般不常用,不在多做解釋。
------
### 總結:
[外鍊圖檔轉存中...(img-WFSphuaj-1665832263250)]