Tomcat 总体架构设计

在开始这篇文章的时候,忽然发现上一篇内容的题目不是很合适,不应该叫启动流程,更确切的应该是叫启动脚本。

在最开始,先介绍下 Tomcat 的总体设计,先有一个大概的印象,对 Tomcat 不至于那么陌生。

先介绍下 Tomcat 的一些基础组件(以下内容来自刘光瑞老师的「tomcat 架构解析」):

组件名称 介绍
Server 这个其实就是 Servlet 容器,一个 Tomcat 中只能有一个 Server
Service Service 表示一个或多个 Connector 的集合,这些 Connector 共享同一个 Container 来处理其请求。在同一个 Tomcat 实例内可以包含任意多个 Service 实例,它们彼此独立
Connector 这个是 Tomcat 的连接器,用于监听并转化 Servlet 的请求,同时将读取的 Socket 请求转交由 Container 处理,并且支持不同的协议以及不同的 I/O 方式。
Container 表示能够执行客户端请求并返回响应的一类对象。在 Tomcat 中存在不同级别的容器 Engine, Host, Context, Wrapper
Engine 表示整个 Servlet 引擎。在 Tomcat 中, Engine 为最高层级的容器对象。尽管 Engine 不是直接处理请求的容器,却是获取目标容器的入口。
Host Host 作为一类容器,表示 Servlet 引擎(即 Engine 中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在 DNS 服务器上注册
Context Context 作为一类容器,用于表示 Servletcontext ,在 Servlet 规范中,一个 Servletcontext 即表示一个独立的 Web 应用。
Wapper Wapper 作为一类容器,用于表示 Web 应用中定义的 Servlet。
Executor 表示 Tomcat 组件间可以共享的线程池。

这个表格大致看一下,了解下 Tomcat 的一些组件,无需记忆,先有个印象,后面的内容会慢慢的聊到每一个组件。

作为一款 Web 容器, Tomcat 大体上实现了两个核心功能:

  • 处理 Socket 连接,负责网络字节流与 RequestResponse 对象的转化。
  • 加载并管理 Servlet ,以及处理具体的 Request 请求。

所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部处理。

Tomcat 为了实现多种支持多种 I/O 模型和应用层协议,将 连接器(Connector)容器(Container) 进行了分离,

在 Tomcat 中,总会有一个 Service ,一个 Service 包含着多个 Connector 和一个 Container(或者说是它的顶层容器 Engine ) 。

而一个 Container 可以包含多个 Host ,一个 Host 内部又可以有多个 Context ,一个 Context 内部又会有多个 Servlet 。

Tomcat 的设计感觉上和 「俄罗斯套娃」 很像,一层包着一层。

Tomcat 启动初始化流程

先放一张启动流程图,然后我们再从源码的角度来验证这个启动流程。

从上面这张图可以清晰的了解到 Tomcat 的初始化的核心过程如下:

BootStrap -> Catalina -> Server -> Service -> Excutor -> Container (Engine -> Host -> Context -> Container)

1 BootStrap.main()

首先,整个程序是从 BootStrap 开始启动的,执行的是 BootStrap.main() :

public static void main(String args[]) {

    synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
} try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
} if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}

在最开始,执行的是一段 synchronized 的同步代码,这里执行代码 bootstrap.init() 初始化了 bootstrap 对象,具体做了什么跟进去看下:

public void init() throws Exception {
// 初始化类加载器
initClassLoaders();
// 设置线程类加载器,将容器的加载器传入
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 加载安全类
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 初始化日志
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 注意这里,通过反射加载了 Catalina
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
// 调用了上面加载的 Catalina 中的 setParentClassLoader 方法
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
// 将类加载器赋值成一个参数
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
// 看名字意思是一个共享加载器
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
// 调用了 Catalina 内的 setParentClassLoader 方法对 Catalina 类内的类加载器赋值
method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance;
}

注释都加好了,就不多解释了,我们接着回到之前的 main ,接下来还有一句是 daemon = bootstrap ,就是把我们刚才初始化过的 bootstrap 赋值给了 daemon 这个变量。

接下来是一大段的判断,主要是用来判断输入参数,这里面我们关注的就中间那一小段,当输入参数为 start 的时候,总共做了两件大事:

  • daemon.load(args)
  • daemon.start()

