SpringMVC 核心源码分析

Spring的源码非常复杂,想要一篇概览几乎不可能,所以只能分而治之,本文摘写自近期抽空翻阅SpringMVC框架的部分源码,借此由浅入深地探索SpringMVC的核心体系结构。

在前后端分离大行其道的今日,由后端完成渲染的JSP/Freemarker时代可以说已经逐渐被NodeJS搭建的大前端体系取代,所以本文也不去翻阅SpringMVC视图渲染部分,仅认为后端作为一个数据服务接口来看到,简而言之,仅分析SpringMVC与前端的JSON交互流程。

基于过往的研发经验相信绝大多数都了解到,SpringMVC的核心组件为DispatcherServlet组件,而实现的规约则是J2EE关于Servlet的规范。如果对于Servlet规范不甚清楚,请抽空自行补充。本篇仅讨论HTTP请求的完整调用链路,不涉及其他协议规范说明。

DispatcherServlet

作为一个全局Servlet,欲深究其意,需先了解DispatcherServlet容器初始化时做了什么处理?而DispatcherServlet对象初始化包括如下三部分:

  • DispatcherServlet对象静态初始化部分?
  • 构造函数?
  • Servlet规范中容器初始化流程?

在整个容器初始化的过程,只有明白其中会涉及到得我们需要去核心关注的部分,才能很好的把握住SpringMVC的核心流程,从中透析核心体系。

静态初始化部分

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());
        }
    }

静态初始化实际是读取了DispatcherServlet.properties文件的内容,其中都有些什么呢?摘4.3.18.RELESE版本的文件,内容如下:

// 省略...
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
// 省略....

找一个类进去翻阅,可以猜测它可能是要初始化这些Bean,再查阅DispatcherServlet#getDefaultStrategies() 即可确认,当然,这部分并非是静态初始化就完成,而是延迟至Serlvet容器初始化时分批进行初始化。

构造函数

翻阅代码可以发现内部有两个构造函数,那么究竟在容器初始化的时候默认调用的是哪个?

easy,打个断点DEBUG一下就可以,基于我的环境上我发现是默认无参构造函数。(基于springboot 1.5.6版本)

那么是谁来调用改无参构造函数的呢?

easy,在springboot环境运行时可以看到有一个显眼的注解SpringBootApplication,而该注解的组成部分还包括EnableAutoConfiguration,这个注解又是要解决什么的呢?

它将默认加载spring-boot-autoconfig下的spring.factories文件内部的类,比如:

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

///.....

那么查阅这些类,它们做了什么?比如DispatcherServletAutoConfiguration,查阅时发现它加了注解Configuration

查阅该注解你又将发现它会处理声明@Bean的方法,并将该Bean注入到Spring容器中,由容器托管其生命周期。

Servlet容器初始化

查阅javax.servlet.Servlet接口的定义,可以清晰的看到规范中定义四个方法,其中需要重点关注如 init, service

那么DispatcherServlet容器在初始化时做了什么?

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

注:如果想要知道Serlevt如何工作,可以翻阅Tomcat源码,以此看到工业级产品如何实现Servlet规范

翻阅DispatchserServlet#initStrategies()可以看到好几个方法,方法名称皆以init打头,那么它们就是在初始化什么?为什么在自己的程序中使用@Controller, @RequestMapping, @ResponseBody就可以跟前端完成常见的JSON交互了呢?

为了完成非常复杂的功能交互,SpringMVC定义了非常多的顶级接口,而我们核心要关注的无非是HandlerMapping, HandlerAdpater

HandlerMapping

查阅DispatcherServlet#initHandlerMappings()方法时发现一个问题,那就是它并非依据前文中提到的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

由于我们实际编码过程经常使用@RequestMapping来完成URL路径标记,那么先从与之有些类似的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) {
      // 比如这个,很多开发者应该都记得以前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;
    }

单单查阅内部方法,类的命名大致可以猜测出一些信息来,不过最终的使用还是要通过其他核心流程来分析。

HandlerAdpater

同理的方式可以看到核心的关注类

image.png

Servlet容器工作流程

DispatcherServlet#doDispatch(),可以认为是Servlet规范中的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;
    }

方法语义其实很简单,从注入的一堆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可以发现最终找到的对象为RequestMappingHandlerMapping,怎么匹配的呢?通过结果来反向推导原因。Spring框架中大量使用了模板方法,所以看类实现过程要关注子类与父类之间的关系。

