SpringBoot 就像一条巨蟒,慢慢缠绕着我们,使我们麻痹。不得不承认,使用了 SpringBoot 确实提高了工作效率,但同时也让我们遗忘了很多技能。刚入社会的时候,我还是通过 Tomcat 手动部署 JavaWeb 项目,还经常对 Tomcat 进行性能调优。除此之外,还需要自己理清楚各 Jar 之间的关系,以避免 Jar 丢失和各版本冲突导致服务启动异常的问题。到如今,这些繁琐而又重复的工作已经统统交给 SpringBoot 处理,我们可以把更多的精力放在业务逻辑上。但是,清楚 Tomcat 的工作原理和处理请求流程和分析 Spring 框架源码一样的重要。至少面试官特别喜欢问这些底层原理和设计思路。希望这篇文章能给你一些帮助。

Tomcat 整体架构

Tomcat 是一个免费的、开源的、轻量级的 Web 应用服务器。适合在并发量不是很高的中小企业项目中使用。

文件目录结构

以下是 Tomcat 8 主要目录结构

目录 功能说明
bin 存放可执行的文件,如 startup 和 shutdown
conf 存放配置文件,如核心配置文件 server.xml 和应用默认的部署描述文件 web.xml
lib 存放 Tomcat 运行需要的jar包
logs 存放运行的日志文件
webapps 存放默认的 web 应用部署目录
work 存放 web 应用代码生成和编译文件的临时目录

功能组件结构

Tomcat 的核心功能有两个,分别是负责接收和反馈外部请求的连接器 Connector,和负责处理请求的容器 Container。其中连接器和容器相辅相成,一起构成了基本的 web 服务 Service。每个 Tomcat 服务器可以管理多个 Service。

组件 功能
Connector 负责对外接收反馈请求。它是 Tomcat 与外界的交通枢纽,监听端口接收外界请求,并将请求处理后传递给容器做业务处理,最后将容器处理后的结果反馈给外界。
Container 负责对内处理业务逻辑。其内部由Engine、Host、Context 和 Wrapper 四个容器组成,用于管理和调用 Servlet 相关逻辑。
Service 对外提供的 Web 服务。主要包含连接器和容器两个核心组件,以及其他功能组件。Tomcat 可以管理多个 Service,且各 Service 之间相互独立。

Tomcat 连接器核心原理

Tomcat 连接器框架——Coyote

连接器核心功能

一、监听网络端口,接收和响应网络请求。

二、网络字节流处理。将收到的网络字节流转换成 Tomcat Request 再转成标准的 ServletRequest 给容器,同时将容器传来的 ServletResponse 转成 Tomcat Response 再转成网络字节流。

连接器模块设计

为满足连接器的两个核心功能,我们需要一个通讯端点来监听端口;需要一个处理器来处理网络字节流;最后还需要一个适配器将处理后的结果转成容器需要的结构。

组件 功能
Endpoint 端点,用来处理 Socket 接收和发送的逻辑。其内部由 Acceptor 监听请求、Handler 处理数据、AsyncTimeout 检查请求超时。具体的实现有 NioEndPoint、AprEndpoint 等。
Processor 处理器,负责构建 Tomcat Request 和 Response 对象。具体的实现有 Http11Processor、StreamProcessor 等。
Adapter 适配器,实现 Tomcat Request、Response 与 ServletRequest、ServletResponse之间的相互转换。这采用的是经典的适配器设计模式。
ProtocolHandler 协议处理器,将不同的协议和通讯方式组合封装成对应的协议处理器,如 Http11NioProtocol 封装的是 HTTP + NIO。

对应的源码包路径 org.apache.coyote 。对应的结构图如下

Tomcat 容器核心原理

Tomcat 容器框架——Catalina

容器结构分析

每个 Service 会包含一个容器。容器由一个引擎可以管理多个虚拟主机。每个虚拟主机可以管理多个 Web 应用。每个 Web 应用会有多个 Servlet 包装器。Engine、Host、Context 和 Wrapper,四个容器之间属于父子关系。

容器 功能
Engine 引擎,管理多个虚拟主机。
Host 虚拟主机,负责 Web 应用的部署。
Context Web 应用,包含多个 Servlet 封装器。
Wrapper 封装器,容器的最底层。对 Servlet 进行封装,负责实例的创建、执行和销毁功能。

对应的源码包路径 org.apache.coyote 。对应的结构图如下

容器请求处理

容器的请求处理过程就是在 Engine、Host、Context 和 Wrapper 这四个容器之间层层调用,最后在 Servlet 中执行对应的业务逻辑。各容器都会有一个通道 Pipeline,每个通道上都会有一个 Basic Valve(如StandardEngineValve), 类似一个闸门用来处理 Request 和 Response 。其流程图如下。

Tomcat 请求处理流程

