Spring MVC 基础源码学习

spring MVC是严格遵守java servlet规范的一种web框架,可以打包成war包交由web容器(Tomcat或者jetty)运行。接下来就来学习下spring mvc的运行过程以及其中的细节,如何和Tomcat无缝合作,如何和spring 本身的核心功能IOC、AOP合作。

MVC 实例

接下来就搭建一个基于maven的spring mvc的实例。

项目结构


image.png

StudentController 类

@Controller
@RequestMapping
public class StudentController {

    @RequestMapping(value = "/h")
    @ResponseBody
    public String getStudentInfo() {
        return "hello world";
    }
}

web.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/applicationContext.xml</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>demo</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>demo</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="com.demo.web" />

</beans>

pom.xml

// 插件部分
<build>
   <finalName>demo</finalName>
   <plugins>
       <plugin>
           <groupId>org.apache.tomcat.maven</groupId>
           <artifactId>tomcat7-maven-plugin</artifactId>
           <version>2.2</version>
           <configuration>
               <path>/</path>
               <uriEncoding>${file_encoding}</uriEncoding>
               <port>9912</port>
               <server>tomcat7</server>
           </configuration>
       </plugin>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-war-plugin</artifactId>
           <version>2.5</version>
       </plugin>


       <!-- Java Compiler -->
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.3</version>
           <configuration>
               <source>${java_source_version}</source>
               <target>${java_target_version}</target>
               <encoding>${file_encoding}</encoding>
           </configuration>
       </plugin>
   </plugins>
   <resources>
       <resource>
           <directory>src/main/resources</directory>
       </resource>
   </resources>

</build>

然后通过mvn tomcat7:run就可以正常启动了

Tomcat 基础

在介绍spring mvc的工作原理之前,有必要介绍下web容器的一种Tomcat。Tomcat是一个开源的web容器,严格准守servlet规范,在Tomcat中包含了各种各样的组件,层层嵌套依赖,如下图所示。

image.png

catalina是最核心的组件,也可以认为Tomcat是从它开始启动的,他持有1个server容器,server容器可以包含了多个service容器,每个service容器都持有了一个connector连接器以及一个engine,engine有层层包含了host、context、wrapper等。

其中engine、host、context、wrapper又分别存在各自的一个管道pipeline以及至少一个阀门valve,阀门可以为request和response添加任何外置的功能。

Tomcat启动是由各自容器的监听器调用启动的,按照上面所说的顺序依次执行启动的。

那我们常用的servlet是在哪里的么?他是被wrapper包装的,每一个wrapper持有一个servlet,所以在xml中配置了几个servlet,则就会存在多少个wrapper。并且servlet是通过ServletContext传递上下文的。在具体的URL映射的时候,会先根据各自的servlet的URL配置在Tomcat的mappingdata中体现,经过host、context、再到选择不同类型的wrapper(包含了wildcardWrappers、extensionWrappers、defaultWrapper、exactWrappers四种wrapper类型)最后才具体到某一个servlet请求上。

DispatcherServlet init 过程

DispatcherServlet类是spring mvc中实现的了servlet规范的实体类,实现了HttpServlet类,如下图

image.png

首先需要明确一点的是,DispatcherServlet也只是一个HttpServlet类,他是被wrapper调用的init(ServletConfig config)方法进入的,设置好config之后,进入到init()方法。

PS:在当前环境中,ServletConfig是StandardWrapperFacade类,可以从中获取到在xml配置的例如contextConfigLocation数据

HttpServletBean 类

    public final void init() throws ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing servlet '" + getServletName() + "'");
        }

        // Set bean properties from init parameters.
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        // 获取xml配置的属性,在下面贴的图片中可以看到,当DispatcherServlet没有任何配置的时候,就会抛出异常,说缺少必备的配置属性
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
                // 这一步就是设置DispatcherServlet的属性值的操作
            }
            catch (BeansException ex) {
                if (logger.isErrorEnabled()) {
                    logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
                }
                throw ex;
            }
        }

        // 开始真正的初始化DispatcherServlet类了
        initServletBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Servlet '" + getServletName() + "' configured successfully");
        }
    }
    
image.png

