首先了解下Tomcat的整体架构
首先要了解它是干什么的,Tomcat我们都知道,是用于处理连接过来的Socket请求的。那么Tomcat就会有两个功能:
- 对外处理连接,将收到的字节流转化为自己想要的Request和Response对象
- 对内处理Servlet,将对应的Request请求分发到相应的Servlet中 Tomcat其实就分为两大部分,一部分是连接器(Connnector)处理对外连接和容器(Container)管理对内的Servelet。 我们可以打开在Tomcat目录配置文件中server.xml中看出来一个Tomcat服务可以对应多个Service。每个Service都有连接器和容器
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8010" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
</Host>
</Engine>
可以看到连接器其实就是Connector,一个Service中可以有多个连接器,容器其实对应的就是Engine。Tomcat的整体架构简单来说就是这样的对应关系。接下来我们简单的介绍连接器的整体架构和容器的整体架构。
连接器
我们可以知道连接器传给容器的是ServletRequest对象,而容器传给连接器的是ServletResponse对象,这些在网络传输过程中是肯定不行的,因为网络传输中传送的字节流。所以连接器的功能需求我们大概能总结出来以下几点。
- Socket连接
- 读取请求网络中的字节流
- 根据相应的协议(Http/AJP)解析字节流,生成统一的
Tomcat Requestt对象 - 将
Tomcat Reques传给容器 - 容器返回
Tomcat Response对象 - 将
Tomcat Response对象转换为字节流 - 将字节流返回给客户端
其实上面的细分都能总结为以下的三点
- 网络通信
- 应用层协议的解析
- Tomcat的
Request/Response与ServletRequest/ServletResponse对象的转化
而在Tomcat中它也用了三个类来实现上面的三个功能,分别对应如下
- EndPoint
- Processor
- Adapter
容器
容器,顾名思义就是装东西的器具,那么这个Tomcat容器是装什么的呢?其实主要的就是装了Servlet的。那么容器是如何设计的呢?Tomcat的容器设计其实是用了组合设计模式。其实从Server.xml中我们也能看到其关系了。
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
在这里面我们只能看到容器中的两个模块,一个是顶层模块Engine,另一个是Host,其实还有两个模块,一个是Context对应的是我们webapp里面的每个应用文件夹,每个文件夹就是对应一个Context,还有一个模块Wrapper对应的是我们Context中的所有servlet,Wrapper管理了访问关系与具体的Servlet的对应。
Tomcat中容器所有模块都实现了Container接口,而组合模式的意义就是使得用户对于单个对象和组合对象的使用具有一致性,即无论添加多少个Context其使用就是为了找到其下面的Servlet,而无论添加多少个Host也是为了找个下面的Servlet。而在容器中设计了这么多的模块,一个请求过来Tomcat如何找到对应的Servlet进行处理呢?
请求如何定位
我们就举个最简单的例子,我们本机应用上启动了一个Tomcat,webapp下有我们部署的一个应用helloword。我们在浏览器上输入http://localhost:8080/helloword/add.do是如何找到对应Servlet进行处理呢?
在我们启动Tomcat的时候,连接器就会进行初始化监听所配置的端口号,这里我们配置的是8080端口对应的协议是HTTP。
- 请求发送到本机的8080端口,被在那里监听的HTTP/1.1的连接器Connector获得
- 连接器Connector将字节流转换为容器所需要的
ServletRequest对象给同级Service下的容器模块Engine进行处理 - Engine获得地址
http://localhost:8080/helloword/add。匹配他下面的Host主机 - 匹配到名为localhost的Host(就算此时请求为具体的ip,没有配置相应的Host,也会交给名为localhost的Host进行处理,因为他是默认的主机)
- Host匹配到路径为
/helloword的Context,即在webapp下面找到相应的文件夹 - Context匹配到URL规则为*.do的servlet,对应为某个Servlet类
- 调用其
doGet或者doPost方法 - Servlet执行完以后将对象返回给Context
- Context返回给Host
- Host返回给Engine
- Engine返回给连接器Connector
- 连接器Connector将对象解析为字节流发送给客户端
Java中的类加载器
类加载器负责在程序运行时将java文件动态加载到JVM中
从Java虚拟机的角度来讲的话,存在两种不同的类加载器:
-
启动类加载器(Bootstrap ClassLoader):这个类加载器是使用C++语言实现的,是虚拟机自身的一部分。
-
其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader,其中其他类加载器大概又分为- ExtensionClassLoader:这个类加载器由
ExtClassLoader实现,它负责加载JAVA_HOME/lib/ext目录中的所有类,或者被java.ext.dir系统变量所指定的路径中所有的类。 - ApplicationClassLoader:这个类加载器是由
AppClassLoader实现的,它负责加载用户类路径(ClassPath)上所指定的所有类,如果应用中没有自定义自己的类加载器,那么一般情况就是程序中默认的类加载器。 - 自定义加载器:根据自己需求,自定义加载特定路径的加载器。
- ExtensionClassLoader:这个类加载器由
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其他加载器都应该有自己的父加载器。这里的父子关系不是通过继承来实现的,而是通过设置parent变量来实现的。
双亲委派模型工作过程是:如果收到一个类加载的请求,本身不会先加载此类,而是会先将此请求委派给父类加载器去完成,每个层次都是如此,直到启动类加载器中,只有父类都没有加载此文件,那么子类才会尝试自己去加载。
为什么要设置双亲委派模型呢?其实是为了保证Java程序的稳定运行,例如Object类,它是存放在rt.jar中,无论哪一个类加载器要加载Object类,最终都会委托给顶层的BootStrapClassLoader,所以所有的类中使用的Object都是同一个类,相反如果没有双亲委派模型的话,那么随意一个类加载器都可以定义一个新的Object类,那么应用程序将会变得非常混乱。其实双亲委派模型代码非常简单。实现在ClassLoader中的loadClass方法下。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果没被本类类加载器加载过,先委托给父类进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有父类,则表明在顶层,就交给BootStrap类加载器加载
c = findBootstrapClassOrNull(name);
}
// 如果最顶层的类也找不到,那么就会抛出ClassNotFoundException异常
} catch (ClassNotFoundException e) {
}
// 如果父类都没有加载过此类,子类才开始加载此类
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
复制代码
我们可以看到findClass方法是需要子类自己去实现的逻辑。
Tomcat中的类加载器
下面的简图是Tomcat9版本的官方文档给出的Tomcat的类加载器的图。
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ..
- Bootstrap : 是Java的最高的加载器,用C语言实现,主要用来加载JVM启动时所需要的核心类,例如
$JAVA_HOME/jre/lib/ext路径下的类。 - System: 会加载
CLASSPATH系统变量所定义路径的所有的类。 - Common:可以被tomcat以及所有的外部程序使用,会加载Tomcat路径下的lib文件下的所有类。
- Webapp1、Webapp2……: 只能被web应用所使用,会加载webapp路径下项目中的所有的类。一个项目对应一个WebappClassLoader,这样就实现了应用之间类的隔离了。
这3个部分,在上面的Java双亲委派模型图中都有体现。不过可以看到ExtClassLoader没有画出来,可以理解为是跟bootstrap合并了,都是去JAVA_HOME/jre/lib下面加载类。 那么Tomcat为什么要自定义类加载器呢?
- 隔离不同应用:部署在同一个Tomcat中的不同应用A和B,例如A用了Spring2.5。B用了Spring3.5,那么这两个应用如果使用的是同一个类加载器,那么Web应用就会因为jar包覆盖而无法启动。
- 灵活性:Web应用之间的类加载器相互独立,那么就可以根据修改不同的文件重建不同的类加载器替换原来的。从而不影响其他应用。
- 性能:如果在一个Tomcat部署多个应用,多个应用中都有相同的类库依赖。那么可以把这相同的类库让Common类加载器进行加载。
Tomcat自定义了WebAppClassLoader类加载器。打破了双亲委派的机制,即如果收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类。我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat既然要打破这个规则,就要重写loadClass方法,我们可以看WebAppClassLoader类中重写的loadClass方
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 从本地缓存中查找是否加载过此类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 2. 从AppClassLoader中查找是否加载过此类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
// 3. 尝试用ExtClassLoader 类加载器加载类,防止Web应用覆盖JRE的核心类
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
tryLoadingFromJavaseLoader = true;
}
boolean delegateLoad = delegate || filter(name, true);
// 4. 判断是否设置了delegate属性,如果设置为true那么就按照双亲委派机制加载类
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
最后借用Tomcat官网上的话总结:
Web应用默认的类加载顺序是(打破了双亲委派规则):
- 先从JVM的BootStrapClassLoader中加载。
- 加载Web应用下
/WEB-INF/classes中的类。 - 加载Web应用下
/WEB-INF/lib/*.jap中的jar包中的类。 - 加载上面定义的System路径下面的类。
- 加载上面定义的Common路径下面的类。
如果在配置文件中配置了<Loader delegate="true"/>,那么就是遵循双亲委派规则,加载顺序如下:
- 先从JVM的BootStrapClassLoader中加载。
- 加载上面定义的System路径下面的类。
- 加载上面定义的Common路径下面的类。
- 加载Web应用下
/WEB-INF/classes中的类。 - 加载Web应用下
/WEB-INF/lib/*.jap中的jar包中的类。