上面的知识点已经零零碎碎地介绍了一个 Tomcat 是如何处理一个请求。简单理解就是连接器的处理流程 + 容器的处理流程 = Tomcat 处理流程。哈!那么问题来了,Tomcat 是如何通过请求路径找到对应的虚拟站点?是如何找到对应的 Servlet 呢?

映射器功能介绍

这里需要引入一个上面没有介绍的组件 Mapper。顾名思义,其作用是提供请求路径的路由映射。根据请求URL地址匹配是由哪个容器来处理。其中每个容器都会它自己对应的Mapper,如 MappedHost。不知道大家有没有回忆起被 Mapper class not found 支配的恐惧。在以前,每写一个完整的功能,都需要在 web.xml 配置映射规则,当文件越来越庞大的时候,各个问题随着也会出现

HTTP请求流程

打开 tomcat/conf 目录下的 server.xml 文件来分析一个http://localhost:8080/docs/api 请求。

第一步:连接器监听的端口是8080。由于请求的端口和监听的端口一致,连接器接受了该请求。

第二步:因为引擎的默认虚拟主机是 localhost,并且虚拟主机的目录是webapps。所以请求找到了 tomcat/webapps 目录。

第三步:解析的 docs 是 web 程序的应用名,也就是 context。此时请求继续从 webapps 目录下找 docs 目录。有的时候我们也会把应用名省略。

第四步:解析的 api 是具体的业务逻辑地址。此时需要从 docs/WEB-INF/web.xml 中找映射关系,最后调用具体的函数。

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <!-- 连接器监听端口是 8080,默认通讯协议是 HTTP/1.1 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" /> <!-- 名字为 Catalina 的引擎,其默认的虚拟主机是 localhost -->
<Engine name="Catalina" defaultHost="localhost"> <!-- 名字为 localhost 的虚拟主机,其目录是 webapps-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true"> </Host>
</Engine>
</Service>
</Server>

SpringBoot 如何启动内嵌的 Tomcat

SpringBoot 一键启动服务的功能,让有很多刚入社会的朋友都忘记 Tomcat 是啥。随着硬件的性能越来越高,普通中小项目都可以直接用内置 Tomcat 启动。但是有些大一点的项目可能会用到 Tomcat 集群和调优,内置的 Tomcat 就不一定能满足需求了。

我们先从源码中分析 SpringBoot 是如何启动 Tomcat,以下是 SpringBoot 2.x 的代码。

代码从 main 方法开始,执行 run 方法启动项目。

SpringApplication.run

从 run 方法点进去,找到刷新应用上下文的方法。

this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);

从 refreshContext 方法点进去,找 refresh 方法。并一层层往上找其父类的方法。

this.refresh(context);

在 AbstractApplicationContext 类的 refresh 方法中,有一行调用子容器刷新的逻辑。

this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();

从 onRefresh 方法点进去,找到 ServletWebServerApplicationContext 的实现方法。在这里终于看到了希望。

protected void onRefresh() {
super.onRefresh(); try {
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}

从 createWebServer 方法点进去,找到从工厂类中获取 WebServer的代码。

if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = this.getWebServerFactory();
// 获取 web server
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
} else if (servletContext != null) {
try {
// 启动 web server
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var4) {
throw new ApplicationContextException("Cannot initialize servlet context", var4);
}
}

从 getWebServer 方法点进去,找到 TomcatServletWebServerFactory 的实现方法,与之对应的还有 Jetty 和 Undertow。这里配置了基本的连接器、引擎、虚拟站点等配置。

public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator(); while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
} this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}

服务启动后会打印日志

o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8900 (http)
o.apache.catalina.core.StandardService : Starting service [Tomcat]
org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.34
o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal ...
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 16858 ms

END

文章到这里就结束了,实在是 hold 不住了,周末写了一整天还没有写到源码部分,只能放在下一章了。再写真的就要废了,有什么不对的地方请多多指出。最后完整版可以通过微信公众号 学英语会编程 阅读。完整版在这个基础上添加了一些单词解析,我们的口号是:英语学得好,源码看的爽。