RequestMappingHandlerMapping
类体系

先查阅整个类结构体系,相当复杂,不过不要紧,看关键点。

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

初始化流程

RequestMappingHandlerMapping作为一个由Spring托管的Bean,在它初始化的生命周期中,有非常关键的一步,即afterPropertiesSet方法,跟踪上去,最终你会看到如下几个核心的方法:

//初始化HandlerMethods
AbstractHandlerMethodMapping#initHandlerMethods
// 判断这个类是否有@Controller 或者@RequestMapping
RequestMappingHandlerMapping#isHandler()
// 查找hanlder内部方法
AbstractHandlerMethodMapping#detectHandlerMethods()
// 找到handler内部带有标记的@RequestMapping的方法,包装成RequestMappingInfo
RequestMappingHandlerMapping#getMappingForMethod()
// 找到一个可以被调用的目标方法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();

注:这些信息最终可以通过反射来完成方法的调用。

在完成初始化之后,当进来时,如何完成URLController中的Method的对应呢?

映射

AbstractHandlerMethodMapping#getHandlerInternal()
AbstractHandlerMethodMapping#lookupHandlerMethod()
MappingRegistry#getMappingsByUrl()
当完成初始化之后,通过请求的URL找到对应的HandlerMethod其实非常简单.

getHandlerAdapter()

通过RequestMappingHandlerMapping来找Adapter,先来看HandlerAdapter接口的定义:MVC framework SPI, allowing parameterization of the core MVC workflow.

它的核心在于,找到RequestMappingHandlerMapping来完成从请求到执行到响应的完整工作流程。

通过DEBUG可以非常快速定外找,需要关注的核心实现为RequestMappingHandlerAdapter.通过其父类实现的support()其实也可以非常快速确定,因为它支持的Handler类型为HandlerMethod.

handler()

RequestMappingHandlerAdapter

可以非常明显看到这个类内部包含着一系列的HttpMessageConverter,这些对象可以解决什么问题呢?

方法调用

RequestMappingHandlerAdapter#invokeHandlerMethod()
ServletInvocableHandlerMethod#invokeAndHandle()
InvocableHandlerMethod#invokeForRequest()
InvocableHandlerMethod#doInvoke()
// 最终方法执行
Method#invoke()

也就是说,HTTP请求进入SpringMVC应用之后,匹配到HandlerMethod,最终通过HandlerMethod中包含的Method对象,而Method通过Java反射即可完成方法调用。

完成调用的过程分两部分:

  • 从HTTP中解析参数
  • 将返回值进行处理(比如声明@ResponseBody
参数解析

参数的解析以及映射是一件相当复杂的工作,比如

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

在一个完整的HTTP请求中,核心参数可能在URL上,也可以在Body上,当要完成参数解析时,需要从中取出参数,并按照方法所需入参进行封装,最后入参完成方法调用,欲深究请关注如下方法

InvocableHandlerMethod#getMethodArgumentValues()

返回值处理

返回值可能会有各式各样的结果,一般情况下可能是一个JSON对象,通过DEBUG可以看到核心类RequestResponseBodyMethodProcessor

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

从HTTP协议来看,在入参时就已经明确了响应体的类型,核心参数为:accept,常见比如application/json, text/html, 等等。SpringMVC如何处理这个问题呢?请聚焦于MediaType

AbstractMessageConverterMethodProcessor#writeWithMessageConverters()

// 从http请求中取出来后进行解析
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
// 根据返回值判断何时的MediaType
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

比如上请求,根据返回值判断即为JSON,故而结果如下图:

image.png

MediaType将决定以何中HttpMessageConvert来完成对象的转换处理,以及流输出。

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

而判断的核心在于实现类的supportedMediaTypes参数值,比如:

public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
钩子函数

在将返回值写入到输出流中前,提供了钩子函数RequestResponseBodyAdviceChain

processDispatchResult()

完成请求的后置处理。

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

其实是将所有的后置拦截器完成调用。

思考

看完了完整的流程后,如果需要对SpringMVC做一些优化,需要如何?

优化的层面往往是往下往上两个层面。

  • 往下从使用层面去考虑,选用更适合SpringMVC框架的用法,比如URL匹配规则简单化
  • 往上从优化层面去考虑,比如减少不必要的Bean创建,简化从HandlerMap, HandlerAdapter等的寻找链路,加快容器初始化,提高调用链路效率,等等
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容