大部分的中间件或者框架都有一些共性的部分,例如多线程、反射和类加载等。所以深入研究一两个中间件的话,面对其他的中间件,那么就会很容易理解它里面所用的技术以及原理。
tomcat目录结构:
bin:startup和shutdown脚本,sh—>linux,bat—>windows
lib:依赖jar包
logs:日志
temp:临时文件
webapps:web应用目录
work:tomcat的运行工作目录(通过webapps中的项目生成的。当客户端用户访问一个jsp文件时,tomcat会通过jsp生成java文件,然后再编译java文件生成class文件,生成的java和class文件都会存放到这个目录下。
conf:存放一些配置文件 像service.xml、web.xml
看一下service.xml文件:
1 | <!-- A "Service" is a collection of one or more "Connectors" that share |
可以从配置文件中看到一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connector(就是因为一个服务可以有多个连接,如同时提供Http和Https,也可以提供向相同协议不同端口的连接)。
tomcat连接器:
连接器的功能(可以从配置文件中里看到:
- 建立Socket连接,读取请求网络中的字节流。
- 根据相应的应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象并传给容器。
- 将容器返回的Tomcat Response对象转换为字节流并返回给客户端。
tomcat容器组件:
tomcat的容器就是来装Servlet的,容器的所有组件都实现了Container接口。
从配置文件中我们可以看到容器中的组件以及其关系:
Engine、Host、Context和Wrapper(配置文件中没。
从上至下依次是:
Engine—>Host—>Context—>Wrapper—>Servlet
如图:
- Engine,它对应的是顶层管理组件,一个Service最多只能有一个Engine,实现类为 org.apache.catalina.core.StandardEngine
- Host,它对应的是虚拟主机,一个 Host 代表一个虚拟主机,通过配置Host就可以添加一个主机,实现类为 org.apache.catalina.core.StandardHost
- Context,它对应的是webapp里面的每个应用文件夹,一个 Web 应用对应一个Context,实现类为 org.apache.catalina.core.StandardContext。
- Wrapper,它对应的是Context中的所有servlet,一个 Wrapper 代表一个 Servlet,管理访问关系与具体的Servlet的对应。实现类为org.apache.catalina.core.StandardWrapper。
※Container处理请求是使用Pipeline-Valve管道来处理的(类似数据结构中树形结构?),所以无论添加多少个Host或者多少个Context都可以找到对应的Servlet。
tomcat处理请求流程:
假设webapp下部署一个应用didistudy。
访问http://localhost:8080/didistudy/tomcat.xx是如何找到对应Servlet进行处理的流程如下:
发送请求到8080端口,连接器Connector获得请求
Connector把字节流转换为ServletRequest对象,然后交给Service下的容器组件Engine进行处理
Engine获得地址后然后匹配他下面名为localhost的Host主机
Host匹配到路径为/didistudy的Context(即在webapp下面找到相应的文件夹
Context匹配到URL规则为*.xx的servlet(即Wrapper对应为某个Servlet类
调用其doGet/doPost方法
Servlet执行完后将对象依次返回,Context—>Host—>Engine—>连接器Connector—>将对象解析为字节流发送给客户端
类加载器:
java类加载器
基本概念:
java源文件(.java):里面就是我们写的代码,但是不能被java虚拟机识别。
java类文件(.class):由java文件通过 javac这个命令(jdk本身提供的工具)编译生成,本质是一种二进制字节码文件,可以由java虚拟机加载(类加载)然后转换成 java.lang.Class 类的一个实例。
java虚拟机(JVM):识别.class文件,可以把.class文件加载到内存中,生成对应的java对象。还有内存管理、程序优化、锁管理等功能。所有的java程序最终都运行在jvm之上。
java运行过程:
java源文件(.java)—>通过编译器编译—>java类文件(.class)—>通过类加载器加载—>JVM class对象
类加载的触发:
JVM运行过程中,首先会加载初始类Main.class,然后再从初始类链接触发它相关的类的加载(如果Main.class中引用了A和C,就会触发A和C的加载,而没有引用B,B就不会被加载)。
通过打断点的方式看一下类加载流程
类加载器的介绍:
分为启动类加载器(BootStrapClassloader)和用户自定义类加载器。
**启动类加载器(BootstrapClassLoader)**:
使用C/C++代码写的,封装到JVM内核中,是虚拟机的一部分,用来加载JVM提供的基础运行类,即位于%JAVA_HOME%jre/lib 这个目录下面的核心类库,如包名为java、javax、sun等开头的类。
用户自定义类加载器:
由Java语言实现,独立于虚拟机外部并且全都继承抽象类java.lang.ClassLoader。
- java库中的平台类加载器PlatformClassLoader和应用程序类加载器AppClassLoader等。
- 自己写的类加载器,比如通过网络加载类等机制,一般来说,用户自定义类加载器以ClassLoader为基类,重写其中的findClass,使findClass可以从用户指定的位置读取字节码.class文件。
※上面提到的ClassLoader类是所有类加载器的基类,完成类的加载工作是通过调用 defineClass 来实现的;而启动类的加载过程是通过调用 loadClass 来实现的。
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器(下文介绍的双亲委派模型会用到)。 |
findClass(String name) | 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例()。 |
loadClass(String name) | 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。和findClass的不同之处在于:loadClass添加了双亲委派和判断 |
findLoadedClass(String name) | 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
※java中的类加载默认是采用双亲委派模型,即加载一个类时,首先判断自身define加载器有没有加载过此类,如果加载了直接获取class对象,如果没有查到,则交给加载器的父类加载器去重复上面过程。
由下到上的关系;
用户类加载器—>AppClassLoader—>PlatformClassLoader—>BootstrapClassLoader
很多具体的可以看看https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html
tomcat的类加载器:
来看一下Tomcat9版本的官方文档给出的Tomcat的类加载器图:
https://tomcat.apache.org/tomcat-9.0-doc/class-loader-howto.html
- Bootstrap : 加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)。
- System: 加载tomcat启动的类,比如bootstrap.jar,tomcat-guli.jar。通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下
- Common:加载tomcat自身使用以及部署在其上面的应用程序通用的一些类,加载路径可以在catalina.properties中common.loader属性配置。
- Server:负责加载Tomcat专用的类。
- Shared:负责加载Tomcat下所有的Web应用程序的类。
- Webapp1、Webapp2……: 会加载webapp路径下项目中的所有的类。一个项目对应一个WebappClassLoader,这样就实现了应用之间类的隔离了。
※上面有提到WebAppClassLoaser,它是各个Webapp私有的类加载器,该加载器加载的类库不仅包括每个web应用下/WEB-INF/classes目录下的所有.class类和资源文件,还包括web应用下/WEB-INF/lib目录下所有的jar类库。加载路径中的class只对当前Webapp可见。
tomcat的类加载流程:
写个简单的servlet,然后对类加载器信息进行在浏览器页面输出即可。
可以看出这些类加载器的关系。
不过tomcat程序的加载类的过程与JVM加载类的双亲委托机制是有些差异的。
在web应用加载某个类的时候,WebappX类加载器首先尝试加载该类,其余的类加载器都会按照JVM中的双亲委托模型来加载该类,如果在配置文件中配置了
那tomcat的类加载器加载类的过程总结就如下:
Bootstrap(包含JVM的bootstrapClassLoader,extClassLoader加载的所有基础的,依赖的类)
->/WEB-INF/Classes/*.class(WebappClassLoader )
->/WEB-INF/lib/*.jar(WebappClassLoader)
-> System tomcat(与JVM中的System类加载器appClassLoader不同)加载的相关类集合 (%CATALINA_HOME%\bin\catalina.bat)
-> Common加载的相关类集合(%CATALINA_HOME%\conf\catalina.properties)
tomcat的war包部署:
.war里面包括写的代码编译成的class文件等等。
在Tomcat的代码中有一个判断,只有当程序为.war结尾时才会去走以下的逻辑。用.war包结尾的就被Tomcat视为一个项目。(注意:但是将其打包为.jar包放在Tomcat文件夹下是无法生效的,因为在Java中一般.jar包是一个依赖包而且源码中也有文件类型判断。)
这里有两种模式
war模式:将WEB工程以war包的形式上传到Tomcat服务器 ,存放于服务器的webapps目录下。
war exploded模式:将WEB工程以当前文件夹的位置关系上传到服务器,包括文件夹、jsp页面 、classes等等移到Tomcat 部署文件夹里面,进行加载部署。(因此这种方式支持热部署)。
tomcat部署war包getshell,不过有时候war包还不一定有效,如果没开热部署,就不会自动解压了。
这里提到两个概念:
热部署(由host负责):Tomcat在启动的时候会将其目录下webapp中war包解压后然后封装为一个Context供外部访问。那么热部署就是在程序运行时,如果我们修改了War包中的东西。那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包。
热加载(由Context负责):Tomcat默认情况下是不开启热加载的。服务器会监听 class 文件改变,包括web-inf/class,wen-inf/lib,web-inf/web.xml等文件,若发生更改,则局部进行加载,不清空session ,不释放内存。
tomcat在host中做WAR解压和部署相关的工作。
HostConfig实现了LifecycleListener接口,它是一个监听器,作用是部署应用。
看一下它其中deployApps的代码:
1 | protected void deployApps() { |
再进一步看一下deployWARs的代码
tomca创建servlet类实例:
当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对xml文件进行解析,并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过反射的方式实例化。
参考文章:
https://www.jianshu.com/p/efd19125255c
https://juejin.cn/post/6844903881067986957
https://cloud.tencent.com/developer/article/1913028?from=article.detail.1890176