SpringMVC源碼分析

強大的DispatcherServlet

還記得在web.xml中配置的DispatcherServlet嗎?其實那個就是SpringMVC框架的入口,這也是struts2和springmvc不同點之一沉颂,struts2是通過filter的锈颗,而springmvc是通過servlet的齐苛】缂危看下servlet的結構圖

結構圖

類圖

從上面這張圖很明顯可以看出DispatcherServlet和Servlet以及Spring的關系尊蚁。而我們今天的重點就從DispatchServlet說起亡笑。

在分析之前我用SpringBoot搭建了一個很簡單的后臺項目,用于分析横朋。代碼如下

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
@AllArgsConstructor
public class User {

private Integer id;

private String name;

private Integer age;

private String address;

public User() {

}
}

/**
* @author generalthink
*/
@RestController
@RequestMapping("/user")
public class UserController {

@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public User getUser(HttpServletRequest request,@PathVariable Integer id) {

//創(chuàng)建一個user,不走數據庫只是為了分析springmvc源碼
User user = User.builder()
.id(id)
.age(ThreadLocalRandom.current().nextInt(30))
.name("zzz" + id)
.address("成都市").build();

return user;
}

@RequestMapping(value = "/condition",method = RequestMethod.GET)
public User getByNameOrAge(@RequestParam String name,@RequestParam Integer age) {
User user = User.builder().name(name).age(age).address("成都市").id(2).build();
return user;
}

@PostMapping
public Integer saveUser(@RequestBody User user) {

Integer id = user.getName().hashCode() - user.getAge().hashCode();

return id > 0 ? id : -id;
}

}

這里為了方便調試把關注點更多集中在SpringMVC源碼中,所以這里的數據都是偽造的仑乌。而且這里的關注點也集中到使用注解的Controller(org.springframework.stereotype.Controller),而不是Controller接口(org.springframework.web.servlet.mvc.Controller),這兩者的區(qū)別主要在意一個只用標注注解琴锭,一個需要實現接口晰甚,但是它們都能完成處理請求的基本功能。我們都知道訪問servlet的時候默認是訪問service方法的决帖,所以我們將斷點打在HttpServlet的service方法中厕九,此時查看整個調用棧如下

調用棧

從這里我們也知道了請求時如何從servlet到了DispatcherServlet的衣吠,我們先來看一下DispatcherServlet的doDiapatch的方法邏輯资溃,這里把核心邏輯列出來了遏匆,把其他的一些非核心邏輯移除了

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        
        //注意這里放回的是HandlerExecutionChain對象
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        
            ModelAndView mv = null;
            Exception dispatchException = null;

            //檢查是否存在文件上傳
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 根據當前request獲取handler,handler中包含了請求url,以及最終定位到的controller以及controller中的方法
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 通過handler獲取對應的適配器,主要完成參數解析
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 調用Controller中的方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        
    }

可以看到核心邏輯其實非常簡單家破,首先檢查是不是multipart request酪耳,如果是則對當前的request進行一定的封裝(提取文件等)板甘,然后獲取對應的handler(保存了請求url對應的controller以及method以及一系列的Interceptor),然后在通過handler獲取到對應的handlerAdapter(參數組裝)同仆,通過它來進行最終方法的調用

解析multipart

那么是如何解析當前請求是文件上傳請求呢倚舀?這里直接進入到checkMultipart方法看看是如何解析的:

//我精簡了下代碼细睡,只提取了核心邏輯
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {        
        return this.multipartResolver.resolveMultipart(request);    
    }
    return request;
}

從這里可以看出通過multipartResolver判斷當前請求是否是文件上傳請求,如果是則返回MultipartHttpServletRequest(繼承自HttpServletRequest).不是則返回原本request對象谷羞。
那么問題來了multipartResolver是什么時候初始化的呢?

我們在idea中可以直接將斷點定位到multipartResolver屬性上溜徙,進行請求訪問這個時候會發(fā)現斷點直接進入到了initMultipartResolver方法中湃缎,接著跟蹤整個調用棧犀填,可以發(fā)現調用關系如下:


初始化multipartResovler

圖上表明了是在初始化servlet的時候對multipartResolver進行了初始化的。