你还记得 Tomcat 的工作原理么的更多相关文章

  1. Tomcat Servlet工作原理

    前言 Tomcat的启动过程 Web应用初始化 创建Servlet实例 初始化Servlet 执行service方法 前言 Servlet实际上就是一个java类,只不过可以和浏览器进行一些数据的交换 ...

  2. 【IntelliJ IDEA】idea部署服务到Tomcat的工作原理

    参考地址: https://blog.csdn.net/qq_41116058/article/details/81435084 为什么idea部署服务到tomcat时候,一定要修改Applicati ...

  3. Tomcat 系统架构与设计模式,第 1 部分: 工作原理(转载)

    简介: 这个分为两个部分的系列文章将研究 Apache Tomcat 的系统架构以及其运用的很多经典设计模式.本文是第 1 部分,将主要从 Tomcat 如何分发请求.如何处理多用户同时请求,还有它的 ...

  4. Tomcat 系统架构与设计模式,第 1 部分: 工作原理

    简介: 这个分为两个部分的系列文章将研究 Apache Tomcat 的系统架构以及其运用的很多经典设计模式.本文是第 1 部分,将主要从 Tomcat 如何分发请求.如何处理多用户同时请求,还有它的 ...

  5. tomcat 系统架构与设计模式 第一部分 系统架构工作原理 转

    Tomcat 系统架构与设计模式,第 1 部分: 工作原理 许 令波, Java 开发工程师, 淘宝网 许令波,现就职于淘宝网,是一名 Java 开发工程师.对大型互联网架构设计颇感兴趣,并对一些开源 ...

  6. 【Tomcat】Tomcat 系统架构与设计模式,第 1 部分: 工作原理

    这个分为两个部分的系列文章将研究 Apache Tomcat 的系统架构以及其运用的很多经典设计模式.本文是第 1 部分,将主要从 Tomcat 如何分发请求.如何处理多用户同时请求,还有它的多级容器 ...

  7. Tomcat 工作原理 1 (转)

    Tomcat 系统架构与设计模式,第 1 部分: 工作原理 这个分为两个部分的系列文章将研究 Apache Tomcat 的系统架构以及其运用的很多经典设计模式.本文是第 1 部分,将主要从 Tomc ...

  8. Tomcat内部结构、工作原理、工作模式和运行模式

    TOMCAT的内部结构 Tomcat是一个基于组件的服务器,它的构成组件都是可配置的,其中最外层的是Catalina servlet容器,其他组件按照一定的格式要求配置在这个顶层容器中.Tomcat的 ...

  9. Tomcat内部结构及工作原理学习

    Tomcat原本是Servlet/JSP的一个调试工具,后来才发展为一个Servlet/JSP的容器. Tomcat作为Servlet容器,负责处理客户请求,把请求传送给Servlet并把结果返回给客 ...

随机推荐

  1. 通过实际案例摸清楚Spring事务传播的行为

    @ 目录 事务传播 案例准备 案例解析 1.无事务 2. Propagation.REQUIRED 3. Propagation.SUPPORTS 4. Propagation.MANDATORY 5 ...

  2. 关于数据库新建用户提示“用户、组或角色‘’XXX‘’在当前数据库中已已存在”的解决办法

    一般在还原数据库后,给这个数据库添加一个登录名时出现. 例如数据库备份文件中已经包含了用户abc,现在还原了数据库,然后发现现有数据库中没有abc这个用户,想要新建一个abc用户,作为该数据库的own ...

  3. JavaScript学习系列博客_31_JavaScript Math 工具类

    Math - Math属于一个工具类,它不需要我们创建对象(例如Date日期对象需要通过构造函数创建对象 var 变量=new Date(),Math不需要),它里边封装了属性运算相关的常量和方法 我 ...

  4. c++线性表和数组的区别

    在传统C语言程序中,描述顺序表的存储表示有两种方式:静态方式.动态方式 顺序表的静态存储表示: #define maxSize 100 typedefintT; typedefstruct{ T da ...

  5. C语言基础练习——打印乘法口诀表

    C语言基础练习--打印乘法口诀表 JERRY_Z. ~ 2020 / 8 / 26 转载请注明出处! 代码: /* * @Author: JERRY_Z. * @Date: 2020-08-26 16 ...

  6. Java面试题(网络篇)

    网络 79.http 响应码 301 和 302 代表的是什么?有什么区别? 301,302 都是HTTP状态的编码,都代表着某个URL发生了转移. 区别: 301 redirect: 301 代表永 ...

  7. MySQL主从同步简单介绍&配置

    介绍: 现在mysql集群基本上都是使用一主多从方式,实现读写分离(主库写.从库读).负载均衡.数据备份,以前只是使用从未配置过,今天简单配置一下! mysql主从复制是通过binary log日志实 ...

  8. WebApis中DOM执行机制的认识

    1.1. 节点操作 1.1.1 删除节点 node.removeChild() 方法从 node节点中删除一个子节点,返回删除的节点. <button>删除</button> ...

  9. Kafka入门(3):Sarama生产者是如何工作的

    摘要 在这一篇的文章中,我将从Sarama的同步生产者和异步生产者怎么创建开始讲起,然后我将向你介绍生产者中的各个参数是什么,怎么使用. 然后我将从创建生产者的代码开始,按照代码的调用流程慢慢深入,直 ...

  10. 3个必备cookie实用方法

    今天跟大家介绍一下三种cookie的使用方法,selenium提供了我们add_cookie()方法来跳过验证码直接登录的方法.我们现在以博客园登录为例,都知道现在博客园登录要拼图验证. 先在网页打开 ...