一、handlerAdapter類
1、handlerAdapter簡介
在前面我們已經分析了通過HandlerMapping(處理器映射器)将請求映射到了對應的Handler上,下面就需要考慮如何解析并執行該handler對象,這裡HandlerAdapter(處理器擴充卡)就登場了,我直接解析調用handler不就行了為啥還要使用HandlerAdapter包裝一層呢?在之前的分析中我們了解到了兩種類型的handler對象。
(1)以實作了Controller接口的Handler類
public class DemoController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("進入DemoController方法執行處理邏輯");
return new ModelAndView("demo");
}
}
(2)以@RequestMapping注解修飾的HandlerMethod對象
@RequestMapping(value = "/book/ListPage",method = RequestMethod.POST)
@ResponseBody
public String getBookPage(@RequestBody BookQuery bookQuery){
return success(pageTotal(pageInfo));
}
(3)其他類型Handler對象
其他還有實作Servlet的執行個體,HandlerFunction執行個體、HttpRequestHandler執行個體等,不同的執行個體對象調用時走不同的方法,為了能将不同的方法轉換成統一的調用形式,這裡使用了擴充卡模式,将各個執行個體的方法調用包裝到HandlerAdapter統一調用。
2、HandlerAdapter的體系結構
HandlerAdapter是一個頂層接口,其提供如下三個方法
supports(Object handler) | 該方法判斷該擴充卡類是否支援解析調用Handler對象,因為Adapter被設計成了一個隻适配執行某一種類型比如Controller接口類型或@RequestMapping類型的Handler需要使用SimpleControllerHandlerAdapter來進行處理,該方法在執行handler()方法之前需要使用該方法來判斷是否支援調用這種類型的Handler對象。 |
handle(req,rep,handler) | 解析并調用Handler對象的方法執行業務邏輯,從request請求中擷取參數,執行handler并将響應結果放入reponse對象 |
getLastModified | 擷取請求的資源的最終修改時間,如果請求的資源未被修改則直接使用浏覽器緩存,這個方法主要是針對http請求為get/head請求為提高性能的緩存處理。 |
通過HandlerAdapter的UML類圖我們可以看到該接口的繼承關系比較簡單,其提供了若幹個分别對應上述不同Handler執行個體的擴充卡對象實作,由于@RequestMapping修飾的方法一個普通的方法是以比較複雜這裡提供了一AbstractHandlerMethodAdapter簡單抽象和繼承前者的RequestMappingHandlerAdapter子類來處@RequestMapping修飾的普通方法的調用。
類執行個體 | 功能描述 |
SimpleControllerHandlerAdapter | 用于處理實作Controller接口的實作類,調用其handleRequest方法處理請求 |
HandlerFunctionAdapter | 用于處理實作HandlerFunction接口的實作類,調用其handle方法處理請求 |
HttpRequestHandlerAdapter | 用于處理實作HttpRequestHandler接口的實作類,調用其handleRequest方法處理請求 |
SimpleServletHandlerAdapter | 用于處理實作Servlet接口的實作類,調用其service方法處理請求 |
AbstractHandlerMethodAdapter | 簡單對HandlerAdappter進行簡單實作,并通過重寫方法讓子類按需擴充 |
RequestMappingHandlerAdapter | 用于處理使用@RequestMapping注解修飾的方法。 |
在上面的擴充卡子類中其他的實作邏輯都很簡單實作形式比較固定,隻要調用固定的方法即可完成執行。但是對于處理@RequestMapping注解修飾的方法調用的RequestMappingHandlerAdapter類就比較複雜了,因為@RequestMapping修飾的是一個普通方法,對于方法調用可以直接使用反射,但是對于方法的有無入參,入參的個數以及類型的解析這個是一個難點同時該類也是我們常用的處理器映射器元件,是以我們下面會着重講解與@RequestMapping注解修飾的普通公有方法調用相關的處理器擴充卡類AbstractHandlerMethodAdapter以及其子類RequestHandlerMethodMappingAdapter。
二、AbstractHandlerMethodAdapter
1、AbstractHandlerMethodAdapter類
根據類的繼承關系RequestMappingHandlerAdapter繼承了AbstractHandlerMethodAdapter類,AbstractHandlerMethodAdapter類對核心HandlerAdapter接口三個方法進行了重寫,并提供了相應的XXXInternal()方法供子類重寫,抽象類同時也實作了Order接口(有順序),Applicationaware接口(持有Application對象)、ServletAware(持有ServletContext對象)、繼承了WebContentGenerator類(提供了http請求的相關參數GET/POST等等)。
1.1、Supports()方法
子類重寫supportsInternal()方法 傳回true,該方法邏輯滿足是使用@RequestMapping修飾的普通方法轉換的HandlerMethod對象才可以使用該Adapter進行處理
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
1.2、getLastModified()方法
完全交由子類重寫getLastModifiedInternal()方法,子類重寫傳回固定值-1,表明無最後修改時間,每次請求都重新擷取資料。
1.3、handle()方法
上面的兩個方法比較簡單其核心的方法應該是如何針對查找的到Handler對象處理web請求對象并傳回參數的handler()方法,該方法涉及參數解析,執行、封裝傳回參數三大步驟,比較複雜,我們這裡使用單獨的章節來說明。、
/**
* Adapter的核心方法 主要是将請求中的參數解析出來,調用其中的Handler方法,并将其執行結果
* 包裝成ModelAndView 傳回。
* @param request web請求對象
* @param response web響應對象
* @param handler 業務處理對象類
* @return 包含資料和視圖的對象類型
*/
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler){
}
通過方法簽名,可以分析出來該方法主要的處理邏輯分成三個部分
- 從request中解析出來相關的請求參數
- 根據請求參數調用相關的執行方法(handler) 處理業務邏輯,
- 針對@ReponseBody等對相關的執行方法的傳回值進行相關處理(序列化等等)設定到MdodelAndView中
三、RequestMappingHandlerAdapter類
該類是我們日常開發中使用的核心類,其對抽象Adapter的暴露出來的supportsInternal、handleInternal、getLastModifiedInternal進行重寫,其中supportsInternal傳回值固定為true結合父類該類隻對使用@RequestMapping修飾的HandlerMethod對象進行處理,同時getLastModifiedInternal傳回-1 則說明springMVC對于GET/HEAD請求不走使用本地緩存。排除掉這兩個簡單的方法我們需要重點關注的便是對應處理handler對象的handleInternal方法。在詳細分析該方法之前我們先了解一下與之相關的知識補充。
1、知識補充
1.1、@InitBinder的使用
使用該注解修飾的方法會在每一個web請求到達Controller的@RequestMapping修飾的方法之前先執行該注解修飾的方法,如果在某個Controller中聲明則該方法作用範圍僅限于該Controller類,
如果想要使其全局起作用則需要搭配@ControllerAdvice修飾的類,即在使用@ControllerAdvice注解修飾的類中再使用@InitBinder注解修飾的方法則該方法是全局的,該項目中所有web請求執行都會調用該方法。
主要的使用場景如下(上圖也進行了說明)
1、注冊屬性編輯器 針對表單送出的日期類型的轉換(預設日期類型的資料會轉換失敗)
2、注冊校驗器 對入參進行校驗
1.2、@ModelAttribute的使用
@ModelAttribute注解用于将方法的參數或方法的傳回值綁定到指定的模型屬性上,該注解修飾的方式和上面@InitBinder注解修飾的方法執行時機一樣都是在每次請求到達Controller的方法之前執行,也有全局和局部範圍,例子如下
//ModelAttribute注解的第一種方式 使用其中的value屬性作為key
//其中方法的傳回值作為value 設定到model中
@ModelAttribute("name")
public String setName(){
return "索隆";
}
//ModelAttribute注解的第二種方式 方法中含有model 則直接設定到model對象中
@ModelAttribute()
public void setModel(Model model){
Student student = new Student();
student.setName("路飛");
student.setAge(24);
model.addAttribute("student",student);
}
//ModelAttribute注解的第三種方式
//注解即沒有value的屬性值作為key,也沒有model對象
//這會通過ModleFactory根據傳回類型解析出參數名作為key,并将傳回對象作為value存放到model中
//TODO 這裡沒有起作用
@ModelAttribute
public Student setModelTwo(){
Student studentTWO = new Student();
studentTWO.setName("山治");
studentTWO.setAge(24);
return studentTWO;
}
//這裡解析參數名為string
@ModelAttribute
public String setModelThree(){
return "山治";
}
//在model中進行了設定可以在該方法中進行擷取
@RequestMapping(value = "testModelAttribute",method = RequestMethod.GET)
public String testModelAttribute(Model model,HttpSession session){
Map param = model.asMap();
logger.info("擷取modelAttribute第一種方式設定的參數:{}",param.get("name"));
logger.info("擷取modelAttribute第二種方式設定的參數:{}",((Student)param.get("student")).getName());
logger.info("擷取modelAttribute第三種方式設定的參數:{}",param.get("name"));
String dream = (String) session.getAttribute("dream");
logger.info(dream);
return "/demo";
}
1.3、ResponseBodyAdvice的使用
該接口可以處理傳回值為json格式的資料 在該資料傳回之前對資料進行處理,如下代碼所示:
/**
* 實作自定義的ReponseBodyAdvice接口并配合ControllerAdvice注解(所有處理器都适用)
* 用于将所有處理器中傳回值為json格式資料響應之前對其進行修改
*/
@ControllerAdvice
public class StudentReponseBodyAdvice implements ResponseBodyAdvice {
/**
* 用于判斷該Advice 是否其作用
* @param returnType 對應的方法包含其參數對象
* @param converterType 類型轉換對象 .MappingJackson2HttpMessageConverter
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 在請求被處理完傳回資料之前可以對資料的方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
System.out.println("TestResponseBodyAdvice==>beforeBodyWrite:" + body.toString() + ","
+ returnType);
//轉換資料
if(body instanceof Student){
Student student = (Student) body;
student.setName("文斯莫克·山治");
return student;
}
return body;
}
}
請求示例及結果:預設傳回name為“山治”,但是因為有ResponseBodyAdvice 的處理名字變為了“文斯莫克·山治”。
@ResponseBody
@RequestMapping(value = "query/{id}",method = RequestMethod.GET)
public Student queryStudent(@PathVariable("id") Integer id){
Student student = new Student();
student.setName("山治");
student.setAge(24);
return student;
}
1.4、@SessionAttributes的使用
@SessionAttributes 隻能作用在類上,作用是将指定的Model中的鍵值對添加至session中,這樣同一個會話中可以使用該對應的屬性。下面是該注解的使用示例。
@
具體是将屬性值設定到session中是依靠SessionAttributeHandler的屬性SessionAttributeStore對象來實作的,大家不要被這個類的英文名字所欺騙認為它是存儲sesion級别屬性的集合,其實該對象确切的說是操作request對象擷取session并設定屬性值的工具類。
1.5、@ControllerAdviceBean的使用
在前面的知識點的補充中我們已經了解到了該注解的用法,主要使用聲明一些全局性的行為方法,比如搭配上面的@InitBinder、@ModelAttributes使其全局有效,或者搭配@ExceptionHandler實作全局異常處理。
2、handleInternal方法分析
2.1、checkRequest()方法
請求的檢查checkRequest() 主要是支援請求方法類型,和是否必須存在session的校驗。可通過xml進行配置。
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<!-- 設定支援的方法 隻有滿足這裡列舉的請求 才會進行處理否則抛出異常-->
<property name="supportedMethods">
<set>
<value>POST</value>
</set>
</property>
<!-- 設定請求必須包含session 否則抛出異常 -->
<property name="requireSession" value="true" />
<property name="messageConverters">
<list>
<ref bean="stringHttpMessageConverter"/>
</list>
</property>
</bean>
2.2、invokeHandlerMethod()方法
第二部分是針對session的是否同步請求 調用核心方法invokeHandlerMethod,同步session與否兩者的差別在于同一個會話使用者伺服器隻能處理一個請求,其他的請求隻能等待串行執行。
2.3、響應資訊的緩存處理
第三部分則是針對請求完後後對于reponse設定相關的本地緩存的請求頭資訊,如果設定緩存請求頭資訊則浏覽器第一次請求完成後會将資源存放到本地緩存,再次請求如果緩存沒有過期則直接使用本地緩存。 RequestMappingHandlerAdapter預設不支援本地緩存 我們可以通過如下示例來實作走本地緩存
- 重寫RequestMappingHandlerAdapter 的getLastModifiedInternal使其傳回一個正數
public class MyRequestHandlerMappingAdapter extends RequestMappingHandlerAdapter {
@Override
protected long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod) {
return 100L;
}
}
- 在xml配置上面編寫的自定義擴充卡中配置cacheSecondsForSessionAttributeHandlers 緩存屬性
<bean class="com.xiu.HandlerAdapter.MyRequestHandlerMappingAdapter">
<!-- 設定緩存的秒數 該屬性好像隻适用于有sessionAttributes屬性對象存在的情況 但是他也提供了CacheControl設定緩存屬性-->
<property name="cacheSecondsForSessionAttributeHandlers" value="1000"/>
</bean>
如上步驟完成,便可以将響應結果緩存,同時再次相同的請求過來則使用浏覽器自身的緩存。
2.4、HandlerInternal()方法
下面是對處理handler請求的HandlerInternale方法進行簡要的分析,在該方法隻是對handler解析調用的處理的入口方法核心的處理落實是交由invokeHandlerMethod()方法中。下面我們來分析該方法。
/**
* 該方法是我們針對擷取到的Handler對象進行轉換解析處理的入口方法
* 主要包含請求request的相關校驗校驗
* 解析、轉換、執行相關的handler的invokeHandlerMethod 并傳回對應的資料視圖對象。
* 對應執行結束的響應對象response的設定 比如SessionAttributes資訊的緩存控制
*/
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
//如果設定supportMethods、requireSession屬性需要對其進行校驗
//如果處理的請求方式不是supportMethods集合支援的抛出異常
//或者requireSession為true但是request不包含session同樣抛出異常
checkRequest(request);
// 針對線程不安全的session 提供兩種方式 一種是同步 進而保證該會話的使用者串行執行請求
// 一種是非同步處理請求 不管同步與否最終調用核心方法invokeHandlerMethod 調用HandlerMethod
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
//為響應資源設定對應的緩存時間,指定時間内可以直接使用本地緩存
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
3、invokeHandlerMethod方法分析
/**
* 解析、轉換、執行、傳回資料視圖的核心方法
*/
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
//先将請求響應對象包裝成ServletWebRequest
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
//擷取該方法的所有使用@InitBinder注解修飾的方法
//包裝成WebDataBinderFactory 在handlerMethod執行前調用(包含類作用範圍的和全局作用範圍的)
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
//擷取該方法的所有使用@ModelAttribute注解修飾的方法 包裝成ModelFactory(包含類作用範圍的和全局作用範圍的)
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
//上面擷取@InitBinder/@ModelAttribute注解修飾的方法雷同,都是先接卸handlerMethod對應的類
//中類範圍的注解修飾的方法(解析完成後放入對應的緩存),接着擷取@ControllerAdvice修飾的全局範圍的使用如上注解修飾的方法
//将對應的方法包裝成InvocableHandlerMethod用于後面的執行,并将其包裝成WebDataBinderFactory/ModelFactory對象。
//兩者的差別在于ModelFactory中不僅僅包含@ModelAttribute注解修飾的方法還包含@SessionAttribute注解修飾的model處理
//同時也将上面解析出來的WebDataBinderFactory存放到modelFactory中。
//将HandlerMethod方法包裝成ServletInvocableHandlerMethod(該對象是InvocableHandlerMethod子類)
//設定相關的元件 比如設定參數解析元件argumentResolvers和傳回結果處理元件returnValueHandlers
//綁定元件binderFactory和parameterNameDiscoverer
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
//建立資料模型和視圖的容器對象 見名知義其包含我們所常見的資料和視圖對象
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
//對于重定向相關的參數儲存需要依賴flashMap對象,如果一個請求是重定向請求 則input_flash_map儲存重定向入參
//如果一個請求需要進行重定向 則參數會存放到output_flash_map中
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
//調用@ModelAttribute注解修飾的方法将對應的屬性存放到mavContainer 的model中
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
//異步請求處理 此處再是先省略
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
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 + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
//通過反射執行相關的handlerMethod 涉及參數解析,傳回值的處理、
//invocableMethod已經包含了進行參數解析和傳回值處理的元件對象
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
//主要處理sessionAttribute 以及對model中的屬性進行屬性編輯器的轉換處理@InitBinder
//對于view對象進行邏輯視圖到實體視圖的轉換以及重定向參數的設定
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}