什么是双亲委托
双亲委托是类加载器的一个特性,当加载一个类的时候,首先会委托父加载器去加载,如果父加载器加载不到,才让子加载器尝试加载。
这样做的好处。
第一,保证了类的唯一性,不会被重复加载。
其次是安全性,你无法通过实现同包同名类来篡改jvm的内部类的行为。(想象一下你引入的jar包存在这种类,如果没有这种安全性做保证,会发生啥)
关于类加载器,我还想提两点
- 如果你使用A加载器加载了A类,那么A类中引用的B类,也会通过A加载器加载。不过A加载器加载B类的时候,还是会走双亲委托。
- 对于每个类加载器,他都会对应一些资源文件,这也确定哪些类会唯一被那个加载器加载。比如BootstarpClassLoader对应加载/jre/lib下的jar包。
破坏双亲委托案例
ServiceLoader
ServiceLoader是jvm内部类,它被BootstrapClassLoader所加载。但是ServiceLoader所加载的扩展点类,不能被BootstrapClassLoader加载。所以在ServiceLoader会通过它内部的持有
的loader类加载器加载这些扩展点类。
loader会在ServiceLoader构造的时候传入。
可以看到有两种情况,第一种使用我们传入的classloader,第二种使用线程上下文中的contextClassLoader。
对于第一种情况,如果传入的classloader为null,也可以理解为你想使用BootStrapClassLoader去加载扩展类,会强制替换为AppClassLoader
对于第二种情况,我们讲解下contextClassLoader绑定的classloader来自于哪里。
默认情况下,子线程的contextClassLoader会继承父线程的contextClassLoader,而最顶层的父线程也就是main线程,它的contextClassLoader为AppClassLoader。
当然你可以在使用ServiceLoader前,通过Thread.currentThread().setContextClassLoader(xxx);
改变线程中的contextClassLoader。
举一个需要改变的例子。
比如的你的扩展点实现类存储在数据库中,这样就肯定用不了AppClassLoader了。
Thread.currentThread().setContextClassLoader(customeClassLoader);
ServiceLoader.load(xxxInterface.class)
哇塞,这好像是一种远程spi的解决方案。
个人感觉ServiceLoader这种处理方式,没有破坏双亲委托,在类加载器内部还是符合双亲委托的,只算是破坏了常规的类加载流程,这种取巧方式只是为了解决这种特定的问题。而tomcat的WebAppClassLoader那是真的破坏了双亲委托。
Tomcat WebAppClassLoader
在我们实现自定义ClassLoader的时候,推荐我们只实现 findClass
方法,而不建议我们重载loadClass
方法。
为啥?因为双亲委托就封装在这个方法中,如果我们破坏,可能会带来不可预知的风险。比如一个类加载了两份。
但是Tomcat真正破坏了双亲委托,显然是修改了这个方法。
看下WebappClassLoade的loadClass方法实现
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
//缓存
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
//缓存
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
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;
}
//如果是jvm内部类 走javaseLoader=extclassloader
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//delegateLoad=true 表示先走commonclassloader
//filter方法用于判断当前加载类是否为tomcat本身需要加载的类
//delegate默认为false
boolean delegateLoad = delegate || filter(name, true);
//delegateLoad=true的情况下,交由parent加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + 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
}
}
//从我们的war包路径下搜索类
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
}
//如果delegateLoad为false,交由parent加载
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);
}
tomcat8在默认配置下,类加载器继承结构为
Bootstrap
|
EXT
|
APP
|
Common
/ \
Webapp1 Webapp2 ...
如果在 conf/catalina.properties对server.loader 或shared.loader 做了配置,类加载器继承结构如下
Bootstrap
|
EXT
|
APP
|
Common
/ \
Server Shared
/ \
Webapp1 Webapp2 ...
我们默认使用第一种方式讲解,也是是上面源码中的parent
为CommonClassLoader
CommonClassLoader默认加载以下路径的资源
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
修改后的loadClass方法大致逻辑如下(delegate默认为false)
- 先走class加载缓存
- 如果class为jvm内置类,走extClassLoader
- 如果filter方法返回true,走commonClassLoader
- 通过WebAppClassLoader加载/WEB-INF/classes和/WEB-INF/lib/*.jar中的类
- 如果filter方法返回false,走commonClassLoader
filter方法内的大致逻辑是,判断该class是否是tomcat自己的类。
也就是是否是tomcat源码中这些类。
从loadClass逻辑中,对于webapp中的类,直接通过WebAppClassLoader去加载,不走双亲委托机制。
这种做法能保证tomcat中部署的2个应用的隔离。如果没有这个改造,那么A应用加载版本1的类后,B应用也被迫使用这个版本,如果B依赖的版本和A不同,那么B应用可能会发生错误。