private void initMultipartResolver(ApplicationContext context) {

//從Spring中獲取id為multipartResolver的類
    this.multipartResolver = context.getBean("multipartResolver", MultipartResolver.class);
}

MultipartResolver接口有CommonsMultipartResolve以及StandardServletMultipartResolver2種實現嗓违,CommonsMultipartResolver接口是依賴于commons-upload組件實現的九巡,而 StandardServletMultipartResolver是依賴于Servlet的part(servlet3才存在)實現的.兩者判斷是否是文件上傳請求的方法isMultipart均是通過判定請求方法是否為post以及content-type頭是否包含multipart/來進行判定的。

DispatchServlet初始化了哪些內容

protected void initStrategies(ApplicationContext context) {
   initMultipartResolver(context);  //初始化multipartResolver
   initLocaleResolver(context);//初始化localeResolver
   initThemeResolver(context);//初始化themResolver
   initHandlerMappings(context);//初始化handerMappings
   initHandlerAdapters(context);//初始化handlerAdapters
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   initViewResolvers(context);//初始化試圖解析器
   initFlashMapManager(context);
}

這些初始化的內容都會在后面被逐一使用靠瞎,這里先有一個印象比庄。

根據請求獲取mapperHandler

還是進入到getHander方法中看看到底做了什么?

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
    for (HandlerMapping hm : this.handlerMappings) {
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
            return handler;
            }
        }
    }
    return null;
}

根據HandlerMapping來查看對應的handler,那么進入到initHandlerMappings方法中查看如何初始化handlerMappings


初始化handlerMappings

其中獲取默認的handlerMappings是去spring-webmvc的org.springframework.web.servlet中的DispatcherServlet.properties中查找乏盐,文件內容是這樣的

DispatcherServlet.properties

因為detechAllhanderMappings默認為true佳窑,所以會獲取到所有HanderMapping的實現類,來看看它的類圖結構是怎樣的
HandlerMapping類圖

this.handlerMappings的值

這幾個HandlerMapping的作用如下:
SimpleUrlHandlerMapping : 允許明確指定URL模式和Handler的映射關系父能,內部維護了一個urlMap來確定url和handler的關系
BeanNameUrlHandlerMapping: 指定URL和bean名稱的映射關系神凑,不常用,我們的關注點也主要集中在RequestMappingHandlerMapping

這里也基本明確了HandlerMapping的作用:幫助DispatcherServlet進行Web請求的URL到具體類的匹配,之所以稱為HandlerMapping是因為在SpringMVC中并不局限于
必須使用注解的Controller我們也可以繼承Controller接口何吝,也同樣可以使用第三方接口溉委,比如Struts2中的Action


RequestMappingHandlerMapping

接著看下getHandler的實現:

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
   if (this.handlerMappings != null) {
      for (HandlerMapping hm : this.handlerMappings) {
         HandlerExecutionChain handler = hm.getHandler(request);
         if (handler != null) {
            return handler;
         }
      }
   }
   return null;
}

返回的handler是HandlerExecutionChain,這其中包含了真實的handler以及攔擊器,可以在執(zhí)行前,執(zhí)行后爱榕,執(zhí)行完成這三個階段處理業(yè)務邏輯瓣喊。
RequestMappingHandlerMapping的getHandler的調用邏輯如下:

調用邏輯

會遍歷所有Controller的url查看是否有符合條件的match(head,url,produce,consume,method都要滿足要求),采用antMatcher的方式來進行url匹配黔酥,如果匹配上了則返回對應的handler藻三,否則返回null,如果映射發(fā)現有重復的映射(url映射相同,請求方法相同跪者,參數相同棵帽,請求頭相同,consume相同渣玲,produce相同逗概,自定義參數相同),則會拋出異常忘衍。

而SimpleUrlHandlerMapping的調用邏輯如下:


SimpleUrlHandlerMapping調用邏輯

其中維護了url到handler的映射逾苫,先通過url到urlMap中找對應的handler,如果沒有找到則嘗試pattenMatch枚钓,成功則返回對應的handler,未匹配則返回null铅搓。

會發(fā)現處理HandlerMapping這里運用了模板方法,在抽象類中定義好了業(yè)務邏輯秘噪,具體實現只需要實現自己的業(yè)務邏輯即可。同時也符合開閉原則勉耀,完全是面向接口編程指煎,不得不讓人嘆服這里的涉及邏輯蹋偏。

