SpringMVC 核心源碼分析

Spring的源碼非常復(fù)雜,想要一篇概覽幾乎不可能扛点,所以只能分而治之哥遮,本文摘寫自近期抽空翻閱SpringMVC框架的部分源碼,借此由淺入深地探索SpringMVC的核心體系結(jié)構(gòu)陵究。

在前后端分離大行其道的今日眠饮,由后端完成渲染的JSP/Freemarker時代可以說已經(jīng)逐漸被NodeJS搭建的大前端體系取代,所以本文也不去翻閱SpringMVC視圖渲染部分铜邮,僅認(rèn)為后端作為一個數(shù)據(jù)服務(wù)接口來看到仪召,簡而言之,僅分析SpringMVC與前端的JSON交互流程松蒜。

基于過往的研發(fā)經(jīng)驗(yàn)相信絕大多數(shù)都了解到扔茅,SpringMVC的核心組件為DispatcherServlet組件,而實(shí)現(xiàn)的規(guī)約則是J2EE關(guān)于Servlet的規(guī)范秸苗。如果對于Servlet規(guī)范不甚清楚召娜,請抽空自行補(bǔ)充。本篇僅討論HTTP請求的完整調(diào)用鏈路惊楼,不涉及其他協(xié)議規(guī)范說明玖瘸。

DispatcherServlet

作為一個全局Servlet秸讹,欲深究其意,需先了解DispatcherServlet容器初始化時做了什么處理雅倒?而DispatcherServlet對象初始化包括如下三部分:

  • DispatcherServlet對象靜態(tài)初始化部分璃诀?
  • 構(gòu)造函數(shù)?
  • Servlet規(guī)范中容器初始化流程屯断?

在整個容器初始化的過程文虏,只有明白其中會涉及到得我們需要去核心關(guān)注的部分,才能很好的把握住SpringMVC的核心流程殖演,從中透析核心體系氧秘。

靜態(tài)初始化部分

static {
        // Load default strategy implementations from properties file.
        // This is currently strictly internal and not meant to be customized
        // by application developers.
        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());
        }
    }

靜態(tài)初始化實(shí)際是讀取了DispatcherServlet.properties文件的內(nèi)容,其中都有些什么呢趴久?摘4.3.18.RELESE版本的文件丸相,內(nèi)容如下:

// 省略...
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
// 省略....

找一個類進(jìn)去翻閱,可以猜測它可能是要初始化這些Bean彼棍,再查閱DispatcherServlet#getDefaultStrategies() 即可確認(rèn)灭忠,當(dāng)然,這部分并非是靜態(tài)初始化就完成座硕,而是延遲至Serlvet容器初始化時分批進(jìn)行初始化弛作。

構(gòu)造函數(shù)

翻閱代碼可以發(fā)現(xiàn)內(nèi)部有兩個構(gòu)造函數(shù),那么究竟在容器初始化的時候默認(rèn)調(diào)用的是哪個华匾?

easy映琳,打個斷點(diǎn)DEBUG一下就可以,基于我的環(huán)境上我發(fā)現(xiàn)是默認(rèn)無參構(gòu)造函數(shù)蜘拉。(基于springboot 1.5.6版本)

那么是誰來調(diào)用改無參構(gòu)造函數(shù)的呢萨西?

easy,在springboot環(huán)境運(yùn)行時可以看到有一個顯眼的注解SpringBootApplication旭旭,而該注解的組成部分還包括EnableAutoConfiguration谎脯,這個注解又是要解決什么的呢?

它將默認(rèn)加載spring-boot-autoconfig下的spring.factories文件內(nèi)部的類持寄,比如:

org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\

///.....

那么查閱這些類源梭,它們做了什么?比如DispatcherServletAutoConfiguration稍味,查閱時發(fā)現(xiàn)它加了注解Configuration

查閱該注解你又將發(fā)現(xiàn)它會處理聲明@Bean的方法咸产,并將該Bean注入到Spring容器中,由容器托管其生命周期仲闽。

Servlet容器初始化

查閱javax.servlet.Servlet接口的定義,可以清晰的看到規(guī)范中定義四個方法僵朗,其中需要重點(diǎn)關(guān)注如 init, service