先跟进去看下 daemon.load(args) 都干了点啥:

private void load(String[] arguments) throws Exception {
// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
// 这里的 catalinaDaemon 刚才在 init() 方法里面进行了初始化,所以这里调用的实际上是 Catalina 的 load 方法
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
method.invoke(catalinaDaemon, param);
}

这里实际上是开启了整个链式调用,接着往下追,看下 Catalina 里面的 load 方法。

在这里,会有一个方法的重载 load(String args[])load() ,不过关系不大,最终都是会调用到那个无参的方法上的。

public void load() {

    if (loaded) {
return;
}
loaded = true; long t1 = System.nanoTime(); initDirs(); // Before digester - it may be needed
initNaming(); // Create and execute our Digester
Digester digester = createStartDigester(); InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
} // This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
} if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
} try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
} getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); // 前面都是在初始化加载各种配置文件,使用了 Digester
// Stream redirection
initStreams(); // Start the new server
try {
// 开始调用的 Server 的初始化方法
// Server 是一个接口,并且继承了 Lifecycle ,进行生命周期的管理
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
} long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}

这一大坨大多数都是在加载各种配置文件,在这里,会完成对 server.xml 文件的解析。

我们关心的其实就只有最后那段 try...catch 里面的那一句 getServer().init() ,开始调用 Server 的初始化方法,这个方法就是开启一系列容器组件的加载方法。

不过看到最后一句的 log 打印,忽然有印象了,之前在启动 Tomcat 的时候,在日志的最后部分,都会看到这句打印,而且可以看到的是,这里并不是我之前以为的是使用毫秒数算出来的,而是取的纳秒数通过除 1000000 计算得出的,难道是这样算的更准?

getServer().init() 实际上是调用了 org.apache.catalina.Server 中的 init() 方法,而 org.apache.catalina.Server 则是一个接口,还继承了 org.apache.catalina.Lifecycle 进行容器生命周期的管理。

而抽象类 org.apache.catalina.util.LifecycleBase 则是实现了 org.apache.catalina.Lifecycle 接口,我们在 org.apache.catalina.Lifecycle 中打开 init() 方法:

public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
} try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}

这里我们的关注点是 initInternal() 这个方法,从名字上来看就是一个初始化方法,点过去一看,结果是一个抽象方法,实现类如下:

我们到 org.apache.catalina.core.StandardServer 中去看下其中的 initInternal() 方法,在这个方法的最后,看到了循环 Service 并且调用了 service.init()

for (Service service : services) {
service.init();
}

因为一个 Server 是可以有多个 Service 的,所以这里用了一个循环,接下来就是一个顺序的容器初始化的调用过程:

StandardServer -> StandardService -> StandardEngine -> Connector

每个容器都在初始化自身相关设置的同时,将子容器初始化。

Tomcat 在启动初始化的时候,是通过链条式的调用,每初始化一个完成一个组件,就会在组件内调用下一个组件的初始化方法。

同样的操作在启动的时候也是一样的,不知道各位是否还记得我们在 Bootstrap.main() 中还有另一句代码 daemon.start()

public void start() throws Exception {
if (catalinaDaemon == null) {
init();
} Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}

操作嘛都是一样的操作,从这段代码开始,调用了 Catalina.start() 的方法,然后开启了另一个链式调用。

我就不多说了,留给各位读者自己去翻翻看源码吧。

参考

https://segmentfault.com/a/1190000023475177

https://zhuanlan.zhihu.com/p/75723328