分析到這里的時候我們會發(fā)現我們之前定義的Controller明顯是符合RequestMappingHandlerMapping的策略的,所以返回的HandlerExecutionChain已經包含了需要訪問的方法的全路徑了至壤。

關于HandlerAdapter

HandlerMapping會通過HandlerExecutionChain返回一個Object類型的Handler對象威始,用于Web請求處理,這里的Handler并沒有限制具體是什么類型像街,一般來說任何類型的Handler都可以在
SpringMVC中使用黎棠,只要它是用于處理Web請求的處理對象就行。

不過對于DispatcherServlet來說就存在問題了镰绎,它無法判斷到底使用的是什么類型的Handler脓斩,也無法知道是調用Handler的哪個方法來處理請求,為了以同意的方式來調用各種類型的Handler,
DispatcherServlet將不同Handler的調用職責轉交給了一個成為HandlerAdapter的角色畴栖。

先看一下HandlerAdpter接口的定義

public interface HandlerAdapter {

boolean supports(Object handler);


@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;


long getLastModified(HttpServletRequest request, Object handler);
}

主要關注supports和handle方法随静。先看下DispatcherServlethandlerAdapters的初始化過程,和handlerMappings的初始化過程是類似的

初始化HandlerAdapters

接著在看一下HandlerAdapter的類關系
HandlerAdapter類圖

同樣的吗讶,仍然通過合適的策略尋找對應的Adapter燎猛,我們主要關注的是RequestMappingHandlerAdapter(其他的用得很少),所以這里就主要講解它照皆。查看它support的實現代碼:
supports方法

上面關于handler的說明中說了其實Object handler實際上是HandlerMethod重绷,所以這里對應的HandlerAdapter就是RequestMappingHandlerAdapter

找到對應的適配器之后膜毁,這個時候就可以調用真正的邏輯了昭卓。在這之前使用者可以通過攔截器做一些事兒,比如記錄日志爽茴,打印執(zhí)行時間等葬凳,所以如果想要在執(zhí)行的方法之前添加一條語句,我們只需要配置自己的攔擊器即可室奏。


執(zhí)行攔截器方法

接下來我們重點分析handle方法火焰,看看它到底會做什么?胧沫,先看一下handle方法的執(zhí)行流程昌简,同樣的adapter同樣使用了模板方法,先在父類里面定義流程绒怨,子類只需要實現邏輯即可纯赎,所以這里首先會調用AbstracthandlerMethodAdapter的invokeHadlerMethod方法,其中對HandlerMethod進行了封裝南蹂。


invokeHandle

invokeAndHandle

我們進入到第一步犬金,看看invokeForRequest方法中主要做了什么
invokeForRequest

發(fā)現這個方法的調用邏輯實際上很簡單,就是解析參數,然后調用方法晚顷。我們來看一下如何進行參數解析的呢峰伙?

參數解析

可以看到幾乎所有的核心邏輯都集中到了argumentResovlers中去,那么支持的arguementResolver有哪些该默?又是在哪里初始化的呢瞳氓?

首先需要定位到這個屬性是從哪里過來的,RequestMappingHandlerAdapter實現了InitializingBean栓袖,所以在初始化的時候會執(zhí)行afterPropertiesSet方法匣摘,在這其中對arguementResolvers以及returnValueHandlers進行了初始化。
不同的resovler支持的參數解析不一樣裹刮,比如說有支持HttpServletRequest注入的音榜,有支持HttpServletREsponse注入的還有支持body體注入的等等。

arguementResovler初始化

returnValueHandlers初始化

經過參數解析之后就得到了反射需要的數據了,class,method以及參數必指,最后通過java的反射api調用即可囊咏。


反射調用真實方法

至此,springmvc的整個調用流程基本就清晰了塔橡。
但是到了這里問題仍然沒有結束梅割,因為我們還不知道參數具體是如何解析的。比如get方式提交的數據葛家?post方式提交的數據户辞?如何轉換成對象的?這寫問題都還存在癞谒,那我們繼續(xù)研究底燎。
這里我使用postman工具來發(fā)起請求,首先訪問 Get http://localhost:8080/user/condition?name=zhangsan&age=25,定位到resolveArgument方法

