起因
针对下面这个代码
public static void loadFile(){
try(InputStream is = XXX.class.getClassLoader.getResourceAsStream("xxxxx.txt"){
//xxxxxx
}
}
有同事提出为啥此处要用“XXX” claas,用其它的class是否可行?
然后我一通解释,甚至还般出了Thread.currentThread().getContextClassLoader().loadClass("xxx.xxx.xx")
的方案。但是进一步探讨时,发现成功的给自己挖了个坑。有以下几个问题自己都解释不通。
- Thread上下文获取到的类加载器与Class.getClassLoader()拿到的有什么区别
- 在加载一个类的时候,是如何找到一个类的位置的
- 在加载文件的文件,第三方jar包中的配置文件与自己项目下的Resource目录下的加载方式有没有什么区别,是否需要不同的类的ClassLoader
- 在加载文件或者类资源的时候,对文件的命名有何要求
在上面几个问题的引导下,重新研究了下ClassLoader的相关文档,并且写例子验证了这些问题,从而有了此文。
ClassLoader原理阐述
ClassLoader,顾名思义,就是类加载器。当我们调用一个new语句时,相应的class文件需在被加载到Jvm中。此时ClassLoader就需要负责查找class,读class文件,校验,连接,最终加载到Jvm中
java提供的默认ClassLoader
- BootStrap ClassLoader: 加载位于
System.getProperty("sun.boot.class.path")
处的class文件,因此可以通过sun.boot.class.path
参数设置BootStrap ClassLoader负责加载的类 - ExtClassLoader:加载位于
System.getProperty("java.ext.dirs")
的类 - AppClassLoader:也称
SystemClassLoader
, 加载位于System.getProperty("java.class.path")
处的类
- BootStrap ClassLoader: 加载位于
ClassLoader的双亲委托机制
ClassLoader是一个虚类,除BootStrap ClassLoader外,每一个ClassLoader都需要继承ClassLoader,并且每个都有一个其父ClassLoader的引用(同样,BootStrap除外)。BootStrap ClassLoader是C++ 语言写的,在系统启动的时候,就会启动BootStrap类加载器。BootStrap ClassLoader是ExtClassLoader的父加载器,但是如果获取ExtClassLoader的父加载器,拿到是null;ExtClassLoader 是AppClassLoader的父加器
但是双亲委托机制并不是绝对的,像后文中提到的SpringBoot的 RestartClassLoader就打破了该机制。
在jvm启动的时候,会加载类sun.misc.Launcher
,在该类的造函数中,会创建ExtClassLoader及AppClassLoader,并且把ExtClassLoader设置为AppClassLoader的父ClassLoader,同时设置线程的上下文加载器为AppClassLoader。类加载过程(默认的
loadClass
流程)- 首先AppClassLoader会检查是否加载过该类,如果加载过,直接返回
- 没有加载过,委托父加载器进行加载(父加载器执行同样的流程)
- 父加载器加载成功,直接返回,如果没有加载成功,则当前加载器开始查找Class再进行加载。
ClassLoader进行了封装,对于子类,不建议覆盖
loadClass
流程,只实现其findClass
方法即可,这样就可以不破坏加载器的双亲委机制。所以对于自定义的ClassLoader而言,实现如何查找类,在哪里查找类就是关键。对于查找资源文件,与加载类一致,在下面的源码分析中会提到。
对于线程的上下文加载器,等下在源码分析中也可以看到一点,总体而言就是将一下加载器保存在线程的上下文中,在某些特定的场景中使用,比如tomcat中会使用到。同时线程的上下文加载器与当前的类可能并不一是致。
ClassLoader相关源码分析
看com.sun.Launcher
源码(分析默认的几个类加载器的构造过程)
public class Launcher {
//系统的ClassLoader,后面会被赋值为AppClassLoader
private ClassLoader loader;
//BootStrap ClassLoader要加载的类的范围
private static String bootClassPath = System.getProperty("sun.boot.class.path");
public Launcher() {
// 创建ExtClassLoader
ClassLoader extcl = ExtClassLoader.getExtClassLoader();
// 创建AppClassLoader,同时设置extcl为其父加载器
loader = AppClassLoader.getAppClassLoader(extcl);
//设置线程上下文类加载器为 AppClassLoader Thread.currentThread().setContextClassLoader(loader);
}
/*
* 获取类加载器
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* 具体加载ExtClassLoader的过程,并且可以看出,其继承自URLClassLoader
*/
static class ExtClassLoader extends URLClassLoader {
private File[] dirs;
// 创建方法
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
return (ExtClassLoader) AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
// 第二个参数就是parent ClassLoader,此处可以看到为null
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
// 处理s相关的内容
return dirs;
}
}
/**
* 创建AppClassLoader
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
// 参数 System.getProperty("java.class.path")
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return (AppClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
// 此处可以看到ExtClassLoader 是 AppClassLoader的父ClassLoader
super(urls, parent, factory);
}
}
}
上面代码可以看到,在创建Launcher的时候,创建了ExtClassLoader,并且将其parent ClassLoader设置为null,创建了AppClassLoader将ExtClassLoader设置为其parent ClassLoader。 同时可以看到每个ClassLoader都继承自URLClassLoader,并且可以看到各ClassLoader负责加载的类的范围
分析ClassLoader的源码
public abstract class ClassLoader {
// 保存的地父加载器的引用
private final ClassLoader parent;
// 加载class文件的核心代码
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) {
long t0 = System.nanoTime();
try {
// 如果父加载器不为空,则委托父加载器进行加载,可以看到双亲委托机制
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 加载器为空,即ExtClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//没有加载到类,抛异常后,吞掉
}
// 如果父加载器没有加载到,轮到自己进行加载
if (c == null) {
// 这个方法是一个虚方法,还未实现,子类实现即可
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
从上面代码可以看到,查找类的时候,采用双亲委托机制,先委托父类进行查找类,如果找不到自己再进行查找。
再来简单介绍一个URLClassLoader:ClassLoader中有一个URLClassPath属性,URLClassPath包含一个List的属性,该属性就保存着当前ClassLoader可以查找的范围。当URL以’/’结尾时,说明是一个directory,当是以非’/’结尾的时候,说明是一个jar包。当查找类或者资源文件时,遍历该URL集合,根据是directory还是jar包分别进行处理。
再看查找资源文件,getResource的方法:
//class 的getResource文件, Class 类
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
可以看到,Class.getResource() 最后是通过调用ClassLoader.getResource()
实现的。当前在此之前,先进行了resolveName(name)
方法。该方法的作用是获取该资源文件相关对于ClassPath的路径,即如果以’/’打头,则认为是一个完整路径,去掉’/’,如果以非’/’打头,则认为是相对当前类文件的地方,则会将当前Class文件的package名加到当明的name前,并且将’.’用’/’替换,形成相对于ClassPath的路径。
ClassLoader的的getResouce的方法
// 同样执行双亲委托机制
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
// findResource 是一个虚方法,子类可以实现,与类加载的过程类似。
// 而对于AppClassLoader以及ExtClassLoader,即从adsw包含的Url属性中查找相关的资源,包含从jar文件中及目录中。
url = findResource(name);
}
return url;
}
实验证明
自定义类加载器,加载非classPath下的文件,并且验证不同的类加载器加载到的类是否是同一个类
//先定义一个我们想要加载的类 package com.person; public class Test{ // 该方法输出自己的类加载器 public void sayClassLoader(){ System.out.println("classLoader:" + Test.class.getClassLoader()); } } public class MainTest { public static void main(String[] args) throws Exception { // 我们把刚定义的java文件编译,放在/tmp/com/person下,其中 ‘/com/person’是对应java文件的package String path = "/tmp/"; URL url = new File(path).toURI().toURL(); List<Class<?>> classes = Lists.newArrayList(); for (int i = 0; i < 2; i++) { Thread thread = new Thread(() -> { try { //使用自定义的ClassLoader MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader()); Class<?> aClass = loader.loadClass("com.person.Test"); // 反射构实例以及调用 sayClassLoader 方法 Object instance = aClass.newInstance(); Method method = aClass.getMethod("sayClassLoader"); method.invoke(instance); classes.add(aClass); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } }); thread.start(); } // 等待两个线程执行完毕 Thread.sleep(2000); //判定两个类是否是同一个类 System.out.println(classes.get(0) == classes.get(1)); } } //自定义的类加载器 class MyClassLoader extends URLClassLoader{ public MyClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } }
执行结果
classLoader:com.yqg.collection.MyClassLoader@20c489d2 classLoader:com.yqg.collection.MyClassLoader@5e9b8a25 false
上面案例中,自定义的类加载器继承了URLClassLoader,通过上面的case可以看出,自定义的类加载器加载了自定义的目录上的文件,并且发现两个ClassLoader加载的类,即使是同一个类文件,但是也不是同一个类。
验证类的双新委托机制
public static void main(String[] args) throws Exception { URLClassLoader classLoader = (URLClassLoader) MainTest.class.getClassLoader(); // 这个反射的方法用于增加该ClassLoader的查找范围,现在增加了/tmp目录 Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); String path = "/tmp/"; URL url = new File(path).toURI().toURL(); method.invoke(classLoader, url); //用自定义的类加载器进行加载 MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader()); Class<?> aClass = loader.loadClass("com.person.Test"); // 输出最终类的加载器 System.out.println(aClass.getClassLoader()); }
运行结果如下
sun.misc.Launcher$AppClassLoader@18b4aac2
从上面看出,
com.person.Test
类虽然是通过调用MyClassLoader进行加载但是根据双亲委托机制,最终由其父加载器AppClassLoader完成加载。
结论
- 类通过ClassLoader进行加载,ClassLoader持有其父加载器的引有,常规情况下,当需要加载一个类或者一个资源文件时,委托父加载器进行加载,当父加载器加载不到时,由当前加载器进行加载。但是这并不是必须的,像ogsi,tomcat就都有违反此机制的情况。
- 加载资源文件时,与加载类文件一致,只要加载器是一样的,其可加载的范围就是一样的,配置文件可以是当前工程的配置文件,也可能是第三方jar的配置文件。
- 当有多个资源文件时,与类加载器的搜索顺序相关。所以可以在findResouce的时候,设定优先级及顺序。
- 不同的类加载器加载到的类文件,不是同一个类,这个可用于在一些特定的场景中,比如tomcat的WebappClassLoader用于隔离多个Context,SpringBoot的RestartClassLoader仅用于加载适用于热更新的类,像第三方jar包则由其它的类加载器进行加载。
- 查找类文件与查找检查资源文件基本上是一致的,也取决于类加载器的实现。
其它case分析(SpringBoot RestartClassLoader)
// 同样继承了URLClassLoader
// 该类加载器只关心可热更新的类(自己项目),而第三方jar包会有其它的ClassLoader进行加载,当项目中的类发生变更时,只需替换当前项目中的类即可,不需要加载全部的类
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {
/**
* Create a new {@link RestartClassLoader} instance.
* @param parent the parent classloader
* @param updatedFiles any files that have been updated since the JARs referenced in
* URLs were created.
* @param urls the urls managed by the classloader
* @param logger the logger used for messages
*/
public RestartClassLoader(ClassLoader parent, URL[] urls,
ClassLoaderFileRepository updatedFiles, Log logger) {
super(urls, parent);
Assert.notNull(parent, "Parent must not be null");
Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
Assert.notNull(logger, "Logger must not be null");
this.updatedFiles = updatedFiles;
this.logger = logger;
if (logger.isDebugEnabled()) {
logger.debug("Created RestartClassLoader " + toString());
}
}
// 重写loadClass方法
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
// 先在当前的updatedFiles中找
ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file != null && file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
Class<?> loadedClass = findLoadedClass(name);
// 如是没有找到
if (loadedClass == null) {
try {
// 先自行进行加载
loadedClass = findClass(name);
}
catch (ClassNotFoundException ex) {
//如果没有加载到,再调用父加载器进行加载,从这个地方可以看到,为了满足热更新,违背了双亲委托原则。
loadedClass = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
}