载入器

载入器是Catalina最重要的组件,servlet容器需要实现一个自定义的类载入器,出于以下原因:

  • 将WEB-INF/classes目录下的应用类与更高级别的容器类用不同的classloader隔离开来
  • 根据classes文件是否发生变化实现类的自动重新装载

Java的类加载器

类加载器的双亲委托模型

Loader接口

Loader接口指的是Web应用程序载入器,而不仅仅是类载入器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Loader {
// properties
// 使用的classloader实例
ClassLoader getClassLoader();
// 绑定的context级别的container
Container getContainer();
DefaultContext getDefaultContext();
// 是否委托给一个父类载入器
boolean getDelegate();
String getInfo();
// 是否支持自动重载
boolean getReloadable();
// method
void addPropertyChangeListener(PropertyChangeListener listener);
void addRepository(String repository);
String[] findRepositories();
// 仓库中的class被修改了,触发类重新加载事件
boolean modified();
void removePropertyChangeListener(PropertyChangeListener listener);
}

WebappLoader类

WebappLoader类实现了Loader接口,其实例就是Web应用的载入器。
像其他Catalina组件一样,WebappLoader类也实现了Lifecycle接口,可以由其关联的容器来启动、关闭组件。
此外,WebappLoader类还实现了Runnable接口,可以指定一个线程来不断调用modifed()方法来检查class是否被修改。

当调用WebappLoader类的start()方法时,会完成以下工作:

创建类加载器

WebappLoader类并没有声明setClassLoader()方法来直接设置classloader,而是声明了setLoaderClass()来设置其私有变量loaderClass的值。这个私有变量保存了字符串类型的classloader类全名,其默认值是"org.apache.catalina.loader.WebappClassLoader"

WebappLoader类在启动时调用其方法createClassLoader()来创建类加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

设置仓库

调用setRepositories()方法,利用classloader.addRepository()将"/WEB-INF/classes"新增仓库,利用classLoader.setJarPath()将"/WEB-INF/lib"设置为jar仓库。

设置类路径

调用setClassPath()方法,解析classloader链条中的repository集合,组装classpath信息。

设置访问权限

调用setPermissions()方法为类载入器设置访问相关目录的权限。

开启新线程执行类的重新载入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void run() {
// 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;
}
}

循环调用classLoader.modified()方法检查是否被修改。一旦为true,通知context容器去重新载入类。

WebappClassLoader类

WebappClassLoader类的设计方案考虑到了优化和安全两方面,例如:

  • 缓存之前已经载入成功或失败的类来提升性能
  • 禁止载入指定类(如javax.servlet.Servlet)
  • 委托系统类载入器载入指定包下的类(如javax, org.xml.sax等)

类缓存

每个由WebappClassLoader载入的类都被视作“资源”,对应class是org.apache.catalina.loader.ResourceEntry

1
2
3
4
5
6
7
8
9
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;
}

所有已加载的类都保存在protected HashMap resourceEntries中,其key就是资源的名称。
所有载入失败的类都保存在protected HashMap notFoundResources中。

载入类

载入类时,遵循如下规则:

  1. 先检查本地缓存
  2. 如果本地缓存中没有,则检查上一层缓存,即调用java.lang.ClassLoader#findLoadedClass(String)方法
  3. 若2个缓存都没有,则使用系统的类载入器加载,防止Web应用中的类覆盖JEE类
  4. 若采用了SecurityManager,则检查是否允许载入该类
  5. 若打开delegate标志位,或待载入的类属于
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * Set of package names which are not allowed to be loaded from a webapp
    * class loader without delegating first.
    */
    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
    };

则委托父载入器来加载相关类

  1. 从仓库中加载类
  2. 如果delegate关闭,委托父载入器加载类
  3. 抛出ClassNotFoundException