Tomcat 第三篇:总体架构设计的更多相关文章

  1. MRS IoTDB时序数据库的总体架构设计与实现

    MRS IoTDB时序数据库的总体架构设计与实现 MRS IoTDB是华为FusionInsight MRS大数据套件最新推出的时序数据库产品,其领先的设计理念在时序数据库领域展现出越来越强大的竞争力 ...

  2. .NET Core实战项目之CMS 第九章 设计篇-白话架构设计

    前面两篇文章给大家介绍了我们实战的CMS系统的数据库设计,源码也已经上传到服务器上了.今天我们就好聊聊架构设计,在开始之前先给大家分享一下这几天我一直在听的<从零开始学架构>里面关于架构设 ...

  3. Netty4详解三:Netty架构设计(转)

    http://blog.csdn.net/suifeng3051/article/details/28861883?utm_source=tuicool&utm_medium=referral ...

  4. Netty4具体解释三:Netty架构设计

         读完这一章,我们基本上能够了解到Netty全部重要的组件,对Netty有一个全面的认识.这对下一步深入学习Netty是十分重要的,而学完这一章.我们事实上已经能够用Netty解决一些常规的问 ...

  5. PowerBI入门 第三篇:报表设计技巧

    最近做了几个PowerBI报表,对PowerBI的设计有了更深的理解,对数据的塑形(sharp data),不仅可以在Data Source中实现,例如在TSQL查询脚本中,而且可以在PowerBI中 ...

  6. PowerBI开发 第三篇:报表设计技巧

    最近做了几个PowerBI报表,对PowerBI的设计有了更深的理解,对数据的塑形(sharp data),不仅可以在Data Source中实现,例如在TSQL查询脚本中,而且可以在PowerBI中 ...

  7. 一个简单可参考的API网关架构设计

    网关一词较早出现在网络设备里面,比如两个相互独立的局域网段之间通过路由器或者桥接设备进行通信, 这中间的路由或者桥接设备我们称之为网关. 相应的 API 网关将各系统对外暴露的服务聚合起来,所有要调用 ...

  8. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  9. Day1:了解APICloud平台、理解APICloud应用设计思想、掌握平台使用流程。学习如何对一款APP进行需求分析、功能分解和架构设计等编码之前重要的准备工作

    学习目标 总体上了解一下APICloud平台,重点介绍相关的学习资源,入门资料,常见的FAQ等 明确我们这七天要开发一个什么样的APP,明确功能需求,跟上每天的课程节奏,可以课前预习 梳理出对于一款A ...

随机推荐

  1. 无锁机制----比较交换CAS Compare And Swap

    一.锁与共享变量 加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全. 无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加 ...

  2. 第3章 Hive数据类型

    第3章 Hive数据类型 3.1 基本数据类型 对于Hive的String类型相当于数据库的varchar类型,该类型是一个可变的字符串,不过它不能声明其中最多能存储多少个字符,理论上它可以存储2GB ...

  3. MySQL经典练习题及答案,常用SQL语句练习50题

    表名和字段 –1.学生表 Student(s_id,s_name,s_birth,s_sex) –学生编号,学生姓名, 出生年月,学生性别 –2.课程表 Course(c_id,c_name,t_id ...

  4. JDBC驱动程序分类

    JDBC驱动程序分类 JDBC驱动程序:各个数据库厂商根据JDBC的规范制作的 JDBC 实现类的类库 JDBC驱动程序总共有四种类型: 第一类:JDBC-ODBC桥. 第二类:部分本地API部分Ja ...

  5. CentOS7 系统基于Vim8搭建Go语言开发环境

    链接:https://pdf.us/2018/11/10/2194.html 问题1:vim-go: could not find 'gopls'. Run :GoInstallBinaries to ...

  6. 个人项目作业WC(JAVA)

    GitHub地址:https://github.com/1666403186/WC 一.题目描述 Word Count1. 实现一个简单而完整的软件工具(源程序特征统计程序).2. 进行单元测试.回归 ...

  7. 问卷星的数据导入spss后变量乱码如何处理?

    一般是字符编码问题.打开一个空的SPSS数据集,选择[编辑]-[选项]-[常规]-[数据和语法的字符编码].修改下当前的编码系统,原来是第一种就换成第二种,原来是第二种就换成第一种,打开一个数据再看看 ...

  8. Kernel_task占CPU问题

    彻底解决高版本 mac Kernel_task占CPU问题 https://blog.csdn.net/liumx2007/article/details/77164795

  9. 初学WebGL引擎-BabylonJS:第4篇-灯光动画与丛林场景

    前几章接触的案例都是接近静态的,由这张开始开始接触大量动态的内容,包括 球体灯光,变动的形体,以及一个虚拟的丛林场景 下章我会试着结合1-9案例的内容做出一个demo出来 [playground]-l ...

  10. 【Spring】IOC容器注解汇总,你想要的都在这儿了!!

    写在前面 之前,我们在[Spring]专题中更新了不少关于Spring注解相关的文章,有些小伙伴反馈说,看历史文章的话比较零散,经常会忘记自己看到哪一篇了.当打开一篇新文章时,总感觉自己似乎是看到过了 ...