SpringMVC中的各种Servlet实现类

一句话总结
主角是这三个类:HttpServletBean、FrameworkServlet和DispatcherServlet,他们有继承关系,见下图,他们使用了模板方法的设计模式,所以一般都是父类会留下一个模板方法,子类去覆盖这个模板方法,以实现子类的特殊功能

image.png

可以看到在Servlet的继承结构中一共有5个类,GenericServlet和HttpServlet在java的servlet规范中,剩下的三个类HttpServletBean、FrameworkServlet和DispatcherServlet是Spring MVC提供的(前2者是抽象类,最后一个是非抽象类)

这三个类直接实现三个接口:EnvironmentCapable、EnvironmentAware和Application-ContextAware。

  • XXXAware在spring里表示对XXX可以感知,通俗点解释就是:如果在某个类里面想要使用spring的一些东西,就可以通过实现XXXAware接口告诉spring,spring看到后就会给你送过来,而接收的方式是通过实现接口唯一的方法set-XXX。

比如,有一个类想要使用当前的ApplicationContext,那么我们只需要让它实现ApplicationContextAware接口,然后实现接口中唯一的方法void setApplicationContext(ApplicationContext applicationContext)就可以了,spring会自动调用这个方法将applicationContext传给我们,我们只需要接收就可以了!很方便吧!

  • EnvironmentCapable,顾名思义,当然就是具有Environment的能力,也就是实现类可以提供Environment,所以EnvironmentCapable唯一的方法是Environment getEnvironment(),用于实现EnvironmentCapable接口的类,就是告诉spring,实现类它可以提供Environment,当spring需要Environment的时候就会调用实现类的getEnvironment方法跟它要。其实getEnvironment()函数里返回的Environment是从哪来的呢?答案是EnvironmentAware接口的setEnvironment函数,所以说要实现EnvironmentCapable提供Environment,那么你必须实现EnvironmentAware获取Environment

1. HttpServletBean

它实现了HttpServlet,HttpServlet中有一个无参数的init()模板方法,HttpServletBean需要覆盖这个模板方法,做一些定制操作。

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

    try {
        //将Servlet中配置的参数封装到pvs变量中,requiredProperties为必需参数,如果没配置将报异常
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
        ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
        bw.registerCustomEditor(Resource.class, new esourceEditor(resourceLoader, getEnvironment()));
        //模板方法,可以在子类调用,做一些初始化工作。bw代表DispatcherServlet
        initBeanWrapper(bw);
        //将配置的初始化值(如contextConfigLocation)设置到DispatcherServlet
        bw.setPropertyValues(pvs, true);
    }catch (BeansException ex) {
        logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
        throw ex;
    }

    // 模板方法,子类初始化的入口方法
    initServletBean();

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

BeanWrapper是干嘛的?
它是Spring提供的一个用来操作JavaBean属性的工具,使用它可以直接修改一个对象的属性。举个例子:

public class BeanWrapperTest {
    public static void main(String[] args) {
        User user = new User();
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);
        bw.setPropertyValue("userName", "张三");
        System.out.println(user.getUserName()); //输出张三
        PropertyValue value = new PropertyValue("userName", "李四");
        bw.setPropertyValue(value);
        System.out.println(user.getUserName()); //输出李四
    }
}

知道了BeanWrapper是干嘛的后,你一定会对这个注释有疑问:“bw代表DispatcherServlet”。创建bw的时候明明传的是this,这样它代表的应该是HttpServletBean,为什么注释说bw代表DispatcherServlet呢?
因为,HttpServletBean是一个抽象类,DispatcherServlet是间接实现了HttpServletBean的,所以这里的this指的是DispatcherServlet,而不是HttpServletBean。

解决了疑问以后,再来总结init()函数干了个啥:

  • 首先将Servlet中配置的参数使用BeanWrapper设置到DispatcherServle的相关属性,
  • 然后调用模板方法initServletBean,子类就通过这个方法初始化。

所以,它跟HttpServlet一样,HttpServletBean也提供了一个模板方法initServletBean(),供子类做定制化

