Spring mvc原理之注册DispatcherServlet

# 背景

spring mvc作为优秀的web框架,从2003年问世(根据changelog)到现在已经经历了21年。springframework框架里,web相关的类从1.0版本的25个,发展到现在6.1版本,已经有103个。还不包括spring-boot里web相关的代码。初学者使用`spring-boot-starter-web` 能很快启动一个web服务,但是要理清内部的运行逻辑和理解作者的设计思路,就要花费很大力气。

下面我尝试模仿spring mvc,从0开始搭建web服务,剖析作者的设计意图。

# web服务的基础-Tomcat

tomcat作为开源轻量级web服务器,支持java servlet,是spring boot默认的web服务器。通过内嵌的tomcat,我们可以很快速的开发web应用。我们通过一个demo,看一下开发web应用需要的最小配置。

## 一个小Demo

引入内嵌tomcat的pom文件:

```xml

<dependency>

    <groupId>jakarta.annotation</groupId>

    <artifactId>jakarta.annotation-api</artifactId>

    <version>2.1.1</version>

    <scope>compile</scope>

</dependency>

<dependency>

    <groupId>org.apache.tomcat.embed</groupId>

    <artifactId>tomcat-embed-core</artifactId>

    <version>10.1.18</version>

    <scope>compile</scope>

</dependency>

```

然后注册一个servlet就可以对外提供服务。

```java

public static void main(String[] args) throws LifecycleException {

Tomcat tomcat = new Tomcat();

    String path = "C:\\Users\\admin\\AppData\\Local\\Temp\\tomcat.default.9999";

    tomcat.setBaseDir(path);

    Context context = tomcat.addContext("", path);

    tomcat.addServlet(context.getPath(), "defaultServlet", new HttpServlet() {

        @Override

        public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

            System.out.println("get request " + req.getRequestURL());

            resp.setStatus(200);

            PrintWriter writer = resp.getWriter();

            writer.println(System.currentTimeMillis());

        }

    });

    context.addServletMappingDecoded("/*", "defaultServlet");

    Connector connector = new Connector("HTTP/1.1");

    connector.setPort(9999);

    tomcat.setConnector(connector);

    tomcat.start();

    tomcat.getServer().await();

}

```

这里做了几件事情:

1. 初始化Tomcat实例,并设置根目录

2. 创建一个Context

3. 创建一个Servlet,并添加url路径到Servlet的映射关系

4. 添加Connector,监听9999端口,这里支持HTTP/1.1协议

5. 启动tomcat服务

访问http://localhost:9999就能看到正常返回了结果:

```java

> curl -i http://localhost:9999/abc/dd?xx=1

HTTP/1.1 200

Content-Length: 15

Date: Wed, 24 Jan 2024 04:05:39 GMT

1706069139905

```

## Spring boot里的tomcat配置

了解完tomcat的基本使用方式,再对比spring boot里tomcat的用法。下面是spring boot初始化tomcat的逻辑:

```java

// org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory

@Override

public WebServer getWebServer(ServletContextInitializer... initializers) {

if (this.disableMBeanRegistry) {

Registry.disableRegistry();

}

Tomcat tomcat = new Tomcat();

File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");

tomcat.setBaseDir(baseDir.getAbsolutePath());

for (LifecycleListener listener : this.serverLifecycleListeners) {

tomcat.getServer().addLifecycleListener(listener);

}

Connector connector = new Connector(this.protocol);

connector.setThrowOnFailure(true);

tomcat.getService().addConnector(connector);

customizeConnector(connector);

tomcat.setConnector(connector);

tomcat.getHost().setAutoDeploy(false);

configureEngine(tomcat.getEngine());

for (Connector additionalConnector : this.additionalTomcatConnectors) {

tomcat.getService().addConnector(additionalConnector);

}

prepareContext(tomcat.getHost(), initializers);

return getTomcatWebServer(tomcat);

}

```

它这里干了几个事:

1. 初始化Tomcat实例,并设置根目录