那么DispatcherServlet容器在初始化時做了什么赖欣?

HttpServlet#init() -> FrameworkServlet#initServletBean -> DispatchserServlet#initStrategies()

注:如果想要知道Serlevt如何工作屑彻,可以翻閱Tomcat源碼,以此看到工業(yè)級產(chǎn)品如何實(shí)現(xiàn)Servlet規(guī)范

翻閱DispatchserServlet#initStrategies()可以看到好幾個方法顶吮,方法名稱皆以init打頭社牲,那么它們就是在初始化什么?為什么在自己的程序中使用@Controller, @RequestMapping, @ResponseBody就可以跟前端完成常見的JSON交互了呢悴了?

為了完成非常復(fù)雜的功能交互搏恤,SpringMVC定義了非常多的頂級接口,而我們核心要關(guān)注的無非是HandlerMapping, HandlerAdpater

HandlerMapping

查閱DispatcherServlet#initHandlerMappings()方法時發(fā)現(xiàn)一個問題湃交,那就是它并非依據(jù)前文中提到的DispatcherServlet.properties中加載出來的對象熟空,而是從ApplicationContext中取

Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);

那么取出了什么Bean呢?

image.png

這些Bean從而何來搞莺?前文說過息罗,本文基于springboot 1.5.6版本,程序啟動加載spring-boot-autoconfig下的spring.factories文件才沧,其中比如org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration

由于我們實(shí)際編碼過程經(jīng)常使用@RequestMapping來完成URL路徑標(biāo)記迈喉,那么先從與之有些類似的RequestMappingHandlerMapping中翻閱代碼,看看初始化時核心放了什么温圆?

public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
        mapping.setOrder(0);
        mapping.setInterceptors(getInterceptors());
        mapping.setContentNegotiationManager(mvcContentNegotiationManager());
        mapping.setCorsConfigurations(getCorsConfigurations());

        PathMatchConfigurer configurer = getPathMatchConfigurer();
        if (configurer.isUseSuffixPatternMatch() != null) {
      // 比如這個挨摸,很多開發(fā)者應(yīng)該都記得以前URL一般都有后綴,比如.action, .do, .ac等等
            mapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
        }
        if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
            mapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
        }
        if (configurer.isUseTrailingSlashMatch() != null) {
            mapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
        }
        UrlPathHelper pathHelper = configurer.getUrlPathHelper();
        if (pathHelper != null) {
            mapping.setUrlPathHelper(pathHelper);
        }
        PathMatcher pathMatcher = configurer.getPathMatcher();
        if (pathMatcher != null) {
            mapping.setPathMatcher(pathMatcher);
        }

        return mapping;
    }

單單查閱內(nèi)部方法岁歉,類的命名大致可以猜測出一些信息來得运,不過最終的使用還是要通過其他核心流程來分析。

HandlerAdpater

同理的方式可以看到核心的關(guān)注類

image.png

Servlet容器工作流程

DispatcherServlet#doDispatch()刨裆,可以認(rèn)為是Servlet規(guī)范中的service()語義澈圈。

截取核心代碼如下,并且將會按照入口核心源碼來分析

HandlerExecutionChain mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

getHandler(processedRequest)

/**
     * Return the HandlerExecutionChain for this request.
     * <p>Tries all handler mappings in order.
     * @param request current HTTP request
     * @return the HandlerExecutionChain, or {@code null} if no handler could be found
     */
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        for (HandlerMapping hm : this.handlerMappings) {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
            }
            HandlerExecutionChain handler = hm.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
        return null;
    }

方法語義其實(shí)很簡單帆啃,從注入的一堆HandlerMapping中查找合適的一個并返回瞬女。那么什么是合適的?查閱HandlerMapping中對于getHandler()方法的注解:

Return a handler and any interceptors for this request. The choice may be made
on request URL, session state, or any factor the implementing class chooses.

HandlerExecutionChain包含著真正的HandlerMapping努潘, 以及一系列的攔截器诽偷。通過DEBUG可以發(fā)現(xiàn)最終找到的對象為RequestMappingHandlerMapping,怎么匹配的呢疯坤?通過結(jié)果來反向推導(dǎo)原因报慕。Spring框架中大量使用了模板方法,所以看類實(shí)現(xiàn)過程要關(guān)注子類與父類之間的關(guān)系压怠。