如何獲取具體的arguementResolver

接著又執(zhí)行revolver.resolveArgument方法弹砚,同樣的這里還是使用的模板方法双仍,在抽象類AbstractNamedValueMethodArgumentResolver中定義流程,各個子類只需要實現自己的邏輯即可桌吃。RequestParamMethodArgumentResolver的參數就是通過request.getParameter來獲取到的朱沃。獲取到了參數之后就執(zhí)行反射調用,這個時候就執(zhí)行了我們寫的UserController的對應方法茅诱,獲取到了User對象逗物,接下來就是處理返回值了,通過returnValueHandlers進行處理

處理返回值

handler會根據返回的類型對數據進行處理瑟俭,比如說這里就通過response向請求方輸出數據翎卓,輸出數據也是通過messageConverter來實現的


處理數據輸出

最后獲取ModalAndView對象,但是這里由于沒有modalAndView所以返回的null.最后在DispatcherServlet的processDispatchResult方法的調用邏輯如下


最后的處理

么對于這樣的請求又時如何解析的呢摆寄?

@PostMapping
public Integer saveUser(@RequestBody User user) {

Integer id = user.getName().hashCode() - user.getAge().hashCode();

return id > 0 ? id : -id;
}

同樣我們聚焦在解析參數的時候失暴,在上一個get請求的示例中我說了會先訪問AbstractNamedValueMethodArgumentResolver坯门,但是在處理@RequestBody的參數中它使用的是RequestResponseBodyMethodProcessor,它復寫了resolveArgument方法。所以不會去執(zhí)行父類的邏輯逗扒。

參數解析

這里最后會定位到jakson的objectMapper中田盈, 在spring boot中,默認使用Jackson來實現java對象到json格式的序列化與反序列化缴阎。當然是可以配置messageConvert的,只需要實現Spring的HttpMessageConverter即可简软。

源碼分析到這里就結束了蛮拔,當然其中還存在一些沒有講的地方,比如View的渲染呀痹升,一般視圖是多種多樣的建炫,又html,xml,jsp等等,所以springmvc也提供了接口供用戶選擇自己需要的模板疼蛾,只需要實現ViewResolver接口即可肛跌。還有關于Theme,MessageResource,Exception的處理等等察郁,如果鋪開來講篇幅實在是太長了衍慎,我更相信掌握了核心流程看其他的處理就會很簡單了,所以這里也就不對其他枝節(jié)內容做分析了皮钠。

一圖勝千言

SpringMVC流程圖
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末稳捆,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子麦轰,更是在濱河造成了極大的恐慌乔夯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件款侵,死亡現場離奇詭異末荐,居然都是意外死亡,警方通過查閱死者的電腦和手機新锈,發(fā)現死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門甲脏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人壕鹉,你說我怎么就攤上這事剃幌。” “怎么了晾浴?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵负乡,是天一觀的道長。 經常有香客問我脊凰,道長抖棘,這世上最難降的妖魔是什么茂腥? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮切省,結果婚禮上最岗,老公的妹妹穿的比我還像新娘。我一直安慰自己朝捆,他們只是感情好般渡,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著芙盘,像睡著了一般驯用。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上儒老,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天蝴乔,我揣著相機與錄音,去河邊找鬼驮樊。 笑死薇正,一個胖子當著我的面吹牛,可吹牛的內容都是我干的囚衔。 我是一名探鬼主播挖腰,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼练湿!你這毒婦竟也來了曙聂?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鞠鲜,失蹤者是張志新(化名)和其女友劉穎宁脊,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體贤姆,經...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡榆苞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了霞捡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坐漏。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖碧信,靈堂內的尸體忽然破棺而出赊琳,到底是詐尸還是另有隱情,我是刑警寧澤砰碴,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布躏筏,位于F島的核電站,受9級特大地震影響呈枉,放射性物質發(fā)生泄漏趁尼。R本人自食惡果不足惜埃碱,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酥泞。 院中可真熱鬧砚殿,春花似錦、人聲如沸芝囤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悯姊。三九已至名党,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挠轴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工耳幢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留岸晦,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓睛藻,卻偏偏與公主長得像启上,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子店印,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容