2. 添加Server容器的LifecycleListener,作为一个扩展点开放出来

3. 添加默认Connector,这里支持自定义Connector的属性,包括设置端口、协议等,又是一个扩展点

4. 配置tomcat的Engine的`backgroundProcessorDelay` 属性和添加自定义`engineValves`

5. 添加用户定义的Connector

6. 创建一个Context,类型是spring自己定义的,继承了tomcat的Context

7. 将Tomcat对象包装成spring的`TomcatWebServer` 对象

在不考虑内部细节的情况下,spring boot初始化tomcat的步骤基本和demo里的步骤是一样的。主要增加了很多扩展点,可以添加Server容器、自定义Connector、添加Engine的Valve(管道处理类)。还封装了tomcat的Context,在自己的Context里也加了很多扩展点。

另外,有一点很大的不同,就是spring boot的初始流程里没用看到注册Servlet的地方。我们都知道spring mvc核心的Servlet是DispatcherServlet,它会代理所有请求。下面分析DispatcherServlet是怎么注册到tomcat的Context。

# DispatcherServlet注册到tomcat容器

spring boot是通过ServletContextInitializer来注册tomcat的Servlet、Filter、Listener等对象到ServletContext里。所以第一步要先将DispatcherServlet转成ServletContextInitializer对象。

## DispatcherServlet怎么转成ServletContextInitializer对象

DispatcherServlet是一个Servlet类型,要转成ServletContextInitializer,需要一个包装类,DispatcherServletRegistrationBean就是这个包装类。

DispatcherServletRegistrationBean是ServletContextInitializer的子类,在spring boot启动时,通过自动装配机制,注册了DispatcherServletRegistrationBean。并且将DispatcherServlet对象放到了DispatcherServletRegistrationBean对象里。

DispatcherServletRegistrationBean的注册流程:

1. DispatcherServletAutoConfiguration配置类先初始化了DispatcherServlet、DispatcherServletRegistrationBean对象

2. 在DispatcherServletRegistrationBean对象里放入注册Servlet需要的信息

```java

public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,

WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {

DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,

webMvcProperties.getServlet().getPath());

registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);

registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());

multipartConfig.ifAvailable(registration::setMultipartConfig);

return registration;

}

```

在得到ServletContextInitializer后就要考虑什么时候去执行它,并将DispatcherServlet注册到tomcat容器。这时先看一下tomcat的初始化过程。

## tomcat创建时机

SpringApplication.run()方法在执行refreshContext(context)时,会调用AnnotationConfigServletWebServerApplicationContext.refresh()方法,一直会调用到web容器的父类ServletWebServerApplicationContext的createWebServer()。

### **生成tomcat对象**

createWebServer()会调用TomcatServletWebServerFactory.getWebServer()来初始化tomcat对象。

初始化tomcat对象需要设置一个tomcat上下文,对应类型是StandardContext。这里spring自定义了StandardContext的子类TomcatEmbeddedContext作为tomcat上下文。

这时spring会预定义3个ServletContextInitializer,并封装到TomcatStarter里。TomcatStarter是ServletContainerInitializer的子类,ServletContainerInitializer是tomcat的对象和ServletContextInitializer不一样,后者是spring的对象。

然后调用TomcatEmbeddedContext.addServletContainerInitializer(TomcatStarter),把TomcatStarter添加到tomcat上下文的initializers属性里。initializers属性在启动tomcat时会用到。

tomcat初始化后,会被包装成TomcatWebServer对象,然后在构造函数里启动tomcat。之后tomcat就会从上下文对象里拿到ServletContextInitializer进行初始化。

### **tomcat初始化流程**

1. tomcat.start()调用server.start()

2. server又调用service.start()

3. service又调用engine.start()

4. engine通过线程池调用host.start()

5. host通过线程池调用TomcatEmbeddedContext.start(),这会执行父类方法StandardContext.startInternal(),方法里会把上面initializers里的ServletContainerInitializer对象拿出来,也就是上面的TomcatStarter对象,调用它的onStartup()方法