RequestMappingHandlerMapping
類體系

先查閱整個類結(jié)構(gòu)體系眠冈,相當(dāng)復(fù)雜,不過不要緊,看關(guān)鍵點(diǎn)蜗顽。

HandlerMapping <-- AbstractHandlerMapping <-- AbstractHandlerMethodMapping <-- RequestMappingInfoHandlerMapping <--RequestMappingHandlerMapping

初始化流程

RequestMappingHandlerMapping作為一個由Spring托管的Bean布卡,在它初始化的生命周期中,有非常關(guān)鍵的一步雇盖,即afterPropertiesSet方法忿等,跟蹤上去,最終你會看到如下幾個核心的方法:

//初始化HandlerMethods
AbstractHandlerMethodMapping#initHandlerMethods
// 判斷這個類是否有@Controller 或者@RequestMapping
RequestMappingHandlerMapping#isHandler()
// 查找hanlder內(nèi)部方法
AbstractHandlerMethodMapping#detectHandlerMethods()
// 找到handler內(nèi)部帶有標(biāo)記的@RequestMapping的方法崔挖,包裝成RequestMappingInfo
RequestMappingHandlerMapping#getMappingForMethod()
// 找到一個可以被調(diào)用的目標(biāo)方法Method
AopUtils.selectInvocableMethod(entry.getKey(), userType)
// 將相信的handler贸街,method等信息注冊到MappingRegistry中,注冊前會將信息包裝成HandlerMethod
AbstractHandlerMethodMapping#registerHandlerMethod(handler, method, RequestMappingInfo)
// 注冊狸相,MappingRegistry有非常核心的Map薛匪,比如urlLookup
MappingRegistry#register(RequestMappingInfo, handler, Method)
最終的HandlerMethod包括了什么信息?

this.bean = bean;
this.beanType = ClassUtils.getUserClass(bean);
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();

注:這些信息最終可以通過反射來完成方法的調(diào)用卷哩。

在完成初始化之后蛋辈,當(dāng)進(jìn)來時,如何完成URLController中的Method的對應(yīng)呢将谊?

映射

AbstractHandlerMethodMapping#getHandlerInternal()
AbstractHandlerMethodMapping#lookupHandlerMethod()
MappingRegistry#getMappingsByUrl()
當(dāng)完成初始化之后冷溶,通過請求的URL找到對應(yīng)的HandlerMethod其實(shí)非常簡單.

getHandlerAdapter()

通過RequestMappingHandlerMapping來找Adapter,先來看HandlerAdapter接口的定義:MVC framework SPI, allowing parameterization of the core MVC workflow.

它的核心在于尊浓,找到RequestMappingHandlerMapping來完成從請求到執(zhí)行到響應(yīng)的完整工作流程逞频。

通過DEBUG可以非常快速定外找栋齿,需要關(guān)注的核心實(shí)現(xiàn)為RequestMappingHandlerAdapter.通過其父類實(shí)現(xiàn)的support()其實(shí)也可以非趁缯停快速確定,因?yàn)樗С值?strong>Handler類型為HandlerMethod.

handler()

RequestMappingHandlerAdapter

可以非常明顯看到這個類內(nèi)部包含著一系列的HttpMessageConverter瓦堵,這些對象可以解決什么問題呢基协?

方法調(diào)用

RequestMappingHandlerAdapter#invokeHandlerMethod()
ServletInvocableHandlerMethod#invokeAndHandle()
InvocableHandlerMethod#invokeForRequest()
InvocableHandlerMethod#doInvoke()
// 最終方法執(zhí)行
Method#invoke()

也就是說,HTTP請求進(jìn)入SpringMVC應(yīng)用之后菇用,匹配到HandlerMethod澜驮,最終通過HandlerMethod中包含的Method對象,而Method通過Java反射即可完成方法調(diào)用惋鸥。

