本系列文章来自《How Tomcat Works》,因本书中描述的tomcat版本是5以下的,所以后面章节在实操部分用了tomcat8的源码。
容器是一个处理用户 servlet 请求并返回对象给 web 用户的模块。org.apache.catalina.Container 接口定义了容器的形式,有四种容器: Engine(引擎) , Host(主机) , Context(上下文) , 和 Wrapper(包装器)。这一章将会介绍 context 和 wrapper,而 Engine 和 Host 会留到第十三章介绍。这一章首先介绍容器接口,然后介绍容器的工作流程。然后介绍的内容是 Wrapper和 Context 接口。然后用两个例子来总结 wrapper 和 context 容器。
容器接口
一个容器必须实现 org.apache.catalina.Container接口。就如在第四章中看到的,传递一个Container实例给 Connector对象的setContainer方法,然后Connector对象就可以使用container的invoke方法。
HttpConnector connector = new HttpConnector();
SimpleContainer container = new SimpleContainer();
connector.setContainer(container);
对于 Catalina 的容器首先需要注意的是它一共有四种不同的容器。
Engine | 表示整个Catalina的servlet引擎 |
---|---|
Host | 表示整个Catalina的servlet引擎 |
Context | 表示一个WEB应用,一个context包含一个或多个wrapper |
Wrapper | 表示一个独立的servlet |
每一个概念之上是用 org.apache.catalina 包来表示的。 Engine、 Host、 Context和 Wrapper 接口都实现了 Container 即可。
它们的标准实现是 StandardEngine,StandardHost, StandardContext, and StandardWrapper,它们都是
org.apache.catalina.core 包的一部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6ay1Ly2-1619231522666)(E:\study\tomcat\img\ContainerInterface.png)]
从图中可以看出,所有的类都扩展自抽象类ContainerBase类。一个 Catalina 功能部署不一定需要所有的四种类型的容器。 一个容器可以有一个或多个低层次上的子容器。例如,一个 Context 有一个或多
个 wrapper,而 wrapper 作为容器层次中的最底层,不能包含子容器。讲一个容器添加到另一容器中可以使用在 Container 接口中定义的 addChild()方法。
更有意思的是 Container 接口被设计成 Tomcat 管理员可以通过 server.xml 文件配置来决定其工作方式的模式。它通过一个 pipeline(流水线)和一系列的阀门来实现,这些内容将会在下一节 Pipelining Task 中讨论。
流水线任务
前面已经解释过连接器通过调用容器的invoke方法的相关动作。下面将分析4个相关的接口。Pipeline,Value,ValveContext和Contained。
一个pipeline包含了改容器要唤醒的所有任务。每一个阀门表示了一个特定的任务。一个容器的流水线有一个基本的阀门,但是你可以添加任意你想要添加的阀门。阀门的数目定义为添加的阀门的个数(不包括基本阀门)。有趣的是,阀门可以通过编辑Tomcat的配置文件server.xml来动态的添加。
如果你懂得servlet过滤器,那就不难想象流水线和阀门的工作方式。流水线像一个过滤链且每个阀门就像一个过滤器。像过滤器一样,一个阀门能控制通过它的请求与响应。一个阀门完成处理后,则调用流水线上的下一个阀门。基本阀门总是在最后才被调用。
一个容器能有一个流水线。当一个容器的invoke方法被调用,则容器通过处理它的流水线,而流水线调用它上的第一个阀门一直到最后一个阀门。你可以想象流水线的 invoke 方法的伪代码如下所示。
// invoke each valve added to the pipeline
for (int n=0; n<valves.length; n++) {
valve[n].invoke( ... );
}
// then, invoke the basic valve
basicValve.invoke( ... );
但tomcat的设计者们采用了一种不同的方式。即通过引入org.apache.catalina.ValveContext接口。下面是介绍它怎样工作。
当连接器调用容器的invoke方法时,容器不会通过硬编码它应该执行的操作。而是容器通过调用流水线的invoke方法。Pipeline接口的invoke方法有下面的签名,与Container接口的invoke方法完全相同。
public void invoke(Request request, Response response) throws IOException, ServletException;
下面是Container接口的invoke方法在org.apache.catalina.core.ContainerBase类中的实现。
public void invoke(Request request, Response response) throws IOException, ServletException {
pipeline.invoke(request, response);
}
代码中的pipeline是容器中的Pipeline接口中的一个实例。现在,流水线必须确保所有的阀门必须添加到流水线上,并且基本阀门必须必调用一次。pipeline通过创建一个ValveContext接口的实例。ValveContext实现了pipeline的内部类,所以ValveContext能访问pipeline的所有成员。ValveContext接口中最重要的方法是invokeNext。
public void invokeNext(Request request, Response response) throws IOException, ServletException
创建ValveContext实例之后,pipeline调用ValveContext的invokeNext方法。ValveContext首先会调用流水线上的第一个阀门,当第一个阀门执行完后接着就调用下一个阀门。ValveContext传递它本身给每个阀门,所以阀门能调用invokeNext方法。下面是Valve接口的invoke方法的签名。
public void invoke(Request request, Response response, ValveContext ValveContext) throws IOException, ServletException
阀门的invoke方法的实现与下面代码有点类似。
public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException {
// Pass the request and response on to the next valve in our pipeline
valveContext.invokeNext(request, response);
// now perform what this valve is supposed to do
...
}
org.apache.catalina.core.StandardPipeline类是所有容器的Pipeline接口的实现。Tomcat 5则依赖于StandardValveContext类。InvokeNext 方法使用下标( subscript)和级别( stage)记住哪个阀门被唤醒。
当第一次唤醒的时候,下标的值是 0,级的值是 1。以你次,第一个阀门被唤醒,
流水线的阀门获得 ValveContext 实例调用它的 invokeNext 方法。这时下标的值是 1 所以下一个阀门被唤醒,然后一步步的进行。
public final void invokeNext(Request request, Response response)
throws IOException, ServletException {
int subscript = stage;
stage = stage + 1;
// Invoke the requested Valve for the current request thread
if (subscript < valves.length) {
valves[subscript].invoke(request, response, this);
} else if ((subscript == valves.length) && (basic != null)) {
basic.invoke(request, response, this);
} else {
throw new ServletException
(sm.getString("standardPipeline.noValve"));
}
}
我们将会解释了Pipeline,Valve和ValveContext接口的更多细节。
Pipeline接口
我们提到的流水线的第一个方法是它的 Pipeline 接口的 invoke 方法,该方法会开始唤醒流水线的阀门。流水线接口允许你添加一个新的阀门或者删除一个阀门。最后,可以使用 setBasic 方法来分配一个基本阀门给流水线, getBasic 方法会得到基本阀门。最后被唤醒的基本阀门,负责处理 request 和回复 response。
public interface Pipeline {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public void removeValve(Valve valve);
}
Vavle接口
Vavle接口表示一个阀门,该组件负责处理请求。该接口有两个方法,invoke和getInfo。invoke在上面已经介绍了。getInfo方法返回阀门实现的信息。
public interface Valve {
public String getInfo();
public void invoke(Request request, Response response,
ValveContext context)
throws IOException, ServletException;
}
ValveContext接口
阀门上下文接口有两个方法。invokeNext 方法如上所述, getInfo 方法会返回阀门上下文的信息。
public interface ValveContext {
public String getInfo();
public void invokeNext(Request request, Response response)
throws IOException, ServletException;
}
Contained接口
一个阀门可以选择性的实现 org.apache.catalina.Contained 接口。该接口定义了其实现类跟一个容器相关联。
public interface Contained {
public Container getContainer();
public void setContainer(Container container);
}
Wrapper接口
包装器接口中重要方法有 allocate 和 load 方法。 allocate 方法负责定位该包装器表示的 servlet 的实例。 Allocate 方法必须考虑一个 servlet 是否实现了
javax.servlet.SingleThreadModel 接口,该部分内容将会在 11 章中进行讨论。Load 方法负责 load 和初始化 servlet 的实例。
应用上下文
连接器每次收到一个 HTTP 请求可以使用上
下文的 invoke 方法。根据前面介绍的内容,其余的工作不难理解。
\1. 一个容器有一个流水线,容器的 invoke 方法会调用流水线的
invoke 方法。
\2. 流水线的 invoke 方法会调用添加到容器中的阀门的 invoke 方法,
然后调用基本阀门的 invoke 方法。
\3. 在一个包装器中,基本阀门负责加载相关的 servlet 类并对请求作
出相应。
\4. 在一个有子容器的上下文中,基本法门使用 mapper 来查找负责处
理请求的子容器。如果一个子容器被找到,子容器的 invoke 方法会被调
用,然后返回步骤 1。
接下来看处理的流程是如何实现的。
如流水下任务一节中介绍的,该段代码唤醒所有的阀门然后调用基本阀门的 。
生命周期
Catalina包含很多组件。当Catalina启动时,这些组件也需要开启。当Catalina停止时,这些组件也必须在合适的机会进行清理。例如,当一个容器停止工作的时候,它必须唤醒所有加载的 servlet 的 destroy 方法,而 session 管理器要保存 session 到二级存储器中。保持组件启动和停止一致的的机制通过实现
org.apache.catalina.Lifecycle 接口来实现。
一个实现了 Lifecycle 接口的组件会触发一个或多个下列事件:
BEFORE_START_EVENT, START_EVENT, AFTER_START_EVENT, BEFORE_STOP_EVENT,
STOP_EVENT, and AFTER_STOP_EVENT。 当组件被启动的时候前三个事件会被触发,而组件停止的时候会触发后边三个事件。 另外,如果一个组件可以触发事件,那么必须存在相应的监听器来对触发的事件作出回应。监听器使用org.apache.catalina.LifecycleListener 来表示。
本章会对 Lifecycle, LifecycleEvent, and LifecycleListener 进行讨论。另外,还会解释一个公用类 LifecycleSupport,它给组件提供了一个简单方式来触发生命周期事件和处理事件监听器。在本章中,会建立一个有实现了Lifecycle 接口的类的工程。
Lifecycle接口
Catalina 的设计允许一个组件包含其它的组件。例如一个容器可以包含一系列
的组件如加载器、管理器等。一个父组件负责启动和停止其子组件。 Catalina
的设计成所有的组件被一个父组件来管理( in custody),所以启动 bootstrap类只需启动一个组件即可。这种单一的启动停止机制通过继承 Lifecycle 来实现。
Lifecycle 中最重要的方法是 start 和 stop 方法。一个组件提供了这些方法的
实现,所以它的父组件可以通过这些方法来启动和停止他们。另外 3 个方法
addLifecycleListener, findLifecycleListeners, 和
removeLifecycleListener 事跟监听器相关的类。组件的监听器对组件可能触发的时间“ 感兴趣” ,当一个事件被触发的时候,相应监听器会被通知。一个Lifecycle 实例可以触发使用静态最终字符串定义的六个事件。
LifecycleEvent类
org.apache.catalina.LifecycleEvent 表示一个生命周期事件。
LifecycleListener接口
org.apache.catalina.LifecycleListener 接口可以表示生命周期监听器。在该接口中,只有一个方法 lifecycleEvent,该方法在事件触发的时候唤醒对其“ 感兴趣” 的监听器。
public interface LifecycleListener {
public void lifecycleEvent(LifecycleEvent event);
}
LifecycleSupport类
一个实现了 Lifecycle 接口的组件并且允许监听器注册其“ 感兴趣” 的事件必
须 Lifecycle 接口提供跟事件相关的方法的代码( addLifecycleListener,
findLifecycleListeners, 和 removeLifecycleListener)。这样就可以将组件
的监听器添加到 ArrayList 或者其他相似的对象中。 Catalina 提供了一个公用
类 org.apache.catalina.util.LifecycleSupport 来简化组件处理监听器和触
发生命周期事件。
public synchronized void start() throws LifecycleException {
if (started)
throw new LifecycleException("SimpleContext has already started");
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
started = true;
try {
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
// Start our child containers, if any
Container children[] = findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).start();
}
// Start the Valves in our pipeline (including the basic),
// if any
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(START_EVENT, null);
}
catch (Exception e) {
e.printStackTrace();
}
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}
其结果就是 SimpleContext 实例中注册的监听器都会被唤醒,一个
SimpleContextLifecycleListener 类型的监听器会注册它“ 感兴趣” 的事件
接下来, start 方法设置 started 布尔变量为真来标记该组件已经启动。
started = true;
start 方法接下来会启动所有组件以及其子容器。现在有两个组件实现了
Lifecycle 接口: SimpleLoader 和 SimplePipeline。 SimpleContext 有两个包
装器作为其子容器。这些包装器也实现了 Lifecycle 接口。
日志系统
日志系统是一个记录信息的组件。在 Catalina 中,日志系统是一个相对简单的
跟容器相关联的组件。 Tomcat 在 org.apache.catalina.logger 包中提供了多个
不同的日志系统。
本章节包含3个部分,第一部分主要讲解org.apache.catalina.Logger接口,所有logger都必须实现该接口。第二部分解释了tomcat中的logger,第三部分这章节中使用tomcat的logger的细节。
Logger接口
一个日志系统必须实现 org.apache.catalina.Logger接口。Logger接口提供了几个log方法给实现类来选择调用。最简单的方法就是接受一个String类型的消息来记录日志。最后面两个log方法接受一个冗余级别。如果传递的数字低于该类的实例设置的冗余级别,就将信息记录下来,否则就忽略信息。使用5个公共静态变量来定义冗余级别。FATAL, ERROR, WARNING, INFORMATION,和DEBUG。使用getVerbosity和setVerbosity方法来获取和设置该值。
除此之外,Logger接口使用getContainer和setContainer方法将Logger实例和容器联系在一起。它同样提供addPropertyChangeListener和removePropertyChangeListener方法来添加或者移除一个PropertyChangeListener。
public interface Logger {
// ----------------------------------------------------- Manifest Constants
public static final int FATAL = Integer.MIN_VALUE;
public static final int ERROR = 1;
public static final int WARNING = 2;
public static final int INFORMATION = 3;
public static final int DEBUG = 4;
// ------------------------------------------------------------- Properties
/**
* Return the Container with which this Logger has been associated.
*/
public Container getContainer();
/**
* Set the Container with which this Logger has been associated.
*
* @param container The associated Container
*/
public void setContainer(Container container);
/**
* Return descriptive information about this Logger implementation and
* the corresponding version number, in the format
* <code><description>/<version></code>.
*/
public String getInfo();
/**
* Return the verbosity level of this logger. Messages logged with a
* higher verbosity than this level will be silently ignored.
*/
public int getVerbosity();
/**
* Set the verbosity level of this logger. Messages logged with a
* higher verbosity than this level will be silently ignored.
*
* @param verbosity The new verbosity level
*/
public void setVerbosity(int verbosity);
// --------------------------------------------------------- Public Methods
/**
* Add a property change listener to this component.
*
* @param listener The listener to add
*/
public void addPropertyChangeListener(PropertyChangeListener listener);
/**
* Writes the specified message to a servlet log file, usually an event
* log. The name and type of the servlet log is specific to the
* servlet container. This message will be logged unconditionally.
*
* @param message A <code>String</code> specifying the message to be
* written to the log file
*/
public void log(String message);
/**
* Writes the specified exception, and message, to a servlet log file.
* The implementation of this method should call
* <code>log(msg, exception)</code> instead. This method is deprecated
* in the ServletContext interface, but not deprecated here to avoid
* many useless compiler warnings. This message will be logged
* unconditionally.
*
* @param exception An <code>Exception</code> to be reported
* @param msg The associated message string
*/
public void log(Exception exception, String msg);
/**
* Writes an explanatory message and a stack trace for a given
* <code>Throwable</code> exception to the servlet log file. The name
* and type of the servlet log file is specific to the servlet container,
* usually an event log. This message will be logged unconditionally.
*
* @param message A <code>String</code> that describes the error or
* exception
* @param throwable The <code>Throwable</code> error or exception
*/
public void log(String message, Throwable throwable);
/**
* Writes the specified message to the servlet log file, usually an event
* log, if the logger is set to a verbosity level equal to or higher than
* the specified value for this message.
*
* @param message A <code>String</code> specifying the message to be
* written to the log file
* @param verbosity Verbosity level of this message
*/
public void log(String message, int verbosity);
/**
* Writes the specified message and exception to the servlet log file,
* usually an event log, if the logger is set to a verbosity level equal
* to or higher than the specified value for this message.
*
* @param message A <code>String</code> that describes the error or
* exception
* @param throwable The <code>Throwable</code> error or exception
* @param verbosity Verbosity level of this message
*/
public void log(String message, Throwable throwable, int verbosity);
/**
* Remove a property change listener from this component.
*
* @param listener The listener to remove
*/
public void removePropertyChangeListener(PropertyChangeListener listener);
}
Tomcat日志系统
tomcat提供了三种日志系统,它们分别是FileLogger,SystemErrLogger和SystemOutLogger。这些类可以在org.apache.catalina.logger包中找到,它们都继承了org.apache.catalina.logger.LoggerBase类。
LoggerBase类
现在来看该类的冗余级别。它被定义为一个 protected 的名为 verbosity 的变量,默认值为 ERROR。
protected int verbosity = ERROR;
冗余级别可以使用setVerbosity方法改变,传递如下值中的一个,包括FATAL, ERROR, WARNING, INFORMATION, 和 DEBUG。
public void setVerbosityLevel(String verbosity) {
if ("FATAL".equalsIgnoreCase(verbosity))
this.verbosity = FATAL;
else if ("ERROR".equalsIgnoreCase(verbosity))
this.verbosity = ERROR;
else if ("WARNING".equalsIgnoreCase(verbosity))
this.verbosity = WARNING;
else if ("INFORMATION".equalsIgnoreCase(verbosity))
this.verbosity = INFORMATION;
else if ("DEBUG".equalsIgnoreCase(verbosity))
this.verbosity = DEBUG;
}
两个log方法接受一个整型值当做一个冗余级别。在这些覆盖的方法中,log(String message)仅仅在实例的冗余级别低于冗余级别才被调用。
public void log(String message, int verbosity) {
if (this.verbosity >= verbosity)
log(message);
}
LoggerBase的三个子类将在下面的部分讨论,你将看到log方法被覆盖。
SystemOutLogger类
SystemOutLogger 作为 LoggerBase 的子类提供了 log(String message)方法的实现。每一个收到的信息都被传递给 System.out.println 方法。
public void log(String msg) {
System.out.println(msg);
}
SystemErrLogger类
SystemErrLogger 类跟 SystemOutLogger 类十分相似,只是它覆盖 log(String message)方法的时候使用的是System.erro.println()方法。
public void log(String msg) {
System.err.println(msg);
}
FileLogger类
FileLogger 是 LoggerBase 类中最复杂的。它将从关联容器收到的信息写到文件
中,每个信息可以选择性的加上时间戳。在第一次实例化的时候,该类的实例会
创建一个文件,该文件的名字带有日期信息。如果日期改变了,它会创建一个新
的文件并把信息写在里面。类的实例允许在日志文件的名字上添加前缀和后缀。
在 Tomcat4 中, FileLogger 类实现了 Lifecycle 接口,所以它可以跟其它实现
org.apache.catalina.Lifecycle 接口的组件一样启动和停止。在 Tomcat5 中,
它是实现了 Lifecycle 接口的 LoggerBase 类的子类。
public void log(String msg) {
// Construct the timestamp we will use, if requested
Timestamp ts = new Timestamp(System.currentTimeMillis());
String tsString = ts.toString().substring(0, 19);
String tsDate = tsString.substring(0, 10);
// If the date has changed, switch log files
if (!date.equals(tsDate)) {
synchronized (this) {
if (!date.equals(tsDate)) {
close();
date = tsDate;
open();
}
}
}
// Log this message, timestamped if necessary
if (writer != null) {
if (timestamp) {
writer.println(tsString + " " + msg);
} else {
writer.println(msg);
}
}
}
log 方法接受一个消息并把消息写到日志文件中。在 FileLogger 实例的生命周
期中, log 方法可以打开或关闭多个日志文件。如果日期改变了的话, log 方法
关闭当前文件并打开一个新文件。接下来看看 open、 close 和 log 这些方法是如
何工作的。
log方法内先创建一个Timestamp类的实例ts,Timestamp是对java.util.Date的轻量级包装。ts的目的是容易获取当前时间。使用Timestamp类的toString方法,你能获取当前时间的string类型的表达式。toString方法的输出如下格式。
yyyy-mm-dd hh:mm: SS.fffffffff
其中fffffffff表示从00:00:00的毫秒数。为了只得到日期和小时,log方法调用了String的substring方法。log方法然后对比了tsDate和String类型的date的值,如果 tsDate 和 date 的值不同,它关闭当前日志文件,将 tsDate 的值赋给 date 并打开一个新日志文件。 最后,日志方法将 PrintWriter 实例的输出流写入到日志文件中。如果布尔变量
timestamp 的值为真,将 timestamp(tsString)的值作为前缀,否则不使用前缀。
open方法
open方法首先检查该目录是否支持创建,如果目录不存在,则创建一个目录。目录路径用类变量directory存储。然后组合文件路径,路径包括directory目录+前缀+当前日期+后缀。然后构造PrintWriter对象来写文件流。
private void open() {
// Create the directory if necessary
File dir = new File(directory);
if (!dir.isAbsolute())
dir = new File(System.getProperty("catalina.base"), directory);
dir.mkdirs();
// Open the current log file
try {
String pathname = dir.getAbsolutePath() + File.separator +
prefix + date + suffix;
writer = new PrintWriter(new FileWriter(pathname, true), true);
} catch (IOException e) {
writer = null;
}
}
close方法
close方法将PrintWriter的write内容刷到磁盘,然后关闭PrintWriter并将writer设置为null,并将date设置为空字符串。
private void close() {
if (writer == null)
return;
writer.flush();
writer.close();
writer = null;
date = "";
}
加载器
本章节解释了Catalina中标准的web应用加载器。一个servlet容器需要一个定制的加载器而不能简单地使用系统的类加载器因为不能相信servlets能运行的好。如果不能使用系统类加载器加载servlet所需要的所有servlets和其他类,像之前的章节一样,那么servlet能访问任何其他类和lib目录,包括JVM运行时的CLASSPATH环境变量,这显然违法了安全规定。一个servlet仅仅允许它加载WEB-INF/classes目录下的类及子目录下的类以及从部署在WEB-INF/lib目录下的类库。这就是为什么一个servlet容器需要它自己的加载器。每个servlet容器中的WEB应用仍有它自己的加载器。一个加载器使用类加载器使用一些规则来加载类。在Catalina中,一个类加载器使用org.apache.catalina.Loader接口表示。
Tomcat 需要一个自己的加载器的另一个原因是它需要支持在 WEB-INF/classes
或者是 WEB-INF/lib 目录被改变的时候会重新加载。 Tomcat 的加载器实现中使
用一个单独的线程来检查 servlet 和支持类文件的时间戳。要支持类的自动加载
功能,一个加载器类必须实现 org.apache.catalina.loader.Reloader 接口。
本节广泛使用的两个术语:库( repository)和源( resources)。库表示加载
器查找的地方,源表示加载器中的 DirContext 对象,它的文档基( document base)
指向了上下文的文档基。
Java类加载器
Java 加载器在 Java 核心类库和 CLASSPATH 环
境下面的所有类中查找类。如果需要的类找不到,会抛出
java.lang.ClassNotFoundException 异常
从J2SE1.2开始,使用了三种类加载器,bootstrap类加载器,extension
类加载器和 systen 类加载器。这三个加载器是父子关系,其中 bootstrap 类加
载器在顶端,而 system 加载器在结构的最底层。
bootstrap类加载器用于启动JVM。一旦调用java.exe程序,bootstrap类加载器就开始工作。所以,它必须使用本地代码实现,因为它需要为JVM加载必要的类到函数。另外,它也同样负责加载所有的核心的Java类,比如在java.lang和java.io包的类。bootstrap类加载器搜索如rt.jar、i18n.jar的核心类库。这些类的搜索依赖于JVM的版本和操作系统。
扩展类加载器负责加载标准扩展目录下面的类。这样就可以使得编写程序变得简单,只需把 JAR 文件拷贝到扩展目录下面即可,类加载器会自动的在下面查找。不同的供应商提供的扩展类库是不同的,Sun的JVM标准扩展目录是/jdk/jre/lib/ext。
system加载器是默认的加载器,它在环境变量 CLASSPATH 目录下面查找相应的类。
所以,JVM使用哪个类加载器是依赖于委派模型的,这是出于安全的考虑。
委派模型对于安全性是非常重要的。如你所知,可以使用安全管理器来限制访问
某个目录。现在,恶意的意图有人能写出一类叫做 java.lang.Object,可用于
访问任何在硬盘上的目录。 因为 JVM 的信任 java.lang.Object 类,它不会关注
这方面的活动。因此,如果自定义 java.lang.Object 被允许加载的安全管理器
将很容易瘫痪。幸运的是,这将不会发生,因为委派模型会阻止这种情况的发生。
下面是它的工作原理。
当自定义 java.lang.Object 类在程序中被调用的时候, system 类加载器将该请
求委派给 extension 类加载器,然后委派给 bootstrap 类加载器。这样 bootstrap
类加载器先搜索的核心库,找到标准 java.lang.Object 并实例化它。这样,自
定义 java.lang.Object 类永远不会被加载
关于在 Java 类加载机制的优势在于可以通过扩展
java.lang.ClassLoader 抽象类来扩展自己的类加载器。 Tomcat 的需求
自定义自己的类加载器原因包括以下内容
- 要制定类加载器的某些特定规则。
- 缓存以前加载的类。
- 事先加载类以预备使用。
Loader接口
在 Web 应用程序中加载 servlet 和其他类需要遵循一些规则。例如,一个应用下的servlet能使用部署在WEB-INF/classes目录及下所有子目录的类。然而,servlets不能访问其他类,即使这些类在运行TOMCAT的JVM的CLASSPATH目录下。此外,一个servlet仅能访问部署在WEB-INF/lib目录下的类而不是其他目录。
一个Tomcat加载器代表一个web应用加载器而不是一个类加载器。一个加载器必须实现org.apache.catalina.Loader接口。加载器的实现使用定制的类加载器org.apache.catalina.loader.WebappClassLoader。可以使用Loader接口的getClassLoader方法获取一个网络加载器。
其中,Loader接口定义了一系列方法跟库协作。一个网络应用程序的目录WEB-INF/classes和WEB-INF/lib被当做一个库。Loader接口的addRepository方法用于添加库而findRepositories方法返回所有库资源。
一个 Tomcat的加载器通常跟一个上下文相关联,Loader接口的getContainer和setContainer用于建立此关联。如果上下文中的类被修改过,一个加载器也能支持重新加载类。一个servlet程序能被重新编译servlet或辅助类,新类将被重新加载而不需要重启tomcat。为了达到重新加载的目的,Loader接口有修改方法。在加载器的实现中,如果在其
库中一个或多个类别已被修改, modeify 方法必须返回 true,因此需要重新加载。
一个加载器不是自己进行重新加载,而是调用上下文接口的重载方法。另外两种方法,
setReloadable 和 getReloadable,用于确定加载器中是否可以使用重加载。 默认情况下,Context的标准实现org.apache.catalina.core.StandardContext没有启动重加载。如果要启动上下文的重加载,需要在server.xml文件中该上下文中增加Context元素。如下,
<Context path="/myApp" docBase="myApp" debug="0" reloadable="true"/>
另外,一个加载器的实现可以确定是否委派给父加载器类。为了实现这一点,Loader 接口提供了 getDelegate 和 setDelegate 方法。
无论一个跟容器相关的加载器何时需要一个 servlet 类,当它的 invoke 方 法被调用的时候,容器首先调用加载器的 getClassLoader 方法获得一个加 载器。然后容器调用 loadClass 方法来加载 servlet 类,更多的细节会在第 11 章中介绍。
Reloader接口
要支持自动重新加载,一个加载器的实现必须实现org.apache.catalina.loader.Reloader接口。
public interface Reloader {
public void addRepository(String repository);
public String[] findRepositories();
public boolean modified();
}
Reloader 接口里最重要的方法是 modified 方法,如果在 web 应用程序中的
servlet 任何支持类被修改的时候该方法返回 true。 addRepository 方法用于添
加一个库而 findRepositories 方法用于返回实现了 Reloader 接口的加载器的所
有的库。
WebappLoader类
org.apache.catalina.loader.WebappLoader 类是 Loader 接口的实现,它表示
一个 web 应用程序的加载器,负责给 web 应用程序加载类。 WebappLoader 创建
一个 org.apache.catalina.loader.WebappClassLoader 类的实例作为它的类加
载器。像其他的 Catalina 组件一样, WebappLoader 实现了
org.apache.catalina.Lifecycle 接口,可有由关联容器启动和停止。
WebappLoader 类还实现了 java.lang.Runnable 接口,所以可以通过一个线程来
重复的调用 modified 方法,如果 modified 方法返回 true, WebappLoader 实例
同志它的关联容器。类通过上下文重新加载自己,而不是 WebappLoader。
当WebappLoader类的start方法被调用时,将完成以下几项重要任务。
- 创建一个类加载器
- 设置库
- 设置类路径
- 设置访问权限
- 开启一个新的线程进行自动重载。
创建一个类加载器
WebappLoader实例使用一个内部类加载器来加载类。如果你愿意,可以创建继承
WebappClassLoader 类的自己的加载器,然后使用 setLoaderClass 方法来强制
WebappLoader 使用你创建的加载器。否则,当它 WebappLoader 启动的时候,它
会使用它的私有方法 createClassLoader 创建 WebappClassLoader 的实例
设置库
WebappLoader 的 start 方法会调用 setRepositories 方法来给类加载器添加一
个库。 WEB-INF/classes 目录传递给加载器 addRepository 方法,而 WEB-INF/lib
传递给加载器的 setJarPath 方法。这样,类加载器能能从 WEB-INF/classes 目
录下面和 WEB-INF/lib 目录下面部署的类库里加载类。
设置类路径
该任务由 start 方法调用 setClassPath 方法完成, setClassPath 方法会给
servlet 上下文分配一个 String 类型属性保存 Jasper JSP 编译的类路径,该内
容先不予讨论。
设置访问权限
如果运行tomcat时使用了安全管理器,setPermissions 给类加载器给必要的目录添加
访问权限,例如 WEB-INF/classes 和 WEB-INF/lib。如果不使用管理器,该方法
马上返回。
开启一个新的自动重载线程
WebappLoader支持自动重载。如果 WEB-INF/classes 或者 WEB-INF/lib 目录被
重新编译过,在不重启 Tomcat 的情况下必须自动重新载入这些类。为了实现这
个目的, WebappLoader 有一个单独的线程每个 x 秒会检查源的时间戳。 x 的值由
checkInterval 变量定义,它的默认值是 15,也就是每隔 15 秒会进行一次检查
是否需要自动重载。该类还提供了两个方法 getCheckInterval 和
setCheckInterval 方法来访问或者设置 checkInterval 的值。
在 Tomcat5 中检查类是否被修改的任务由org.apache.catalina.core.StandardContext 类的 backgroundProcess方法完成。该方法会被org.apache.catalina.core.ContainerBase类中一个专门的线程周期性的调用,ContainerBase是StandardContext类的父类。 注意 ContainerBase类的ContainerBackgroundProcessor内部类实现的Runnable接口。
WebappClassLoader类
类 org.apache.catalina.loader.WebappClassLoader 表示在一个 web 应用程序中使用的加载器。处于安全性的考虑, WebappClassLoader类不允许一些特定的类被加载。这些类被存储在一个 String 类型的数组中,现在仅仅有一个成员。
private static final String[] triggers = {
"javax.servlet.Servlet" // Servlet API
};
另外在委派给系统加载器的时候,你也不允许加载属于该包的其它类或者它的子包。
private static final String[] packageTriggers = {
"javax", // Java extensions
"org.xml.sax", // SAX 1 & 2
"org.w3c.dom", // DOM 1 & 2
"org.apache.xerces", // Xerces 1 & 2
"org.apache.xalan" // Xalan
};
接下来让我们看下该类是如何实现缓存和类加载的。
缓存
为了提高性能,当一个类被加载的时候会被放到缓存中,这样下次需要加载该类
的时候直接从缓存中调用即可。缓存由 WebappClassLoader 类实例自己管理。另
外, java.lang.ClassLoader 维护了一个 Vector,可以避免前面加载过的类被当
做垃圾回收掉。在这里,缓存被该超类管理。
每一个可以被WebappClassLoader 加载的类(放在 WEB-INF/classes 目录下的类文件或者 JAR 文件)
都被当做一个源。一个源被org.apache.catalina.loader.ResourceEntry类表示。一个ResourceEntry实例保存一个 byte类型的数组表示该类、最后修改的时间或者清单(如果资源是从JAR文件加载)等等。
public class ResourceEntry {
public long lastModified = -1;
public byte[] binaryContent = null;
public Class loadedClass = null;
public URL source = null;
public URL codeBase = null;
public Manifest manifest = null;
public Certificate[] certificates = null;
}
所有缓存的资源存储在一个叫resourceEntries的HashMap中。key为资源的名字,所有没有被找到的资源存储到notFoundResources的HashMap中。
加载类
当加载一个类的时候,WebappClassLoader类使用下面的规则。
-
所有加载过的类都已进行缓存,所以首先需要检查本地缓存。
-
如果无法在本地缓存找到类,使用 java.langClassLoader类的 findLoaderClass 方法在缓存查找类。
-
如果在两个缓存中都无法找到该类,使用系统类加载器避免从J2EE类中覆盖来的web应用程序。
-
如果使用了安全管理器,检查该类是否允许加载,如果该类不允许加载,则抛出 ClassNotFoundException 异常。
-
如果要加载的类使用了委派标志或者该类属于 trigger 包中,使用父加载器来加载类,如果父加载器为 null,使用系统加载器加载。
-
从当前的源中加载类
-
如果在当前的源中找不到该类并且没有使用委派标志,使用父类加载器。如果父类加载器为 null,使用系统加载器。
-
如果该类仍然找不到,抛出 ClassNotFoundException 异常。
public byte[] binaryContent = null;
public Class loadedClass = null;
public URL source = null;
public URL codeBase = null;
public Manifest manifest = null;
public Certificate[] certificates = null;
}
所有缓存的资源存储在一个叫resourceEntries的HashMap中。key为资源的名字,所有没有被找到的资源存储到notFoundResources的HashMap中。
#### 加载类
当加载一个类的时候,WebappClassLoader类使用下面的规则。
1. 所有加载过的类都已进行缓存,所以首先需要检查本地缓存。
2. 如果无法在本地缓存找到类,使用 java.langClassLoader类的 findLoaderClass 方法在缓存查找类。
3. 如果在两个缓存中都无法找到该类,使用系统类加载器避免从J2EE类中覆盖来的web应用程序。
4. 如果使用了安全管理器,检查该类是否允许加载,如果该类不允许加载,则抛出 ClassNotFoundException 异常。
5. 如果要加载的类使用了委派标志或者该类属于 trigger 包中,使用父加载器来加载类,如果父加载器为 null,使用系统加载器加载。
6. 从当前的源中加载类
7. 如果在当前的源中找不到该类并且没有使用委派标志,使用父类加载器。如果父类加载器为 null,使用系统加载器。
8. 如果该类仍然找不到,抛出 ClassNotFoundException 异常。