Engine即为全局引擎容器,它的标准实现是StandardEngine。
Host在整个Servlet引擎中抽象出Host容器用于表示虚拟主机,它是根据URL地址中的主机部分抽象的,一个Servlet引擎可以包含若干个Host容器,而一个Host容器可以包含若干个Context容器。在Tomcat中Host的标准实现是StandardHost,它从虚拟主机级别对请求和响应进行处理。
一个Context对应一个Web应用程序,但Web项目的组成比较复杂,它包含很多组件。对于Web容器,需要将Web应用程序包含的组件转换成容器的组件。
Wrapper属于Tomcat中4个级别容器中最小级别的容器,与之相对应的是Servlet。
文章目录
1. Container
下面有段注释十分重要:
- Engine - 整个Catalina servlet引擎的表示, 引擎 -整个Catalina servlet引擎的表示,
- Host -包含数字的虚拟主机的表示形式的上下文。
- Context - 表示单个ServletContext,它将通常为受支持的servlet包含一个或多个包装器。
- Wrapper -单个servlet定义的表示
/** * A Container是一个对象,它可以执行从 *客户端,并返回基于这些请求的响应。一个容器可以 *可选地支持一个管道阀门处理请求 *通过实现管道接口,在运行时配置顺序 *。 *容器将存在于Catalina的几个概念层次上。的 *以下是常见的例子: * <ul> * <li><b>Engine</b> - 整个Catalina servlet引擎的表示, 引擎 -整个Catalina servlet引擎的表示, * <li><b>Host</b> -包含数字的虚拟主机的表示形式的上下文。 * <li><b>Context</b> - 表示单个ServletContext,它将 *通常为受支持的servlet包含一个或多个包装器。 * <li><b>Wrapper</b> -单个servlet定义的表示 * (如果servlet本身支持多个servlet实例) *实现SingleThreadModel)。 * </ul> *给定的卡特琳娜部署不需要包括所有的容器 *以上描述的级别。例如,管理应用程序 *嵌入式网络设备(如路由器)可能只包含 *一个上下文和几个包装器,甚至一个包装器如果 应用程序相对较小。因此,容器实现 *需要进行设计,使它们能在不在场的情况下正常运行 *指定部署中的父容器。 * <p> *容器还可以与许多支持组件相关联 *提供可能被共享的功能(通过附加到 *父容器)或单独定制。以下支持 *目前认可的组件: * <ul> * <li><b>Loader</b> - 用于集成新Java类的类装入器 将此容器放入运行Catalina的JVM中。 * <li><b>Logger</b> -实现log()方法 * ServletContext接口的属性。 * <li><b>Manager</b> -相关联的会话池的管理这个容器。 * <li><b>Realm</b> - 安全域的只读接口,用于验证用户身份及其对应角色。 * <li><b>Resources</b> - 支持访问静态的JNDI目录上下文 *资源,启用自定义链接到现有的服务器组件时 ,Catalina被嵌入到一个更大的服务器中。 * </ul> */ public interface Container extends Lifecycle {
1.1 ContainerBase
ContainerBase实现Container,定义了容器所需要的一些共有组件与方法。
public abstract class ContainerBase extends LifecycleMBeanBase implements Container { /** * 属于此容器的子容器,按名称键入。 */ protected final HashMap<String, Container> children = new HashMap<>(); /** * 处理器延迟这个组件。 */ protected int backgroundProcessorDelay = -1; /** *此容器的容器事件监听器。实现为一个 * CopyOnWriteArrayList,因为监听器可能会调用方法来添加/删除 *自己或其他听众,并与读写洛克,将触发 *死锁。 */ protected final List<ContainerListener> listeners = new CopyOnWriteArrayList<>(); /** 与此容器相关联的日志程序实现。 */ protected Log logger = null; /** *关联的日志记录器名称。 */ protected String logName = null; /** 与此容器相关联的集群。 */ protected Cluster cluster = null; private final ReadWriteLock clusterLock = new ReentrantReadWriteLock(); /** *容器名称。 */ protected String name = null; /** * 此容器是其子容器的父容器。 */ protected Container parent = null; /** *安装加载器时要配置的父类加载器。 */ protected ClassLoader parentClassLoader = null; /** * 与此容器相关联的管道对象。 */ protected final Pipeline pipeline = new StandardPipeline(this); /** * 与此容器相关联的领域。 */ private volatile Realm realm = null; /** * 用于控制对领域的访问的锁。 */ private final ReadWriteLock realmLock = new ReentrantReadWriteLock(); /** * 这个包的字符串管理器。 */ protected static final StringManager sm = StringManager.getManager(Constants.Package); /** * 当添加子节点时是否会自动启动。 */ protected boolean startChildren = true; /** *此组件的属性更改支持。 */ protected final PropertyChangeSupport support = new PropertyChangeSupport(this); /** *后台线程。 */ private Thread thread = null; /** *后台线程完成信号。 */ private volatile boolean threadDone = false; /** *该容器通常处理的请求使用的访问日志 *在处理链中较早处理的。 */ protected volatile AccessLog accessLog = null; private volatile boolean accessLogScanComplete = false; /** *任何可用于处理启动和停止事件的线程数 *与此容器关联的子容器。 */ private int startStopThreads = 1; protected ThreadPoolExecutor startStopExecutor;
2. Engine接口
注意Engine接口与Host接口都继承了Container,其共有的组件都继承自ContainerBase类。
/** * An <b>Engine</b> 是容器是否表示整个Catalina servlet * engine. 它在以下类型的场景中非常有用: *您希望使用拦截器来查看每个被处理的请求整个引擎。 您希望使用独立的HTTP连接器运行Catalina,但仍然是这样希望支持多个虚拟主机。 *一般来说,你不会使用引擎部署卡特琳娜连接 *连接到web服务器(如Apache),因为连接器将具有 *利用web服务器的功能来确定上下文(或 *甚至可能是哪个包装)应该被用来处理这个请求。 * 附加到引擎的子容器通常是实现 主机(表示虚拟主机)或上下文(表示个体)的* *一个单独的servlet上下文),取决于引擎实现。 *如果使用引擎,引擎总是卡特琳娜的顶级容器 *层次结构。因此,实现的setParent()方法 *应该抛出IllegalArgumentException。 */ public interface Engine extends Container { /** * @return 获取此引擎的默认主机名。 */ public String getDefaultHost(); /** * 设置此引擎的默认主机名。 * * @param defaultHost The new default host */ public void setDefaultHost(String defaultHost); /** * @return 此引擎的JvmRouteId。 */ public String getJvmRoute(); /** * 设置此引擎的JvmRouteId。 * * @param jvmRouteId(新的)JVM路由ID。集群中的每个引擎 * 必须有唯一的JVM路由ID。 */ public void setJvmRoute(String jvmRouteId); public Service getService(); /** *设置与我们关联的service (如果有的话)。 * * @param 拥有这个引擎的服务 */ public void setService(Service service); }
3. StandardEngine
Engine标准实现是StandardEngine,包含的主要组件有Host组件、AccessLog组件、Pipeline组件、Cluster组件、Realm组件、LifecycleListener组件和Log组件。
组件主要继承自org.apache.catalina.core.ContainerBase:-
Host
Host组件是Engine容器的子容器,它表示一个虚拟主机。Host也包含很多其他的组件。 -
AccessLog
Engine容器里的AccessLog组件负责客户端请求访问日志的记录。因为Engine容器是一个全局的Servlet容器,所以这里的访问日志作用的范围是所有客户端的请求访问,不管访问哪个虚拟主机都会被该日志组件记录。 -
Pipeline
Pipeline其实属于一种设计模式,在Tomcat中可以认为它是将不同容器级别串联起来的通道,当请求进来时就可以通过管道进行流转处理。Tomcat中有4个级别的容器,每个容器都会有一个属于自己的Pipeline。 -
Cluster
Tomcat中有Engine和Host两个级别的集群,而这里的集群组件正是属于全局引擎容器。它主要把不同JVM上的全局引擎容器内的所有应用都抽象成集群,让它们能在不同的JVM之间互相通信,使会话同步、集群部署得以实现。 -
2.5 Realm
Realm对象其实就是一个存储了用户、密码及权限等的数据对象,它的存储方式可能是内存、xml文件或数据库等。它的作用主要是配合Tomcat实现资源认证模块。
在配置文件的<Engine>节点下配置Realm,则在启动时对应的域会添加到Engine容器中。 -
LifeCycleListener
Engine容器内的生命周期监听器是为了监听Tomcat从启动到关闭整个过程的某些事件,然后根据这些事件做不同的逻辑处理。 -
Log
日志组件负责的事情就是不同级别的日志输出,几乎所有系统都有日志组件。
4. Host接口
/** * A Host是一个容器,它表示一个虚拟主机在 * Catalina servlet引擎。它在下列情况下是有用的: * <ul> * <li>您希望使用拦截器来查看每个被处理的请求 *由这个特定的虚拟主机。 * <li>您希望使用独立的HTTP连接器运行Catalina,但仍然是这样 *希望支持多个虚拟主机。 * </ul> *一般来说,你不会在部署Catalina connected时使用主机 *连接到web服务器(如Apache),因为连接器将具有 *利用web服务器的功能来确定上下文(或 *甚至可能是哪个包装)应该被用来处理这个请求。 * <p> *连接到主机的父容器通常是一个引擎,但也可能是 *是一些其他的实现,或者可能被省略,如果没有必要。 * <p> 附加到主机的子容器通常是实现 上下文的*(表示单个servlet上下文)。 */ public interface Host extends Container {
接口内方法如下:
5. StandardHost
Host容器包含了若干Context容器、AccessLog组件、Pipeline组件、Cluster组件、Realm组件、HostConfig组件和Log组件。
其中的AccessLog组件、Pipeline组件、Cluster组件、Realm组件和Log组件,继承自ContainerBase。- Context
每个Host容器包含若干个Web应用(Context)。对于Web项目来说,其结构相对比较复杂,而且包含很多机制,Tomcat需要对它的结构进行解析,同时还要具体实现各种功能和机制,这些复杂的工作就交给了Context容器。Context容器对应实现了Web应用包含的语义,实现了Servlet和JSP的规范。
/** * The Java class name of the default Context implementation class for * deployed web applications. */ private String contextClass = "org.apache.catalina.core.StandardContext";
Context也是比较复杂的一块。
- AccessLog
Host容器的访问日志作用的范围是该虚拟主机的所有客户端的请求访问,不管访问哪个应用都会被该日志组件记录。 - Pipeline
不同级别容器的管道完成的工作都不一样,每个管道要搭配阀门(Valve)才能工作。Host容器的Pipeline默认以StandardHostValve作为基础阀门,这个阀门主要的处理逻辑是先将当前线程上下文类加载器设置成Context容器的类加载器,让后面Context容器处理时使用该类加载器,然后调用子容器Context的管道。 - Cluster略
- Realm略
- HostConfig
见下一小节。
5.1 HostConfig
Host作为虚拟主机容器用于放置Context级别容器,而Context其实对应的就是Web应用,实际上每个虚拟主机可能会对应部署多个应用,每个应用都有自己的属性。
当Tomcat启动时,必须把对应Web应用的属性设置到对应的Context中,根据Web项目生成Context,并将Context添加到Host容器中。另外,当我们把这些Web应用程序复制到指定目录后,还有一个重要的步骤就是加载,把Web项目加载到对应的Host容器内。
当Tomcat启动时,它必须把所有Web项目都加载到对应的Host容器内,完成这些任务的就是HostConfig监听器。
HostConfig实现了Lifecycle接口,当START_EVENT事件发生时则会执行Web应用部署加载动作。
Web应用有三种部署类型:
Descriptor描述符、WAR包以及目录。所以部署时也要根据不同的类型做不同的处理。Descriptor描述符略过,感兴趣的可以自行搜索。
WAR包类型如下:
WAR包类型的部署是直接读取%CATALINA_HOME%/webapps目录下所有以war包形式打包的Web项目,然后根据war包的内容生成Tomcat内部需要的各种对象。
为了优化多个应用项目部署时间,使用了线程池和Future机制。
目录类型如下:
目录类型的部署是直接读取%CATALINA_HOME%/webapps目录下所有目录形式的Web项目。
优化同上。5.2 代码跟踪
-> org.apache.catalina.core.StandardHost#startInternal,调用父类
-> org.apache.catalina.core.ContainerBase#startInternal 调用setState
-> org.apache.catalina.util.LifecycleBase#setStateInternal 调用fireLifecycleEvent方法,执行监听任务protected void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(this, type, data); for (LifecycleListener listener : lifecycleListeners) { listener.lifecycleEvent(event); } }
-> org.apache.catalina.startup.HostConfig#lifecycleEvent
@Override public void lifecycleEvent(LifecycleEvent event) { // Identify the host we are associated with try { host = (Host) event.getLifecycle(); if (host instanceof StandardHost) { setCopyXML(((StandardHost) host).isCopyXML()); setDeployXML(((StandardHost) host).isDeployXML()); setUnpackWARs(((StandardHost) host).isUnpackWARs()); setContextClass(((StandardHost) host).getContextClass()); } } catch (ClassCastException e) { log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { check(); } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) { beforeStart(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { stop(); } }
-> org.apache.catalina.startup.HostConfig#start
/** *为找到的任何目录或WAR文件部署应用程序 *在我们的“应用程序根目录”中。 */ protected void deployApps() { File appBase = host.getAppBaseFile(); File configBase = host.getConfigBaseFile(); String[] filteredAppPaths = filterAppPaths(appBase.list()); // Deploy XML descriptors from configBase deployDescriptors(configBase, configBase.list()); // Deploy WARs deployWARs(appBase, filteredAppPaths); // Deploy expanded folders deployDirectories(appBase, filteredAppPaths); }
6. Context接口
/** * Context是一个表示servlet上下文的容器 因此,一个独立的web应用程序,在Catalina servlet引擎中。 因此,它在几乎所有的部署Catalina(即使a 连接到web服务器(如Apache)的连接器使用web服务器的 *识别处理此请求的适当包装器的功能。 *它还提供了一种方便的机制来使用see的拦截器 每个请求由这个特定的web应用程序处理。 * <p> 附加到上下文的父容器通常是宿主,但也可能是宿主 *是一些其他的实现,或者可能被省略,如果没有必要。 * <p> 附加到上下文的子容器通常是实现 包装器的*(表示单独的servlet定义)。 * <p> */ public interface Context extends Container, ContextBind {
下面以StandardContext为例。6.1 Context相关的配置文件
- Tomcat的server.xml配置文件中的节点可用于配置Context,它直接在Tomcat解析server.xml时就完成Context对象的创建,而不用交给HostConfig监听器创建。
- Web应用的/META-INF/context.xml文件可用于配置Context,此配置文件用于配置该Web应用对应的Context属性。
- 用%CATALINA_HOME%/conf/[EngineName]/[HostName]/[WebName].xml文件声明创建一个Context。
- Tomcat全局配置为conf/context.xml,此文件配置的属性会设置到所有Context中。
- Tomcat的Host级别配置文件为/conf/[EngineName]/[HostName]/context.xml.default文件,它配置的属性会设置到某Host下面的所有Context中。
6.2 StandardContext
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
其继承的ContainerBase的组件就不过多介绍。
- Wrapper
Context容器会包含若干个子容器,这些子容器就叫Wrapper容器。它属于Tomcat中最小级别的容器,它不能再包含其他子容器,而且它的父容器必须为Context容器。每个Wrapper其实就对应一个Servlet, Servlet的各种定义在Tomcat中就Wrapper的形式存在。
/** *将要添加的LifecycleListeners的类名集 *通过createWrapper()来创建每个新创建的包装器。 */ private String wrapperLifecycles[] = new String[0];
- ErrorPage
每个Context容器都拥有各自的错误页面对象,它用于定义在Web容器处理过程中出现问题后向客户端展示错误信息的页面,这也是Servlet规范中规定的内容。它可以在Web部署描述文件中配置。
private final ErrorPageSupport errorPageSupport = new ErrorPageSupport();
- Manager
Context容器的会话管理器用于管理对应Web容器的会话,维护会话的生成、更新和销毁。每个Context都会有自己的会话管理器,如果显式在配置文件中配置了会话管理器,则Context容器会使用该会话管理器;否则,Tomcat会分配默认的标准会话管理器(StandardManager)。
/** * 与此容器关联的管理器实现。 */ protected Manager manager = null;
- DirContext
DirContext接口其实是属于JNDI的标准接口,实现此接口即可实现目录对象相关属性的操作。
例如:通过“/META-INF/context.xml”获取到context.xml的文件。 - JarScanner
/** * Jar扫描器用于搜索可能包含的Jar *配置信息,如tld或web-fragment.xml文件。 */ private JarScanner jarScanner = null;
从JarScanner的名字上已经知道它的作用了,它一般包含在Context容器中,专门用于扫描Context对应的Web应用的Jar包。每个Web应用初始化时,在对TLD文件和web-fragment.xml文件处理时都需要对该Web应用下的Jar包进行扫描,因为Jar包可能包含这些配置文件,Web容器需要对它们进行处理。
JarScanner在设计上采用了回调机制,每扫描到一个Jar包时都会调用回调对象进行处理。回调对象需要实现JarScannerCallback接口。-
过滤器
过滤器提供了为某个Web应用的所有请求和响应做统一逻辑处理的功能。也就是我们意义上理解的过滤器。 -
NamingResource
将配置文件中声明的不同的资源及其属性映射到内存中,这些映射统一由NamingResource对象封装
命名资源的配置有两个地方,分别为Tomcat容器的server.xml文件和每个Web项目的context.xml文件。 -
Mapper
Context容器包含了一个请求路由映射器(Mapper)组件,它属于局部路由映射器,它只能负责本Context容器内的路由导航。即每个Web应用包含若干Servlet,而当对请求使用请求分发器RequestDispatcher以分发到不同的Servlet上处理时,就用了此映射器。 -
WebappLoader(重点部分)
每个Web应用都有各自的Class类和Jar包。一般来说,在Tomcat启动时要准备好相应的类加载器,包括加载策略及Class文件的查找,方便后面对Web应用实例化Servlet对象时通过类加载器加载相关类。因为每个Web应用不仅要达到资源的互相隔离,还要能支持重加载,所以这里需要为每个Web应用安排不同的类加载器对象加载,重加载时可直接将旧的类加载器对象丢弃而使用新的。
每个Web应用对应一个WebappLoader,每个WebappLoader互相隔离,各自包含的类互相不可见。
- ApplicationContext
/** * 与此上下文关联的ServletContext实现。 */ protected ApplicationContext context = null;
在Servlet的规范中规定了一个ServletContext接口,它提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。
首先来看ServletContext,包属于JDK的包:package javax.servlet;
等等方法。。。
对于Tomcat容器,Context容器才是其运行时真正的环境。为了满足Servlet规范,它必须要包含一个ServletContext接口的实现,这个实现就是ApplicationContext。ApplicationContext是ServletContext的标准实现,用它表示某个Web应用的运行环境,每个Tomcat的Context容器都会包含一个ApplicationContext。public class ApplicationContext implements ServletContext {
ApplicationContext对ServletContext接口的所有方法都进行了实现,所以Web开发人员可以在Servlet中通过getServletContext()方法获得该上下文,进而再对上下文进行操作或获取上下文中的各种资源。但实际上getServletContext()获取到的并非ApplicationContext对象,而是一个ApplicationContext的门面对象ApplicationContextFacade。
门面模式的作用就是提供一个类似代理的访问模式,把ApplicationContext里面不该暴露的方法和属性屏蔽掉,不让Web开发人员访问。ApplicationContext就是为了满足Servlet标准的ServletContext接口而实现的一个类,它按Servlet的规范要求提供了各种实现方法。
-
InstanceManager
Context容器中包含了一个实例管理器,它主要的作用就是实现对Context容器中监听器、过滤器以及Servlet等实例的管理。其中包括根据监听器Class对其进行实例化,对它们的Class的注解进行解析并处理,对它们的Class实例化的访问权限的限制,销毁前统一调用preDestroy方法等。 -
ServletContainerInitializer
在Web容器启动时为让第三方组件机做一些初始化工作,例如注册Servlet或者Filters等,Servlet规范中通过ServletContainerInitializer实现此功能。
/** *方法中的一个条目注册了ServletContainerInitializers (SCIs) * 文件meta - inf / services / javax.servlet。ServletContainerInitializer必须是包含在包含SCI实现的JAR文件中。 * <p> * SCI处理的执行与元数据完成的设置无关。 * SCI处理可以通过片段排序来控制每个JAR文件。如果 *定义了绝对顺序,然后只将jar包含在顺序中 *将为科学资讯系统处理。若要完全禁用SCI处理,请使用空 *可以定义绝对顺序。 * <p> *SCIs注册对注释(类、方法或字段)和/或感兴趣 *通过{@link javax.servlet.annotation类型。HandlesTypes}注释,被添加到类中。 */ public interface ServletContainerInitializer { /** * Receives notification during startup of a web application of the classes * within the web application that matched the criteria defined via the * {@link javax.servlet.annotation.HandlesTypes} annotation. * * @param c The (possibly null) set of classes that met the specified * criteria * @param ctx The ServletContext of the web application in which the * classes were discovered * * @throws ServletException If an error occurs */ void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; }
每个框架要使用ServletContainerInitializer就必须在对应的Jar包的META-INF/services目录中创建一个名为javax.servlet.ServletContainerInitializer的文件。文件内容指定具体的ServletContainerInitializer实现类,于是,当Web容器启动时,就会运行这个初始化器做一些组件内的初始化工作。
Tomcat容器的ServletContainerInitializer机制,主要交由Context容器和ContextConfig监听器共同实现。ContextConfig监听器首先负责在容器启动时读取每个Web应用的WEB-INF/lib目录下包含的Jar包的META-INF/services/javax.servlet.ServletContainerInitializer,以及Web根目录下的META-INF/services/javax.servlet.ServletContainerInitializer,通过反射完成这些Servlet ContainerInitializer的实例化,然后再设置到Context容器中。最后,Context容器启动时就会分别调用每个ServletContainerInitializer的onStartup方法,并将感兴趣的类作为参数传入。
- Context容器的监听器
Tomcat启动过程中一般默认会在Context容器中添加4个监听器,分别为ContextConfig、TldConfig、NamingContextListener以及MemoryLeakTrackingListener。
ContextConfig监听器主要负责在适当的阶段对Web项目的配置文件进行相关处理;TldConfig监听器主要负责对TLD标签配置文件的相关处理;NamingContextListener监听器主要负责对命名资源的创建、组织、绑定等相关的处理工作,使之符合JNDI标准;MemoryLeakTrackingListener监听器主要用于跟踪重加载可能导致内存泄漏的相关处理。
6.3 Tomcat的类加载机制
贴一篇写得很不错的博客
org.apache.catalina.core.StandardContext#startInternalif (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
WebappLoader的核心工作其实交给其内部的WebappClassLoader,它才是真正完成类加载工作的加载器,它是一个自定义的类加载器。
那么我们进入WebappLoader看看:/** * 由此加载器组件管理的类加载器。 */ private WebappClassLoaderBase classLoader = null;
/** * 创建相关的类加载器。 */ private WebappClassLoaderBase createClassLoader() throws Exception { Class<?> clazz = Class.forName(loaderClass); WebappClassLoaderBase classLoader = null; if (parentClassLoader == null) { parentClassLoader = context.getParentClassLoader(); } Class<?>[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor<?> constr = clazz.getConstructor(argTypes); classLoader = (WebappClassLoaderBase) constr.newInstance(args); return classLoader; }
public class WebappClassLoader extends WebappClassLoaderBase { public abstract class WebappClassLoaderBase extends URLClassLoader {
WebappClassLoader间接继承了URLClassLoader,只需要把/WEB-INF/lib和/WEB-INF/classes目录下的类和Jar包以URL形式添加到URLClassLoader中即可,后面就可以用该类加载器对类进行加载。
WebappClassLoader并没有遵循双亲委派机制,而是按自己的策略顺序加载类。根据委托标识,加载分为两种方式。
org.apache.catalina.loader.WebappClassLoaderBase#getResources(java.lang.String)
org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)boolean delegateFirst = delegate || filter(name, false);
- 当委托标识delegate为false时,WebappClassLoader类加载器首先先尝试从本地缓存中查找加载该类,然后用System类加载器尝试加载类,接着由自己尝试加载类,最后才由父类加载(Common)器尝试加载。所以此时它搜索的目录顺序是:
<JAVA_HOME>/jre/lib→<JAVA_HOME>/jre/lib/ext→CLASSPATH→/WEB-INF/classes→/WEB-INF/lib→$CATALINA_BASE/lib和$CATALINA_HOME/lib。 - 当委托标识delegate为true时,WebappClassLoader类加载器首先先尝试从本地缓存中查找加载该类,然后用System类加载器尝试加载类,接着由父类加载器(Common)尝试加载类,最后才由自己尝试加载。所以此时它的搜索的目录顺序是<JAVA_HOME>/jre/lib→<JAVA_HOME>/jre/lib/ext→CLASSPATH→$CATALINA_BASE/lib和$CATALINA_HOME/lib→/WEB-INF/classes→/WEB-INF/lib。
对于公共资源可以共享,而属于Web应用的资源则通过类加载器进行了隔离。对于重加载的实现,也比较清晰,只需要重新实例化一个WebappClassLoader对象并把原来WebappLoader中旧的置换掉即可完成重加载功能,置换掉的将被GC回收。6.4 内存泄漏
因为Tomcat提供了不必重启容器而只须重启Web应用以达到热部署的功能,其实现是通过定义一个WebappClassLoader类加载器,当热部署时,就将原来的类加载器废弃并重新实例化一个WebappClassLoader类加载器。但这种方式可能存在内存泄漏问题,因为类加载器是一个结构复杂的对象,导致它不能被GC回收的可能性比较多。除了对类加载器对象有引用可能导致其无法回收之外,对其加载的元数据(方法、类、字段等)有引用也可能会导致它无法被GC回收。
Tomcat的类加载器之间有父子关系。这里只看启动类加载器Bootstrap ClassLoader和Web应用类加载器WebappClassLoader。在JVM中,BootstrapClassLoader负责加载rt.jar包的java.sql.DriverManager,而WebappClassLoader负责加载Web应用中的Mysql驱动包。其中有一个很重要的步骤是Mysql的驱动类需要注册到DriverManager中,即DriverManager.registerDriver(new Driver()),它由Mysql驱动包自动完成。这样一来,当Web应用进行热部署操作时,如果没有将Mysql的Driver从DriverManager中反注册掉,则会导致整个WebappClassLoader无法回收,造成内存泄漏。
Tomcat如何对此内存泄漏进行监控,要判断WebappClassLoader会不会导致内存泄漏,只须判断WebappClassLoader有没有被GC回收即可。在Java中有一种引用叫弱引用,它能很好地判断WebappClassLoader有没有被GC回收,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。即如果某WebappClassLoader对象只被某弱引用关联,则它会在下次垃圾回收时被回收,但如果WebappClassLoader对象除了被弱引用关联外还被其他对象强引用,那么WebappClassLoader对象是不会被回收的,根据这些条件就可以判断是否有WebappClassLoader发生内存泄漏了。Tomcat的实现是通过WeakHashMap来实现弱引用的,只须将WebappClassLoader对象放到WeakHashMap中,例如weakMap.put(“Loader1”, WebappClassLoader)。当WebappClassLoader及其包含的元素没有被其他任何类加载器中的元素引用到时,JVM发生垃圾回收时则会把WebappClassLoader对象回收。
使用一个WeakHashMap跟踪WebappClassLoader,在查找内存泄漏之前会先强制调用System.gc()进行一次垃圾回收,保证没问题的WebappClassLoader都被回收。这时如果还有WebappClassLoader的状态是非Started(正常启动的都为Started,关闭的则为非Started)的,则它是未被垃圾回收的WebappClassLoader,发生了内存泄漏。
在Tomcat中每个Host容器都会对应若干个应用。为了跟踪这些应用是否有内存泄漏,需要将对应Context容器注册到Host容器中的WeakHashMap中,而这里讨论的监听器MemoryLeakTrackingListener就负责Context对应的WebappClassLoader的注册工作。
7. Wrapper接口
/** * A <b>Wrapper</b> 容器是否表示一个单独的servlet * 来自web应用程序部署描述符的定义。它 *提供了一种方便的机制来使用拦截器来查看每一个 *请求此定义所表示的servlet。 * <p> *包装器的实现负责管理servlet的生命周期 *循环,包括调用init()和 *在适当的时候破坏(),以及尊重的存在 servlet类本身的SingleThreadModel声明。 * <p> *附加到包装器的父容器通常是 实现上下文,表示servlet上下文(和 (因此是web应用程序),servlet在其中执行。 * <p> *子容器不允许在包装器实现,所以 * addChild()方法应该抛出一个 * <代码> IllegalArgumentException > < /代码。 */ public interface Wrapper extends Container {
Servlet的概念对于我们来说非常熟悉,我们会在它的doGet和doPost等方法上编写逻辑处理代码,而Wrapper则负责调用这些方法的逻辑。一般来说,一个Wrapper对应一个Servlet对象,也就是说,所有处理线程都共用同一个Servlet对象。
7.1 Servlet机制
规范提供了一个Servlet接口,接口中包含的重要方法是init、service、destroy等方法。Servlet在初始化时要调用init方法,在销毁时要调用destroy方法,而对客户端请求处理时则调用service方法。对于这些机制,都必须由Tomcat在内部提供支持,具体则由Wrapper容器提供支持。
当客户端请求到达服务端后,请求被抽象成Request对象后向4个容器进行传递,首先经过Engine容器的管道通过若干阀门,最后通过StandardEngineValve阀门流转到Host容器的管道,处理后继续往下流转,通过StandardHostValve阀门流转到Context容器的管道,继续往下流转,通过StandardContextValve阀门流转到Wrapper容器的管道,而对Servlet的核心处理也正是在StandardWrapperValve阀门中。
StandardWrapperValve阀门先由Application FilterChain组件执行过滤器,然后调用Servlet的service方法对请求进行处理,然后对客户端响应。Servlet工作机制的大致流程是:Request→StandardEngineValve→StandardHostValve→StandardContextValve→StandardWrapperValve→实例化并初始化Servlet对象→由过滤器链执行过滤操作→调用该Servlet对象的service方法→Response。
7.2 Servlet对象池
Servlet在不实现SingleThreadModel的情况下以单个实例模式运行,这种情况下,Wrapper容器只会通过反射实例化一个Servlet对象,对应此Servlet的所有客户端请求都会共用此Servlet对象。
那么:在某个Servlet中使用成员变量累加去统计访问次数,这就存在线程安全问题。为了支持一个Servlet对象对应一个线程,Servlet规范提出了一个SingleThreadModel接口, Tomcat容器必须要完成的机制是:如果某个Servlet类实现了SingleThreadModel接口,则要保证一个线程独占一个Servlet对象。假如线程1正在使用Servlet1对象,则线程2不能再使用Servlet1对象,只能用Servlet2对象。
针对SingleThreadModel模式,Tomcat的Wrapper容器使用了对象池策略,Wrapper容器会有一个Servlet堆,负责保存若干个Servlet对象,当需要Servlet对象时从堆中pop出一个对象,而当用完后则push回堆中。当然获取Servlet的个数有一个上限支持,超过这个上限就只能阻塞等待。
7.3 过滤器链
请求通过管道流转到Wrapper容器的管道,经过若干阀门后到达基础阀门StandardWrapperValve,它将创建一个过滤器链Application FilterChain对象。
- 从Context容器中获取所有过滤器的相关信息。
- 通过URL匹配过滤器,匹配的加入到过滤器链中。
- 通过Servlet名称匹配过滤器,匹配的加入到过滤器链中。
7.4 Servlet种类
根据请求资源的不同种类,可以把Servlet分成三种类别,比如请求可能访问一个普通的Servlet,也可能访问一个JSP页面,也可能访问一个静态资源。
Servlet路径的匹配规则如下:
首先,尝试使用精确匹配法匹配精确类型Servlet的路径。然后,尝试使用前缀匹配通配符类型Servlet。接着,尝试使用扩展名匹配通配符类型Servlet。最后,匹配默认Servlet。着重讲一下org.apache.catalina.core.StandardWrapper#allocate:
可以通过该方法获取到相应的Servlet。/** *分配一个已初始化的Servlet实例 *其service()方法调用。如果servlet类这样做 *未实现SingleThreadModel,将(only)初始化 实例可能会立即返回。如果servlet类实现了 * SingleThreadModel,包装器实现必须保证 *这个实例不会再次分配,直到它被a释放 *调用deallocate()。 * * @exception 如果servlet init()方法被抛出,则使用ServletException * @exception 如果加载错误发生,ServletException */ @Override public Servlet allocate() throws ServletException { //如果我们当前正在卸载这个servlet,抛出一个异常 if (unloading) { throw new ServletException(sm.getString("standardWrapper.unloading", getName())); } boolean newInstance = false; //如果不是SingleThreadedModel,则每次返回相同的实例 if (!singleThreadModel) { // 如果需要,加载并初始化我们的实例 if (instance == null || !instanceInitialized) { //做一个线程安全锁 synchronized (this) { if (instance == null) { try { if (log.isDebugEnabled()) { log.debug("Allocating non-STM instance"); } // 注意:我们不知道Servlet是否实现了 // 单线程模型,直到我们加载它。 instance = loadServlet(); newInstance = true; if (!singleThreadModel) { // 对于非stm,在这里递增以防止竞争 // 条件与卸载。Bug 43683,测试用例 // #3 countAllocated.incrementAndGet(); } } catch (ServletException e) { throw e; } catch (Throwable e) { ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("standardWrapper.allocate"), e); } } if (!instanceInitialized) { initServlet(instance); } } } if (singleThreadModel) { if (newInstance) { //必须在上面的同步之外做这个来防止a //可能死锁 synchronized (instancePool) { instancePool.push(instance); nInstances++; } } } else { if (log.isTraceEnabled()) { log.trace(" Returning non-STM instance"); } // 对于新实例,计数将在创建时增加 if (!newInstance) { countAllocated.incrementAndGet(); } return instance; } } synchronized (instancePool) { while (countAllocated.get() >= nInstances) { // 如果可能,分配一个新实例,否则等待 if (nInstances < maxInstances) { try { instancePool.push(loadServlet()); nInstances++; } catch (ServletException e) { throw e; } catch (Throwable e) { ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("standardWrapper.allocate"), e); } } else { try { instancePool.wait(); } catch (InterruptedException e) { // Ignore } } } if (log.isTraceEnabled()) { log.trace(" Returning allocated STM instance"); } countAllocated.incrementAndGet(); return instancePool.pop(); } }
-