2. FrameWorkServlet

它继承了HttpServletBean,而HttpServletBean提供的模板方法是initServletBean,所以它的初始化入口方法应该是initServletBean。

// org.springframework.web.servlet.FrameworkServlet
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
    }
    long startTime = System.currentTimeMillis();
    try {
        this.webApplicationContext = initWebApplicationContext();
        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;
    }

    if (this.logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        this.logger.info("FrameworkServlet '" + getServletName() + "': 
            initialization completed in " +
            elapsedTime + " ms");
   }
}

可以看到这里的核心代码只有两句:

  • 一句用于初始化WebApplicationContext,也就是创建spring容器。
  • 另一句用于初始化FrameworkServlet,而且initFrameworkServlet方法是模板方法,子类可以覆盖然后在里面做一些初始化的工作。

下面来看一下initWebApplicationContext方法:

// org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext initWebApplicationContext() {
//获取rootContext,rootContext早就在ContextLoaderListener已经被创建了,是属于Spring源码分析范畴的,这里就不分析它怎么来的了
    WebApplicationContext rootContext =
         WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    //如果已经通过构造方法设置了webApplicationContext
    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);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
       // 当webApplicationContext已经存在ServletContext中时,通过配置在Servlet中的contextAttribute参数获取
       wac = findWebApplicationContext();
   }
    if (wac == null) {
        // 如果webApplicationContext还没有创建,则创建一个
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        // 当ContextRefreshedEvent事件没有触发时调用此方法,模板方法,可以在子类重写
           onRefresh(wac);
    }

    if (this.publishContext) {
        // 将ApplicationContext保存到ServletContext中
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                "' as ServletContext attribute with name [" + attrName + "]");
        }
    }
    return wac;
}

initWebApplicationContext方法做了三件事:

  • 获取spring的根容器rootContext。(rootContext早就在ContextLoaderListener被调用时已经被创建了,是属于Spring源码分析范畴的,这里就不分析它怎么来的了)

  • 获取或者创建webApplicationContext,并根据情况调用onRefresh方法。
    获取或者创建webApplicationContext一共有三种方法:

    1. 第一种方法是在构造方法中已经传递webApplicationContext参数,这时只需要对其进行一些设置即可。这种方法主要用于Servlet3.0以后的环境中,Servlet3.0之后可以通过代码添加ServletWebApplicationContext, 创建DispatcherServlet过程中会使用ServletContext.addServlet(new DispatcherServlet(servletAppContext))方式注册Servlet, 这种情况下new DispatcherServlet的时候带了参数, 而这个参数就是之前通过代码添加的ServletWebApplicationContext,所以构造函数里就有了webApplicationContext),这样就可以直接获取了。
    2. 第二种略过把
    3. 第三种方法是在前面两种方式都无效的情况下自己创建一个。常规SpringMVC项目就是使用的这种方式。创建过程在createWebApplicationContext方法中,createWebApplicationContext内部又调用了configureAndRefreshWebApplicationContext方法。
  • 将webApplicationContext设置到ServletContext中。

上面说的第三种方法中的configureAndRefreshWebApplicationContext函数,代码如下:

// org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
    //获取创建类型,通过读取web.xml中的contextClass属性,如果没配则默认使用org.springframework.web.context.support.XmlWebApplicationContext
    Class<?> contextClass = getContextClass();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Servlet with name '" + getServletName() +
              "' will try to create custom WebApplicationContext context of class '" +
              contextClass.getName() + "'" + ", using parent context [" + parent + "]");
    }
//检查创建类型
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
       throw new ApplicationContextException(
            "Fatal initialization error in servlet with name '" + getServletName() +
            "': custom WebApplicationContext class [" + contextClass.getName() +
            "] is not of type ConfigurableWebApplicationContext");
    }