FrameworkServlet 类

    protected final void initServletBean() throws ServletException {
        try {
            this.webApplicationContext = initWebApplicationContext();
            // 这个是初始化当前dispatchservlet的webApplicationContext参数
            // 然后该参数中会持有spring的IOC容器等信息,通过这个参数可以获取bean数据
            // 具体细节可看下面的代码
            initFrameworkServlet();
            // 未做任何事情,由子类实现,不过暂时为空
        }
        catch (ServletException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }
        catch (RuntimeException ex) {
            this.logger.error("Context initialization failed", ex);
            throw ex;
        }
    }
    
    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        // 从当前的上下文中查找是否存在跟上下文信息(通过ContextLoaderListener加载的都会存在的)
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    // 重新再刷新一次WebApplicationContext持有的内容信息
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // 创建一个新的WebApplicationContext信息,并设置好其parent
            // 这里需要好好看看,同样的拆分为两部分执行
            // 1、创建WebApplicationContext类,也就是XmlWebApplicationContext实体类,并设置好其环境、配置属性、父类等信息
            // 2、调用configureAndRefreshWebApplicationContext,同样的设置好其关于servlet的上下文的属性信息,最后调用wac.refresh()
            // PS: refresh()就开始了spring IOC的解析存储操作了
            wac = createWebApplicationContext(rootContext);
            // 不过这里有一步需要注意到,在refresh()中,最后会有onApplicationEvent()的操作,他会调用在DispatcherServlet类的initStrategies方法,完成URL映射、Template等操作
            // 并且设置this.refreshEventReceived为true
        }

        if (!this.refreshEventReceived) {
            // 完成了上面的刷新操作就不要再刷新了
            // 这里的onRefresh还是会调用initStrategies方法
            // 殊途同归罢了
            onRefresh(wac);
        }

        if (this.publishContext) {
            // 把当前的上下文信息也保存到ServletContext中
            // 如果注意到的函数开头的rootContext的获取方法会发现也是通过这样的方式获取的
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
        }
        return wac;
    }

这样就完成了DispatcherServlet的初始化操作了,接下来就可以具体处理http请求了,当前这其中遗漏了很多重要的的点,例如

  • rootContext 这是怎么一回事,是必须的么,和applicationContext.xml又有什么关系呢?
  • xml配置的context-param和servlet的init-param有什么区别?

这几个点会在后续的学习笔记中再了解其原理,当前主要是介绍DispatcherServlet以及相关的东西。

DispatcherServlet URL映射以及请求处理 过程

Tomcat会通过的DispatcherServlet的servlet-mapping的属性匹配到合适的wrapper,再关联到具体的DispatcherServlet,也就意味着在web.xml确实可以配置多个servlet,只是在spring mvc中常用的就这一个而已。

HTTP请求,最后都会打到DispatcherServlet类的doService方法中,设置一些属性之后,又来到了doDispatch方法中,这个方法是核心也是最重要的http请求处理的方法,通过URL选择合适的controller,选择具体的modelandview,再渲染生成数据回调等等操作

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

                mappedHandler = getHandler(processedRequest);
                // 通过URL找到合适的controller,并存储在HandlerExecutionChain对着中
                // 还会检测是否进行cors跨域操作,如果存在跨域就会按照跨域的要求去处理
                // @CrossOrigin(origins="http://test.com") 可以直接放在controller的注解上
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                   // 没有找到合适的处理handle,就是404了
                   // 这就可以自定义配置404页面以及跳转等信息
                   // response.sendError(HttpServletResponse.SC_NOT_FOUND)
                   // 这里又可以引出一个问题了,如何配置404页面
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // 检测当前获取的controller是否合适,并且得到合适的HandlerAdapter
                HandlerAdapter ha = 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 (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                   // 使用spring本身的拦截器前置处理
                    return;
                }

                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                // 真正的处理请求,在本demo中会调到AnnotationMethodHandlerAdapter类中执行handle方法,返回的mv是ModelAndView
                // 解析当前handler中包含了所有的方法,匹配其中合适的方法之后,invoke调用
                // 不过这里需要注意到,类似于返回json的请求,是不需要模板渲染的,此时mv返回的是null,不过具体的json数据已经填入到了responseBody中

                applyDefaultViewName(processedRequest, mv);
                // 如果mv
                mappedHandler.applyPostHandle(processedRequest, response, mv);
                // spring 拦截器的后置处理
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            // 处理结果了,如果存在异常会把exception带上,例如500错误等,按照异常处理
            // 如果存在了模板,需要经过render处理
            // 否则就直接把得到的数据当做body返回
            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);
                }
            }
        }
    }

到这里就完成了对一个普通的http请求的处理全过程,不过还是存在诸多问题没有去分析,如图

  • cors跨域是什么,如何使用
  • URL映射规则是如何完成的,以及和Tomcat的URL映射有什么关联么?
  • 模板是如何被渲染的,在xml中如何设置不同的模板的?

还有个问题一直疏忽了,在spring mvc中存在大量的if(***.debug)这种操作,那么该如何配置使用日志系统呢?

接下来会再写笔记去分析上述提到的各种问题~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,732评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • 本章主要内容: 备用的Spring MVC配置项 处理文件上传 控制器中的异常处理 使用flash属性 “等等,客...
    hoxis阅读 3,514评论 1 26
  • 看进阴影里面 ——裸体的边缘 看到了吗?在那里 另一个你 很美好,太孤单
    我是蝎大人阅读 182评论 2 3