在前面的章节中我们已经看到了一个简单加载器的实现,一个用来加载servlet的简单加载器。本章将讨论的一个在Catalina中的标准网络应用加载器。一个servlet容器需要定制一个加载器,之所以不使用系统的类加载器,是因为在它上面运行的servlet是不受信任的。就像前面的章节那样,我们使用系统的类加载器来加载servlet和servlet使用到的类,那么,这个servlet就可以获取到所有在虚拟机JVM的CLASSPATH环境变量下所有的类或是类库。这是一个潜在的安全隐患。一个servlet应该是只可以加载到WEB-INF/classes目录或是WEB-INF/classes子目录,或是部署在WEB-INF/lib目录下的类。每一个Web应用(即一个Context)中的servlet容器都有自己的一个加载器。Catalina中的加载器是基于特定的规则来加载类的。在Catalina中一个加载器是由接口org.apache.catalina.Loader表示的。
还有一个Tomcat要实现自己的类加载器的原因是,当WEB-INF/classes目录或是WEB-INF/lib目录下文件发生变化时加载器可以重新加载这些发生变化了的文件。在Tomcat的类加载器中,加载器的实现中使用一个单独的线程持续检测servlet等文件的时间戳。如果要支持自动加载,加载必须实现org.apache.catalina.loader.Reloader接口。
第一节将会介绍Java中的类加载机制,然后是Loader和ReLoader接口,接下来的加载器的实现。
在本章中使用比较广泛的两个词:库(repository)和资源(resources)。库为加载器查找的地方。资源是一个加载器中的DirContext对象,它的文件库指向了Context的文件库。
Java类加载器
第次你创建一个Java类的实现时,这个类必须先被加载到内存中。JVM使用一类加载器来加载类。这个加载器一般会在Java核心类库和CLASSPATH环境变量下所有的目录中查找。如果它没有找到指定的类,就会抛出一个ClassNotFoundException异常。
从J2SE 1.2开始,JVM使用了三个加载器:bootstrap类加载器、extension类加载器和system类加载器。这三个类加载器之间是一种上下级关系,bootstrap类加载器是结构的最顶层,而system类加载器是结构的最底层。
bootstrap类加载器用于启动JVM,当你运行java.exe时这个加载器就会启动。所以这个加载器是由原生代码实现的,它加载的是运行JVM所需要的类(基于不同的平台,这个类的实现也是不同的)。同时,它也会加载Java的核心类库,如java.lang或是java.io包中的类。根据JVM不同的版本或是不同的操作系统,bootstrap会搜索不同的核心类库,如rt.jar或i18n.jar等。
extension类加载器负责加载在标准扩展目录中的类。这可以减轻程序员的工作,只要将jar文件复制到标准扩展目录中就可以自动加载这个jar文件。不同的供应商提供的这个标准扩展目录也是不同的,Sun公司JVM的标准扩展目录是/jdk/jre/lib/ext。
system类加载器是默认的类加载器,查找CLASSPATH环境变量中所有的目录和jar文件。
那么,JVM如何使用类加载器?答案就在委派模型(delegation model)中,使用委派模型主要是出于安全性的考虑。当有一个class类需要被加载时,首先调用的system类加载器。但是,system类加载器并不马上加载这个类。system类加载器会将这个加载任务分派给它的上级extension类加载器,然后extension类加载器将加载任务分派给上级bootstrap类加载器。因此,所有类的加载都是从bootstrap类加载器开始的。如果bootstrap类加载器没有找到需要的类,那么就使用extension类加载器再加载这个类。如果extension再加载失败,使用system类加载器加载,如果还失败抛出ClassNotFoundException异常。为什么要使用这么一个往返的过程呢?
委派模型对于安全性来说是非常重要的,如你所知,可以使用安全管理器来限制访问某个目录。那么现在,某些怀有恶意的人写了一个名为java.lang.Object的类,这个类可以访问硬盘上的任何目录。因为JVM信任java.lang.Object这个类,所以JVM不会关注这个类的行为。结果就是这个自定义的java.lang.Object类就会允许被加载,安全管理器对此也无能为力。幸运的是,由于委派模型这种情况并不会发生。
当一个自定义的java.lang.Object类在程序的某个地方被调用了,system类加载器将这个加载请求传递给extension类加载器,再由extension类加载器传递给bootstrap类加载器。bootstrap类加载器会搜索其核心类库,找到标准的java.lang.Object类并实例化它。这样自定义的java.lang.Object类就根本不会被加载。
Java的类加载机制中最棒的就是你可以通过继承java.lang.ClassLoader类实现一个你自己的类加载器。下面列出了Tomcat为什么需要自定义一个自己的类加载器的原因:
- 在加载类时执行相应的规则
- 缓存之前加载的类
- 预先加载类以便需要时使用
Loader接口
在一个Web应用程序中servlet或是其它类的加载需要遵循一定的规则。举例来说,应用中的servlet可以使用部署在WEB-INF/classes目录以及其子目录下的类。除此之外,servlet并没有其它任何类的访问权限,即使是运行Tomcat的JVM中CLASSPATH环境变量目录中的类也不可以。
Tomcat的加载器是一个Web应用的加载器而不是一个Java的加载器。加载器必须实现org.apache.catalina.Loader接口。加载器由org.apache.catalina.loader.WebappClassLoader表示,它是一个自定义的加载器。在加载器中使用Loader的getClassLoader方法来获取一个ClassLoader。
此外,Loader接口定义了一些方法处理一个库的集合(repositories)。一个Web应用的WEB-INF/classes和WEB-INF/lib目录被视作为库。Loader接口的addRepository方法用于添加一个库,findRepositories方法返回所有库的一个数组。
Tomcat的类加载器往往与一个Context关联,Loader接口中的getContainer方法和setContainer方法就是用于建立这种关联的。如果Context中的一个或多个类发生了变化,加载器也可以支持重新加载。这样的话,一个servlet程序员重新编制servlet或是其它支持类后不需要重启Tomcat就可以被重新加载。为了实现重新加载,Loader接口拥有一个modified方法。在一个加载器的实现中,如果在其库中有一个或多个类发生了变化,modified方法就会返回true,这样就需要重新加载了。加载器并不是自己来做重新加载这个工作,它会直接调用Context接口的reload方法。另外两个方法:setReloadable和getReloadable用来检测Loader中的重新加载是否被启用。默认情况下Conext标准实现类中,重新加载是不启用的。因此,若想启用Context的重新加载功能,你需要在server.xml中的当前Context里添加一个< context >元素,如下:
<Context path="/myApp" docBase="myApp" debug="0" reloadable="true"/>
同时,一个Loader的实现类需要被告知是否要委派任务给其父加载器。为了实现这个目的,Loader接口提供了getDelegate和setDelegate方法。
下面的Loader接口代码:
package org.apache.catalina;
import java.beans.PropertyChangeListener;
public interface Loader {
public ClassLoader getClassLoader();
public Container getContainer();
public void setContainer(Container container);
public DefaultContext getDefaultContext();
public void setDefaultContext(DefaultContext defaultContext);
public boolean getDelegate();
public void setDelegate(boolean delegate);
public String getInfo();
public boolean getReloadable();
public void setReloadable(boolean reloadable);
public void addPropertyChangeListener(PropertyChangeListener listener);
public void addRepository(String repository);
public String[] findRepositories();
public boolean modified();
public void removePropertyChangeListener(PropertyChangeListener listener);
}
Catalina提供了org.apache.catalina.loader.WebappLoader类作为Loader接口的实现类。对于这个加载器,WebappLoader中持有一个org.apache.catalina.loader.WebappClassLoader类实例,这个实例同时也是java.net.URLClassLoader的一个子类。
无论何时,一个关联了加载器的容器需要一个servlet类,当它的invoke方法被调用,容器会首先调用加载器的getClassLoader方法来获取一个加载器的实例。然后容器再调用加载器的loadClass方法来加载servlet类。更多细节将会在后面章节讨论。
Loader接口和其实现类的类图如下:
Reloader接口
类加载器需要实现org.apache.catalina.loader.Reloader接口来支持重新加载功能。Reloader接口如下:
package org.apache.catalina.loader;
public interface Reloader {
public void addRepository(String repository);
public String[] findRepositories();
public boolean modified();
}
Reloader接口中最重要的方法就是modified,返回true就说明Web应用中某个servlet或是其它支持类发生了变化。addRepository方法用于添加一个库,findRepositories方法以String数组方式返回所有的库。
WebappLoader类
org.apache.catalina.loader.WebappLoader类实现了Loader接口,它是一个Web应用加载器负责加载servlet。WebappLoader创建一个org.apache.catalina.loader.WebappClassLoader实例作为它的类加载器。同其它Catalina组件一样,WebappLoader实现了org.apache.catalina.Lifecycle接口,可由与它关联的容器来启动或停止。WebappLoader同时实现了java.lang.Runnable接口,所以它可以启动一个线程来重复检查它的类加载器的modified方法,如果modified方法返回true,WebappLoader就是通知与它关联的容器(在本例中,这个容器就是Context)。由Context执行reloading方法,这里重新加载并不是由WebappLoader来发起的。Context中如何处理这会在后面的章节中详细讨论。
当WebappLoader的start方法被调用时的处理过程如下:
- 创建一个类加载器
- 设置库repositories
- 设置类路径
- 设置权限
- 为重新加载启动一个新线程
下面将详细讨论:
创建类加载器
WebappLoader实例通过内部的一个加载器来加载类。在前面讨论的Loader接口中提供了一个getClassLoader方法,但是并没有提供setClassLoader方法。因此,你不能实例化一个加载器,再将这个加载器传递给WebappLoader。这是否意味着WebappLoader并不能灵活地适配一个非默认的加载器?
答案当然是no。WebappLoader类提供了getLoaderClass和setLoaderClass方法来获取或是修改其私有变量loaderClass的值。这个变量是一个String类型保存的是类加载器的类名。默认情况下这个变量的值为org.apache.catalina.loader.WebappClassLoader。你也可以创建一个继承了WebappClassLoader类的自定义加载器,通过调用setLoaderClass方法来强制WebappLoader使用你定制的加载器。否则的话,启动时WebappLoader就会通过调用它的私有方法createClassLoader来创建一个WebappClassLoader加载器。下面是这个方法的代码:
private WebappClassLoader createClassLoader()
throws Exception {
Class clazz = Class.forName(loaderClass);
WebappClassLoader classLoader = null;
if (parentClassLoader == null) {
// Will cause a ClassCast is the class does not extend WCL, but
// this is on purpose (the exception will be caught and rethrown)
classLoader = (WebappClassLoader) clazz.newInstance();
} else {
Class[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoader) constr.newInstance(args);
}
return classLoader;
}
由以上代码可以看出,除非是一个WebappClassLoader实例,否则使用任何其它的类加载器都会报错,因为createClassLoader方法只能返回WebappClassLoader。因此,如果你的加载器没有继承WebappClassLoader,那么就会抛一个异常出来。
设置库repositories
WebappLoader类的start方法调用了setRepositories方法来为它的加载器添加库。路径WEB-INF/classes被传递给类加载器的addReporitory方法,路径WEB-INF/lib被传递给类加载器的setJarPath方法。这样类加载器就可以从目录WEB-INF/classes以及部署在WEB-INF/lib下的jar文件中查找类。
设置类路径
在start方法中调用setClassPath来设置类路径。setClassPath方法会给servlet上下文分配一个String类型属性保存Jasper JSP编译的类路径,该内容先不予讨论。
设置权限
如果在Tomcat运行时使用了安全管理器。setPermissions方法为加载器加载必要的目录提供访问限制,如WEBINF/classes和WEB-INF/lib。如果没有使用安全管理器,这个方法会马上返回。
为自动加载启动一个线程
WebappLoader支持重新加载,如果在WEB-INF/classes或WEBINF/lib目录下有一个类被重新编译了,在不重启Tomcat的情况下就可以重新加载这个类。为了实现这一点,WebappLoader使用一个线程每隔x秒持续检查每个资源的时间戳是否发生变化。x是通过变量checkInterval的值来确定的,默认情况下这个值为15,也就是说每隔15秒要检查下是否有资源文件的时间戳发生了变化。getCheckInterval和setCheckInterval用来获取和设置这个值。
WebappLoader实现了java.lang.Runnable接口来支持自动加载,其中run方法的实现如下:
public void run() {
if (debug >= 1)
log("BACKGROUND THREAD Starting");
// Loop until the termination semaphore is set
while (!threadDone) {
// Wait for our check interval
threadSleep();
if (!started)
break;
try {
// Perform our modification check
if (!classLoader.modified())
continue;
} catch (Exception e) {
log(sm.getString("webappLoader.failModifiedCheck"), e);
continue;
}
// Handle a need for reloading
notifyContext();
break;
}
if (debug >= 1)
log("BACKGROUND THREAD Stopping");
}
方法中循环执行一个while,这个循环会一直执行started的值为false。
- 将线程休眠checkInterval秒时间
- 调用WebappLoader实例类加载器的modified方法检查已经加载的是否已被修改,如果没有,则继续
- 如果有类被修改,调用私有方法notifyContext,然后调用与这个WebappLoader关联的Context容器去reload。
private void notifyContext() {
WebappContextNotifier notifier = new WebappContextNotifier();
(new Thread(notifier)).start();
}
notifyContext方法并不会直接调用Context接口的reload方法。它先会实例一个WebappContextNotifier的内部类,将它传递给一个线程对象,并调用它的start方法。这样话,reload的执行就会由另一个线程来完成。
protected class WebappContextNotifier implements Runnable {
public void run() {
((Context) container).reload();
}
}
当一个WebappContextNotifier实例被传递给一个Thread线程,并且调用了它的start方法,WebappContextNotifier的run方法就会被调用。然后,run方法就会调用Context接口的reload方法。在后面的章节中有reload方法的详细实现。
WebappClassLoader类
org.apache.catalina.loader.WebappClassLoader类是一个类加载器,负责加载Web应用需要的类。WebappClassLoader类继承了java.net.URLClassLoader类。
WebappClassLoader在设计时进行了必要的优化和安全检查。举例来说,它会缓存之前加载过的类避免重复加载来提供性能。同样,它也会缓存那些在Web应用中没有找的类的类名,这样下次再有相同的加载请求时它就会直接抛出一个ClassNotFoundException异常,而不用在web应用重新查找一遍。WebappClassLoader在源列表以及特定的JAR文件中查找类。
出于安全考虑,WebappClassLoader不允许一些特定的类被加载。这些类被储存在一个String数组triggers中,目前这个数组中只有一个元素。
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
};
Caching
为了提高性能,当一个类被加载的时候会被放到缓存中,这样下次需要加载该类的时候直接从缓存中调用即可。缓存由WebappClassLoader类实例自己管理。另外,java.lang.ClassLoader类维护了一个保存之前加载过的类的Vector来防止这些类垃圾回收掉。
每一个可以被加载的类(放在 WEB-INF/classes目录下的类文件或者 JAR 文件)都被当做一个源。一个源被org.apache.catalina.loader.ResourceEntry类表示。一个ResourceEntry实例保存一个byte类型的数组表示该类、最后修改的数据或者副本等等。
package org.apache.catalina.loader;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.jar.Manifest;
public class ResourceEntry {
public long lastModified = -1;
// Binary content of the resource.
public byte[] binaryContent = null;
public Class loadedClass = null;
// URL source from where the object was loaded.
public URL source = null;
// URL of the codebase from where the object was loaded.
public URL codeBase = null;
public Manifest manifest = null;
public Certificate[] certificates = null;
}
所有缓存的资源都存放在一个名为resourceEntries的HashMap中。键值为资源名。所有没有找到的资源被保存在一个名为notFoundResources的HashMap中。
加载类
在加载一个类时,WebappClassLoader遵守以下规则:
- 所有已加载过类都会被缓存,所以首先会检查本地缓存
- 如果没有在缓存中找到,调用java.lang.ClassLoader类的findLoadedClass方法。
- 如果以第2步的两个缓存中都没有找到(java.lang.ClassLoader会自己维护一个缓存),调用Java的system类加载器,这样做是为了防止Web中有与J2EE类库中重名的类。
- 如果使用了安全管理器,检查这个类是否允许加载,如果不允许加载这个类,抛出ClassNotFoundException异常。
- 如果要加载的类使用了委派标志或者该类属于trigger包中,使用父加载器来加载类,如果父加载器为null,使用系统加载器加载。
- 从当前库中加载类
- 如果在当前的源中找不到该类并且没有使用委派标志,使用父类加载器。如果父类加载器为null,使用系统加载器
- 如果该类仍然找不到,抛出ClassNotFoundException异常。