6. TomcatStarter又会调用它里面的几个ServletContextInitializer.onstartup(servletContext)

TomcatStarter默认包含3个ServletContextInitializer,我们关注的是ServletWebServerApplicationContext.selfInitialize()方法对应的匿名ServletContextInitializer,代码如下:

```java

private void selfInitialize(ServletContext servletContext) throws ServletException {

prepareWebApplicationContext(servletContext);

registerApplicationScope(servletContext);

WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);

for (ServletContextInitializer beans : getServletContextInitializerBeans()) {

beans.onStartup(servletContext);

}

}

```

核心逻辑在getServletContextInitializerBeans()里,方法返回ServletContextInitializerBeans对象。

ServletContextInitializerBeans是ServletContextInitializer的集合,它会把beanFactory里的ServletContextInitializer对象加进来,并且还把Servlet、Fileter、Listerner等spring bean包装成RegistrationBean(RegistrationBean是ServletContextInitializer的子类)也加进来。这样就得到一个ServletContextInitializer列表,默认会加载的对象有:

1. DispatcherServletRegistrationBean

2. CharacterEncodingFilter

3. OrderedFormContentFilter

4. OrderedRequestContextFilter

然后调用每个ServletContextInitializer的onStartup(servletContext)方法。

### DispatcherServletRegistrationBean注册DispatcherServlet

注册DispatcherServlet要执行这两行代码:

```java

// 添加servlet

servletContext.addServlet(servletName, servlet);

// 添加映射关系

context.addServletMappingDecoded(urlPattern, servletName);

```

DispatcherServletRegistrationBean里onStratup方法会调用register()方法。register()方法会做两个事情:

1. 执行addRegistration()方法,会把里面的DispatcherServlet对象注册到ServletContext,并返回ServletRegistration对象。

   

    ```java

    protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {

    String name = getServletName();

    return servletContext.addServlet(name, this.servlet);

    }

    ```

   

2. 执行configure()方法,注册url到DispatcherServlet对象的映射,逻辑在registration.addMapping(urlMapping)里

   

    ```java

    // org.springframework.boot.web.servlet.ServletRegistrationBean#configure

    protected void configure(ServletRegistration.Dynamic registration) {

    // ...

   

    if (!ObjectUtils.isEmpty(urlMapping)) {

    registration.addMapping(urlMapping);

    }

   

    // ...

    }

   

    // org.apache.catalina.core.ApplicationServletRegistration#addMapping

    public Set<String> addMapping(String... urlPatterns) {

        // ...

   

        for (String urlPattern : urlPatterns) {

            context.addServletMappingDecoded(UDecoder.URLDecode(urlPattern, StandardCharsets.UTF_8), wrapper.getName());

        }

   

        return Collections.emptySet();

    }

    ```

   

到这里,DispatcherServlet就成功注册到了toncat的上下文上,并且和url建立了映射关系,默认url是"/"。

# 总结

spring boot的基础是tomcat,就要遵循tomcat的servlet规范。它通过ServletContextInitializer实现了Servlet的自动注册机制;用DispatcherServlet代理所有请求,内部实现了请求的路由、类型转换等。将开发者和tomcat解耦,也方便框架去替换不同的web容器。

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

推荐阅读更多精彩内容

  • 对于Spring Boot项目来说只需要如下代码就可以启动整个项目 那么Spring容器,Web容器等等是怎么启动...
    Jeff_tian阅读 355评论 0 0
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,732评论 6 342
  • 5)、CRUD-员工列表 实验要求: 1)、RestfulCRUD:CRUD满足Rest风格; URI: /资源名...
    wshsdm阅读 564评论 0 0
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,301评论 1 92
  • 1.Spring背景 1.1.Spring四大原则: 使用POJO进行轻量级和最侵入式开发; 通过依赖注入和基于借...
    嗷大彬彬阅读 769评论 0 2