Servlet规范定义的类加载顺序
在Servlet规范中有对web应用程序类的加载方式作出建议,重要的有两点:
- 容器要加载某个类时,类加载器首先应该加载本地web应用程序中“WEB-INF/classes”路径中的类,然后再到“WEB-INF/lib”依赖库中加载类,最后在容器级别的lib中加载类;
- 同时,类加载器要保证应用程序不会覆盖Java核心类,即
java.*
和javax.*
命名空间中的类,也就是说web应用程序如果定义了一个和java核心类名字相同的类则是无效的。
Tomcat的应用程序类加载器
Tomcat内部有多种类型的类加载器,其中WebappClassLoader
是应用程序类加载器,tomcat启动时对于它管理的每个web应用程序都会创建一个单独WebappClassLoader
实例,在tomcat内部的StandardContex
类中的启动方法中找到。
根据servlet规范,容器要首先在本地应用程序库中加载请求的类,同时要避免应用程序中的类覆盖Java平台类库中的核心类,WebappClassLoader
加载器为了实现这些要求没有遵循java的父加载器委托的模型,它重写了ClassLoader的loadClass
方法,重写的loadClass
方法的源代码细节如下:
// 代码省略了一些与类加载逻辑关系不大的细节,通过分析这段代码的逻辑来了解是否复合servlet规范的建议。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// ...
Class<?> clazz = null;
// ...
// (0) 检查是否已经从本地库中加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.1) 检查是否此加载器是否已经加载过这个类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2)
String resourceName = binaryNameToPath(name, false);
// getJavaseClassLoader方法返回的是引用javaseClassLoader,这个引用变量
// 引用的是java内建的加载:bootstrapClassLoader,或者systemClassLoader,或者extClassLoader。
// 这里首先使用java内建的加载器加载,防止应用程序中用户定义的类和覆盖java核心类,
// 如果不先用内建加载器加载,而首先加载本地程序库中的类,如果应用程序类和java核心类
// 同名,则java核心类无法被加载。
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 通过上面的三个步骤后,说明name类没有被加载过,并且也不是java的核心类,
// 接下来按照servlet的规范到本地应用程序库中加载。
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
// 检查访问权限 ...
}
// filter方法是检查类名是否属于javax命名空间下的核心类或者是tomcat的核心类
// delegate是可以通过外部控制的变量
boolean delegateLoad = delegate || filter(name, true);
// (1) 这里的if块说明两点:
// 第一是tomcat也支持使用java的上层委托加载机制,只要把delegate字段设置为true即可,
// delegate默认是false,说明tomcat默认并没有开启委托加载机制。
// 第二是对于javax命名空间的核心类和tomcat本身的核心类使用委托加载机制。
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
// forName采用的就是委托加载机制
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) 没有使用委托加载机制,到本地应用程序库中加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) 没有使用委托机制加载过,并且本地应用程序库中也没有加找到类,最后再委托到父加载器加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
WebappClassLoader实现细节的关键点总结如下:
- 为了满足前面说的servlet加载规范的第二点要求,在loadClass方法的(0.2)步,tomcat首先使用了java内建的加载器加载类,这个类加载器大多数情况下是bootstrap。
- 为了满足servlet规范第一点要求,tomcat首先去/WEB-INF/classes,/WEB-INF/lib以及其他自定义的URL路径下加载,在这些路径下如果找不到类,然后才委托到父加载器加载,这和java的父类加载器委托机制相反,loadClass方法中的(1)(2)(3)这几个步骤就是实现的这点要求。
- tomcat的类加载机制也可以完全兼容父类加载器委托机制,即首先使用父类加载器加载,父加载器找不到类的话,然后再到本地应用程序库中加载类,这只需要把
delegate
字段值设置为true即可。
Tomcat的类加载器层次
Tomcat内部实现的类加载器有很多种,上面分析的WebappClassLoader
就是属于web应用程序的类加载器,它负责加载web应用程序中的业务类及其他资源文件。Tomcat在启动时默认情况下会创建出下面这几种类加载器,并且它们的层级关系如下:
bootstrap
/|\
system
/|\
common
/ \
webapp1 webapp2 ......
这些类加载器的不同之处其实就是它们查找类的路径不相同:
- bootstrap就是虚拟机内部的加载器,它负责加载Java平台核心类。
- system加载器就是Java平台内建的system加载器,负载加载CLASSPATH路径下的类,CLASSPATH默认是当前工作目录,但是tomcat重新定义了CLASSPATH,在启动脚本
catalina.sh
中把CLASSPATH的值设置为下面这几个路径:$CATALINA_HOME/bin/bootstrap.jar
,tomcat服务器实例的启动类包含在里面。$CATALINA_BASE/bin/tomcat-juli.jar
或者CATALINA_HOME/bin/juli.jar
。$CATALINA_HOME/bin/commons-daemon.jar
,这个jar没有在catalina.sh脚本中显示设置为CLASSPATH的一个值,而是通过bootstrap.jar中引用的。
- common加载器负责加载通过“$CATALINA_HOME/conf/catalina.properties”文件中的
common.loader
属性定义的类:
这个定义实际上包含的类如下:common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
$CATALINA_BASE/lib
路径下所有的class文件和其他资源$CATALINA_BASE/lib
路径下所有的jar文件$CATALINA_HOME/lib
路径下所有的class文件和其他资源$CATALINA_HOME/lib
路径下所有的jar文件
- webapp加载器则是每个web应用程序都会创建的,专门负责加载web应用程序本地库
/WEB-INF/classes/
路径下的class和资源文件,也包括/WEB-INF/lib/
路径下的所有jar中打包的class文件以及资源文件,也可以加载其他明确指定的URL路径的资源,webapp loader只会在这几个地方加载类和资源。
更细致的类加载器层次
实际上tomcat的类加载器的层次结构还可以设定的更细致一些,可以配置另外两种加载器:server loader和shared loader,这两种类加载器和其他类加载器的层次关系现在如下:
bootstrap
/|\
system
/|\
common
/ \
server shared
/ \
webapp1 webapp2 ......
server loader只对tomcat自己内部的类可见,shared loader对所有的web应用程序的类都是可见的,可以用来加载所有web应用程序所共用代码。实际上server loader和shared loader加载的路径只是对common loader的细分,因此common loader可以用来管理tomcat内部和外部web应用程序所共同依赖的类。
如果要开启这两个类加载器,需要分别为“$CATALINA_HOME/conf/catalina.properties”文件中的server.loader
和shared.loader
这两个属性设置路径。
多层次结构加载器的好处
Tomcat的这种多类型多层次结构的类加载器实现机制有一个很好的好处是:既能实现类的隔离,又能实现类的共享。
通常的部署模式我们一般是一个tomcat容器中运行一个web应用程序,但是如果我们想要在一个tomcat容器中部署多个web应用程序,可能出现两个问题,首先如果两个web程序中有两个相同名字的类,但是它们的实现代码是完全不同的,类加载器如何区分两个类;另一种情况是两个web应用程序中依赖于同一个第三方库,但是依赖的版本不一致,这时如何正确的加载第三方库中的类。这两种情况其实就是要隔离不同的应用程序,tomcat就是通过为每一个web应用程序都创建一个独立的WebappClassLoader
来实现的,不同的WebappClassLoader
只会加载自己程序里面的类。
共享则是通过父加载器来实现的,共享类能减少内存区空间的占用,例如我们在一个容器中部署了多个web应用程序,假如它们都依赖于相同版本的Spring框架,那么其实可以共用Spring的类,而不必每个应用程序都独自加载一遍Spring的类,这个时候shared loader就是用来解决这个问题的,我们可以把Spring库放到shared loader查找的路径下,所有应用程序要加载Spring的类时都使用同一个shared loader来加载,因此最终方法区只会存在一份Spring类。上面我们分析过应用程序类加载器WebappClassLoader
是能够向上委托到shared loader的,当它不能在本地应用程序库中找到类时,会委托到父加载器加载。
与shared loader类似,common loader也是一个用来加载共用类的加载器,只是common loader加载的类对tomcat自己和应用程序都是共享的。
来源:oschina
链接:https://my.oschina.net/u/3227308/blog/3224486