9.8 Web应用中的类加载器
类加载器在基于Java EE技术实现的Web容器中有着非常重要的作用。在一个Web容器中通常运行着很多个Web应用。这些应用之间是互相隔离的,互不影响,但又都依赖于Web容器提供的功能,运行在同一个Java虚拟机之上。这种受管理的隔离方式是通过类加载器来实现的。典型的做法是每个Web应用使用自己的类加载器对象来加载应用中包含的Java类和资源。不同的Web应用中相同名称的Java类可以共存于虚拟机中。比较典型的场景是对Web应用中使用的第三方库的处理。不同Web应用在开发中可能使用同样的第三方库,但是使用的库的版本可能不同。如果没有进行隔离,那么所有Web应用都会引用某一个版本的库中的Java类。某些应用可能会因为使用了错误版本的库而出现错误。
在Java EE中的Servlet规范中给出了Web应用的类加载器的实现方式的推荐做法,即对默认的双亲优先的代理模式进行修改,改为使用当前类加载器优先的方式。这种做法的出发点是解决Web应用中的第三方库和容器本身使用的第三方库的冲突问题。如果采用双亲优先的方式,那么容器中提供的第三方库的Java类会被优先加载。Web应用可能使用了同样的库,但是版本与容器提供的并不兼容,这会造成Web应用出现错误。通过提高Web应用本身的Java类和第三方库的优先级,可以避免这个问题。一般的Web容器都遵循Servlet规范中的推荐做法。有些应用服务器允许Web应用在双亲优先和当前类加载器优先这两种策略中进行选择。
下面通过具体的Apache Tomcat 7.0示例分析来说明Web容器中类加载器是如何工作的。Tomcat是一个使用很广泛的Web容器,也是开放源代码的。Tomcat中的Web应用对应的类加载器是org.apache.catalina.loader.WebappClassLoader类的对象。WebappClassLoader类继承自标准库中的java.net.URLClassLoader类。在类加载器中,最重要的是loadClass方法的实现。WebappClassLoader类中loadClass方法的实现按照下面几个步骤尝试加载Java类。
1)调用findLoadedClass来查找该Java类是否已经被加载过。一般的类加载器都会进行这样的检查,可以避免不必要的查找过程。
2)调用系统类加载器的loadClass方法来尝试加载类。这是为了避免Web应用覆盖Java标准库中的类。
3)WebappClassLoader类不一定会把加载类的请求代理给双亲类加载器。在两种情况下会进行代理。第一种情况是使用setDelegate方法把代理模式打开之后。在默认情况下代理模式是关闭的。第二种情况是要加载的Java类的名称满足一定的条件,比如名称以“javax.servlet”开头的Servlet API相关的Java类是由双亲类加载器来加载的。
4)调用findClass方法来查找Web应用本身的Java类。
5)如果在第3)步中没有把加载类的请求代理给双亲类加载器,则在这一步中进行。从第4)和第5)步的顺序可以看出,WebappClassLoader类使用的是当前类加载器优先的策略。
对于Web应用本身的Java类,Tomcat是按照划分成不同仓库的方式来进行管理的。每个仓库表示一个Java类的存放位置。仓库分成外部仓库和内部仓库两种。每个外部仓库对应一个URL类的对象,表示一个加载类时的查找路径。由于WebappClassLoader类继承自URLClassLoader类,通过URLClassLoader类中的addURL方法可以添加新的外部仓库。内部仓库则指的是Web应用本身的WEB-INF目录下的classes和lib目录。这两个标准目录分别用来存放Web应用的class文件和使用的第三方库的jar包。WebappClassLoader类的对象可以配置在查找时是否优先外部仓库。在WebappClassLoader类的findClass方法中的查找过程如下:
1)如果存在外部仓库并且配置了优先查找外部仓库,则先调用双亲类加载器对象的findClass方法进行查找。
2)在内部仓库中进行查找。
3)如果存在外部仓库并且配置了不优先查找外部仓库,则说明第1)步没有执行。此时调用双亲类加载器对象的findClass方法来进行查找。
在Web应用开发中,每个Web应用自己的Java类文件和使用的库的jar包,分别放在WEB-INF目录下的classes和lib目录下。多个应用共享的Java类文件和jar包,则放在Web容器指定的由所有Web应用共享的目录下面。通过这种标准的方式,可以避免一些常见的类加载相关的错误。