《How tomcat works》读书笔记_加载器

在前面的章节中我们已经看到了一个简单加载器的实现,一个用来加载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。

  1. 将线程休眠checkInterval秒时间
  2. 调用WebappLoader实例类加载器的modified方法检查已经加载的是否已被修改,如果没有,则继续
  3. 如果有类被修改,调用私有方法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遵守以下规则:

  1. 所有已加载过类都会被缓存,所以首先会检查本地缓存
  2. 如果没有在缓存中找到,调用java.lang.ClassLoader类的findLoadedClass方法。
  3. 如果以第2步的两个缓存中都没有找到(java.lang.ClassLoader会自己维护一个缓存),调用Java的system类加载器,这样做是为了防止Web中有与J2EE类库中重名的类。
  4. 如果使用了安全管理器,检查这个类是否允许加载,如果不允许加载这个类,抛出ClassNotFoundException异常。
  5. 如果要加载的类使用了委派标志或者该类属于trigger包中,使用父加载器来加载类,如果父加载器为null,使用系统加载器加载。
  6. 从当前库中加载类
  7. 如果在当前的源中找不到该类并且没有使用委派标志,使用父类加载器。如果父类加载器为null,使用系统加载器
  8. 如果该类仍然找不到,抛出ClassNotFoundException异常。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值