完成調(diào)用的過程分兩部分:

  • 從HTTP中解析參數(shù)
  • 將返回值進(jìn)行處理(比如聲明@ResponseBody
參數(shù)解析

參數(shù)的解析以及映射是一件相當(dāng)復(fù)雜的工作杂穷,比如

@ResponseBody
@RequestMapping("data")
fun userData(@RequestParam TransferObject json) : UserData {
    return UserData(...);
}

在一個完整的HTTP請求中,核心參數(shù)可能在URL上卦绣,也可以在Body上耐量,當(dāng)要完成參數(shù)解析時,需要從中取出參數(shù)滤港,并按照方法所需入?yún)⑦M(jìn)行封裝廊蜒,最后入?yún)⑼瓿煞椒ㄕ{(diào)用,欲深究請關(guān)注如下方法

InvocableHandlerMethod#getMethodArgumentValues()

返回值處理

返回值可能會有各式各樣的結(jié)果,一般情況下可能是一個JSON對象劲藐,通過DEBUG可以看到核心類RequestResponseBodyMethodProcessor

public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class));
    }

從HTTP協(xié)議來看八堡,在入?yún)r就已經(jīng)明確了響應(yīng)體的類型,核心參數(shù)為:accept聘芜,常見比如application/json, text/html缝龄, 等等汰现。SpringMVC如何處理這個問題呢?請聚焦于MediaType

AbstractMessageConverterMethodProcessor#writeWithMessageConverters()

// 從http請求中取出來后進(jìn)行解析
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
// 根據(jù)返回值判斷何時的MediaType
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

比如上請求叔壤,根據(jù)返回值判斷即為JSON瞎饲,故而結(jié)果如下圖:

image.png

MediaType將決定以何中HttpMessageConvert來完成對象的轉(zhuǎn)換處理,以及流輸出炼绘。

messageConverter.canWrite(declaredType, valueType, selectedMediaType)
messageConverter.write(outputValue, declaredType, selectedMediaType, outputMessage)

而判斷的核心在于實(shí)現(xiàn)類的supportedMediaTypes參數(shù)值嗅战,比如:

public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
鉤子函數(shù)

在將返回值寫入到輸出流中前,提供了鉤子函數(shù)RequestResponseBodyAdviceChain

processDispatchResult()

完成請求的后置處理俺亮。

比如mappedHandler.triggerAfterCompletion(request, response, null)

其實(shí)是將所有的后置攔截器完成調(diào)用驮捍。

思考

看完了完整的流程后,如果需要對SpringMVC做一些優(yōu)化脚曾,需要如何东且?

優(yōu)化的層面往往是往下往上兩個層面。

  • 往下從使用層面去考慮本讥,選用更適合SpringMVC框架的用法珊泳,比如URL匹配規(guī)則簡單化
  • 往上從優(yōu)化層面去考慮,比如減少不必要的Bean創(chuàng)建拷沸,簡化從HandlerMap, HandlerAdapter等的尋找鏈路色查,加快容器初始化,提高調(diào)用鏈路效率撞芍,等等
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秧了,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子勤庐,更是在濱河造成了極大的恐慌示惊,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愉镰,死亡現(xiàn)場離奇詭異米罚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)丈探,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門录择,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事隘竭√燎兀” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵动看,是天一觀的道長尊剔。 經(jīng)常有香客問我,道長菱皆,這世上最難降的妖魔是什么须误? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮仇轻,結(jié)果婚禮上京痢,老公的妹妹穿的比我還像新娘。我一直安慰自己篷店,他們只是感情好祭椰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疲陕,像睡著了一般方淤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸭轮,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天臣淤,我揣著相機(jī)與錄音,去河邊找鬼窃爷。 笑死邑蒋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的按厘。 我是一名探鬼主播医吊,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逮京!你這毒婦竟也來了卿堂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤懒棉,失蹤者是張志新(化名)和其女友劉穎草描,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體策严,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡穗慕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了妻导。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逛绵。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡怀各,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出术浪,到底是詐尸還是另有隱情瓢对,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布胰苏,位于F島的核電站硕蛹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏碟联。R本人自食惡果不足惜妓美,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鲤孵。 院中可真熱鬧,春花似錦辰如、人聲如沸普监。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凯正。三九已至,卻和暖如春豌蟋,著一層夾襖步出監(jiān)牢的瞬間廊散,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工梧疲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留允睹,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓幌氮,卻偏偏與公主長得像缭受,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子该互,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容