//具体创建
    ConfigurableWebApplicationContext wac =
          (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    // 将它的父Spring容器设置为Root WebApplicationContext
    wac.setParent(parent);
    //将web.xml中设置的contextConfigLocation参数传给wac,默认传入WEB-INFO/[ServletName]-Servlet.xml
    wac.setConfigLocation(getContextConfigLocation());

    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }else {
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
            ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());
        }
    }

    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    //添加监听ContextRefreshedEvent的监听器
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    postProcessWebApplicationContext(wac);
    applyInitializers(wac);
    // 执行refresh
    wac.refresh();
}

3. DispatcherServlet

DispatcherServlet虽然继承了FrameworkServlet,但是它并没有去重写FrameworkServlet提供的initFrameworkServlet()模板函数。它比较重要的函数是doService(...)函数(也是FrameworkServlet提供的模板函数,Servlet容器有请求进来会间接回调到这个函数)。

所以,DispatcherServlet里面执行处理的入口方法应该是doService,不过doServic并没有直接进行处理,而是交给了doDispatch进行具体的处理。

  • doService:在这里主要是对request设置了一些属性,如果是include请求还会对request当前的属性做快照备份,并在处理结束后恢复。最后将请求转发给doDispatch方法。
  • doDispatch方法也非常简洁,从顶层设计了整个请求处理的过程。doDispatch中最核心的代码只要4句,它们的任务分别是:
    ①根据request从HandlerMapping找到Handler;
    ②根据Handler找到对应的HandlerAdapter;
    ③用HandlerAdapter处理Handler;
    ④调用processDispatchResult方法处理上面处理之后的结果(包含找到View并渲染输出给用户),对应的代码如下:
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

3.1 doDispatch函数的具体处理流程

  1. doDispatch中首先检查是不是上传请求,如果是上传请求,则将request转换为MultipartHttpServletRequest,并将multipartRequestParsed标志设置为true。其中使用到了Multipart-Resolver。
  2. 然后通过getHandler方法获取Handler处理器链,其中使用到了HandlerMapping,返回值为HandlerExecutionChain类型,HandlerExecutionChain中包含着与当前request相匹配的Interceptor和Handler。

执行时先依次执行Interceptor的preHandle方法,最后执行Handler,返回的时候按相反的顺序执行Interceptor的postHandle方法。就好像要去一个地方,Interceptor是要经过的收费站,Handler是目的地,去的时候和返回的时候都要经过加油站,但两次所经过的顺序是相反的。

getHandler代码如下:

// org.springframework.web.servlet.DispatcherServlet
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;
}
  1. 接下来是处理GET、HEAD请求的Last-Modified。当浏览器第一次跟服务器请求资源(GET、Head请求)时,服务器在返回的请求头里面会包含一个Last-Modified的属性,代表本资源最后是什么时候修改的。在浏览器以后发送请求时会同时发送之前接收到的Last-Modified,服务器接收到带Last-Modified的请求后会用其值和自己实际资源的最后修改时间做对比,如果资源过期了则返回新的资源(同时返回新的Last-Modified),否则直接返回304状态码表示资源未过期,浏览器直接使用之前缓存的结果。

  2. 接下来依次调用相应Interceptor的preHandle

  3. 处理完Interceptor的preHandle后就到了此方法最关键的地方——让HandlerAdapter使用Handler处理请求,Controller就是在这个地方执行的。这里主要使用了HandlerAdapter.

  4. Handler处理完请求后,如果需要异步处理,则直接返回,如果不需要异步处理,当view为空时(如Handler返回值为void),设置默认view,然后执行相应Interceptor的postHandle。设置默认view的过程中使用到了ViewNameTranslator

  5. 接下来使用processDispatchResult方法处理前面返回的结果,其中包括处理异常、渲染页面、触发Interceptor的afterCompletion方法三部分内容。

整个过程的流程图大致如下,中间是doDispatcher的处理流程图,左边是Interceptor相关处理方法的调用位置,右边是doDispatcher方法处理过程中所涉及的组件。图中上半部分的处理请求对应着MVC中的Controller也就是C层,下半部分的processDispatchResult主要对应了MVC中的View也就是V层,M层也就是Model贯穿于整个过程中。

image.png

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

推荐阅读更多精彩内容