Java中的类加载器
类加载器是Java语言的创新,它负责通过类的全限定名找到其对应的字节码,并且从中定义出Java类对象即java.lang.Class的实例。虚拟机的设计者甚至运行开发者定义自己的类加载器,实现自己的类加载逻辑(但是要遵循一定得规则,后面会讲到)。
一,双亲委派
所有的知道类加载器classloader的Java开发者都应该知道它的双亲委派模型。类加载器在加载一个类之前,先尝试通过其父加载器来加载这个类,类似于Spring的BeanFactory。这样做的好处是,父加载器加载过得类如java.lang.Object,子加载器不会再进行加载,保证了应用程序的正确执行。那么,有的人可能有想法,我可以自己写一个java.lang.Object类然后通过自定义的加载器来加载。这样是行不通的,因为虚拟机已经做了限制。下面来分别说说系统中类加载器
1,引导类加载器(bootstrap class loader)
它用来加载 Java 的核心库,即存放在JAVA_HOME/lib目录中的类,这个目录也可以通过-Xbootclasspath参数指定。但是,虚拟机是识别文件名字的,一个陌生的jar包即使放到启动类的目录中也不会被加载。引导类加载器是用原生代码来实现的,并不继承自 java.lang.ClassLoader,也无法被开发者直接引用
。
2,扩展类加载器(extensions class loader)
扩展类加载器用来加载 Java 的扩展库,它的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载的目录为JAVA_HOME/lib/ext,同时也可以用java.ext.dirs系统变量指定。
3,系统类加载器(system class loader)
它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类,其实现类为sun.misc.Launcher$AppClassLoader。一般来说,如果应用程序没有自己实现类加载器的话,Java 应用的类都是由它来完成加载的。我们可以通过 ClassLoader.getSystemClassLoader()
来获取它。
二,线程上下文类加载器
类加载器的双亲委派模型带来的好处显而易见,但是也有它的局限性。在只能使用父加载器的场景下,无论如何都不能加载到子加载器才能加载到的类。也就是说,如果我们可以打破双亲委派,自己指定系统或当前线程使用哪个类加载器就好了。java.lang.Thread中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)就可以
用来获取和设置线程的上下文类加载器。如果我们没有主动设置的话,线程会从父线程继承原来的类加载器。线程上下文类加载器的典型应用就是JNDI服务。
三,实现自己的类加载器
首先,实现自己的类加载器要继承ClassLoader或者其子类。其次,最好不好复写loadClass方法,而是复写findClass方法,因为这样可能破坏双亲委派原则导致系统异常。如果必须要复写loadClass,别忘了基础类还是要通过父加载器进行加载。任何一个类都是Object的类,所以不论是什么类加载器都必须可以直接过间接地加载到Object类。下面是Classloader中方法的说明:
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) | 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) | 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
四,加载资源
ClassLoader不仅可以加载类,同时也可以加载资源。另外,Class对象也可以进行资源的加载,它们的方法都是getResource(String name)和getResourceAsStream(String name)。它们在路径的查找方式上有些不同,如下面代码:
public static void main(String[] a){
Class clazz = ResourceLoad.class;
ClassLoader classLoader = ResourceLoad.class.getClassLoader();
System.out.println(clazz.getResource(""));
System.out.println(clazz.getResource("/"));
System.out.println(classLoader.getResource(""));
System.out.println(classLoader.getResource("/"));
}
Classloader在加载资源的时候选择的是从classpath目录下开始查找,传入的路径是相对于classpath的,这个方法是可以被重写的。而通过class加载资源,传入的路径是相对于当前类的,或者以“/”开头表示一个绝对的路径(相对于classpath)。getResourceAsStream也是相同的道理。其实class最终也会通过classloader来加载资源,只不过是利用下面的方法做了路径的转换
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
*/
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}