Java_讨论类加载器的双亲委派机制
双亲委派机制
简而言之,当某个类加载器在接到加载请求时,优先会将任务委托给父类加载器,一直到最顶层的类加载器,如果不能成功加载,才会尝试自己加载
java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
..
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
按照类库的设计思路,如果想实现自己的类加载器,我们应该重写findClass(),这样不会破坏双亲委派机制
优点和限制
优点比较明显,java核心类永远由上层类加载器加载(索引指定位置),不会出现代码里自定义了一个java.lang.String类型,把标准库里覆盖的情况出现;限制呢,jdk9之前的限制(jdk9的模块化还没有看懂)我知道的有两点
SPI接口
java提供了服务提供者接口,允许第三方为这些接口提供实现。拿jdbc举例子,jdbc驱动管理类java.sql.DriverManager显然是核心类,而驱动类在哪呢,在用户的classPath下。问题来了,加载DriverManager使用的上层类加载器,只在特定目录寻找class,找不到驱动类,想找到怎么办?驱动类在用户目录,换一个索引用户目录的类加载器就好了,全名是Application ClassLoader
java设计者将类加载器藏得比较隐晦,并没有直接new一个App类加载器,而是增加了和当前线程绑定的类加载器,在jvm.Main()中被赋值,默认就是App类加载器。严格意义上这不算打破,而是绕过了双亲委派机制
DriverManager.loadInitialDrivers() -> ServiceLoader.load(Driver.class)
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Tomcat类加载器
Tomcat是Servlet容器,Servlet规范中定义每个Web应用程序都应该使用自己的类加载器,以允许容器中的不同部分以及在容器上运行的Web程序可以拥有类相互隔离和公用的功能
在tomcat里不同的web程序都是放在一个目录下的,web1和web2里定义了相同的类,但是实现不一样;同样的,tomcat自己还有一些代码,web程序若包含同名类同样可能冲突;这很容易想到每个web程序需要有自己的类加载器,而且这个类加载器需要优先加载自己本应用目录下的类。当然,JVM的核心类,还是要放在最开始加载
看一下tomcat的web类加载器是怎么玩的,javaseLoader就是JVM的核心类加载器了,思路还是清晰的
org.apache.catalina.loader.WebappClassLoaderBase
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
..
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
} catch (ClassCircularityError cce) {
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
..
}
// (2) Search 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) Delegate to parent unconditionally
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);
}
测试
第一个SPI不容易理解,本地模拟一下不同类加载器的表现,展示为什么需要打破双亲委托机制
不同类加载器加载的类是不同的
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String filename = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(filename);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Class<?> testClazz = myLoader.loadClass("my.ClassLoaderTest");
System.out.println("the classLoader of Test is " + testClazz.getClassLoader());
Object obj = testClazz.newInstance();
System.out.println(obj instanceof ClassLoaderTest);
the classLoader of Test is my.ClassLoaderTest$1@232204a1
false
不同加载器之间的目录隔阂
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
name = name.replace("@", "");
try {
String filename = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(filename);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Class<?> testClazz = myLoader.loadClass("my.Test");
System.out.println("the classLoader of Test is " + testClazz.getClassLoader());
Object obj = testClazz.newInstance();
Method test = testClazz.getDeclaredMethod("test");
// Thread.currentThread().setContextClassLoader(myLoader);
test.invoke(obj);
}
}
public class Test {
public void test() throws ClassNotFoundException {
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println("the class loader that will be used is " + classLoader);
Class<?> a = classLoader.loadClass("@my.A");
System.out.println("the classloader of A is " + a.getClassLoader());
}
}
class A {}
这里自定义类加载器支持加载@开头的类名,可以理解为同app加载器的一个目录上的区分。这段程序执行是报错的,因为app加载器无法加载@my.A
the classLoader of Test is my.ClassLoaderTest$1@232204a1
the class loader that will be used is sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at my.ClassLoaderTest.main(ClassLoaderTest.java:32)
Caused by: java.lang.ClassNotFoundException: @my.A
如何加载,注释里的内容使用了同SPI一样的解决方式,使用当前线程绑定的类加载器,放开注释
the classLoader of Test is my.ClassLoaderTest$1@232204a1
the class loader that will be used is my.ClassLoaderTest$1@232204a1
the classloader of A is my.ClassLoaderTest$1@232204a1
加载成功。这个例子想表达的是,通过人为添加@
,模拟出两种加载器的不同加载目录,通过线程加载器可以解决。想了一下,这似乎只能处理核心类想加载用户类的情况(这对java类库本身已够用),更多的需求,还是要像tomcat一样自定义实现
思考
这里类加载器知识点看了好多年了,之前一直不理解为什么需要打破双亲委派机制(这是可以打破的么??),处于一种似懂非懂的状态里。最近拿起tomcat源码,有一种豁然开朗的感觉,还顺带把之前不理解的SPI也打通了。双亲委派机制只是一种编程规范或者说设计思路,也可以有别的设计想法。知识,总是常读常新。