遵照之前解析反射 中,我们说到类的加载器ClassLoader在对类进行加载的时候,默认会使用双亲委托模式。系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载。
我们现在来对此加以验证,先写一个简单类HelloLoader
public class HelloLoader {
public void print() {
System.out.println("I am in Boot ClassLoader");
}
}
再写一个调用类FindClassOrder
public class FindClassOrder {
public static void main(String[] args) {
HelloLoader loader = new HelloLoader();
loader.print();
}
}
这两个类放在同一个包下面,我这里的包名为com.guanjian.calculate.test
运行结果
I am in Boot ClassLoader
现在将生成的HelloLoader.class文件拷贝出来,放到我事先准备的文件夹下面,目录为/Users/admin/Downloads/com/guanjian/calculate/test,本人的系统为mac的,注意该文件夹不与我们的工程文件夹相同,我们之前到工程文件夹为/Users/admin/Downloads/calculate
此时修改HelloLoader的代码
public class HelloLoader {
public void print() {
System.out.println("I am in App ClassLoader");
}
}
此时运行FindClassOrder,结果如下
I am in App ClassLoader
现在给FindClassOrder添加JVM参数-Xbootclasspath/a:/Users/admin/Downloads/
再次运行,结果发生了改变
I am in Boot ClassLoader
给FindClassOrder添加一条代码,如下
public class FindClassOrder {
public static void main(String[] args) {
HelloLoader loader = new HelloLoader();
loader.print();
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
运行结果
I am in Boot ClassLoader
/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/jre/classes:/Users/admin/Downloads/
从最后一部分/Users/admin/Downloads/我们可以看到,该目录已经加入到了启动的ClassPath中。
由此我们可以看出,HelloLoader的应用版本并没有运行,取而代之的是启动版本。当系统需要加载一个类时,会先从顶层的启动类加载器开始加载,逐层向下,直到找到该类。
现在我们将FindClassOrder修改如下
public class FindClassOrder1 {
public static void main(String[] args) throws Exception {
//获取FindClassOrder1的类加载器,此处为一个应用类加载器
ClassLoader cl = FindClassOrder1.class.getClassLoader();
//将HelloLoader.class字节码转换成字节数组
byte[] bHelloLoader = loadClassBytes("com.guanjian.calculate.test.HelloLoader");
//ClassLoader的defineClass()方法可以将字节数组转换成Class
Method md_defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
md_defineClass.setAccessible(true);
//获取Class HelloLoader
md_defineClass.invoke(cl, bHelloLoader, 0, bHelloLoader.length);
md_defineClass.setAccessible(false);
HelloLoader loader = new HelloLoader();
//打印loader对象的类加载器
System.out.println(loader.getClass().getClassLoader());
loader.print();
}
private static byte[] loadClassBytes(String className) throws ClassNotFoundException {
try {
//获取class文件的完整路径
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new
ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileC.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outC.write(buffer);
buffer.clear();
}
fis.close();
return baos.toByteArray();
} catch (IOException fnfe) {
throw new ClassNotFoundException(className);
}
}
private static String getClassFile(String name) {
StringBuilder sb = new StringBuilder("/Users/admin/Downloads/calculate/target/classes");
name = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + name);
return sb.toString();
}
}
添加JVM参数-Xbootclasspath/a:/Users/admin/Downloads/
运行结果如下
sun.misc.Launcher$AppClassLoader@18b4aac2
I am in App ClassLoader
由此我们可以看出,虽然定义了启动ClassPath,但是系统并没有加载启动版本的HelloLoader。当判断类是否需要加载时,是从底层的应用类加载器开始判断的,如果已经在应用类加载器加载过了,就不会请求上层类加载器了。
我们再将FindClassOrder做出修改
public class FindClassOrder2 {
public static void main(String[] args) throws Exception {
//获取FindClassOrder1的类加载器,此处为一个应用类加载器
ClassLoader cl = FindClassOrder1.class.getClassLoader();
//将HelloLoader.class字节码转换成字节数组
byte[] bHelloLoader = loadClassBytes("com.guanjian.calculate.test.HelloLoader");
//ClassLoader的defineClass()方法可以将字节数组转换成Class
Method md_defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
md_defineClass.setAccessible(true);
//获取Class HelloLoader
md_defineClass.invoke(cl, bHelloLoader, 0, bHelloLoader.length);
md_defineClass.setAccessible(false);
//通过应用类加载器的双亲扩展类加载器进行加载
//如果扩展类加载器加载不了,则会请求扩展类加载器的双亲启动类加载器进行加载
Object loader = cl.getParent().loadClass("com.guanjian.calculate.test.HelloLoader").newInstance();
//打印loader对象的类加载器
System.out.println(loader.getClass().getClassLoader());
//获取loader对象的print()方法
Method m = loader.getClass().getMethod("print", null);
//执行print()方法
m.invoke(loader,null);
}
private static byte[] loadClassBytes(String className) throws ClassNotFoundException {
try {
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new
ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileC.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outC.write(buffer);
buffer.clear();
}
fis.close();
return baos.toByteArray();
} catch (IOException fnfe) {
throw new ClassNotFoundException(className);
}
}
private static String getClassFile(String name) {
StringBuilder sb = new StringBuilder("/Users/admin/Downloads/calculate/target/classes");
name = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + name);
return sb.toString();
}
}
添加JVM参数-Xbootclasspath/a:/Users/admin/Downloads/
运行结果
null
I am in Boot ClassLoader
通过结果,我们可以看出,这里是调用了启动类加载器进行加载的,因为启动类加载器在Java中没有对象,是用C写的,所以为null;而打印的I am in Boot ClassLoader也正好说明加载的是启动ClassPath下的那个HelloLoader.class。虽然在扩展类加载器加载HelloLoader之前,该类已经在应用类加载器中了,但是扩展类加载器并不会向应用类加载器进行确认,而是只在自己的路径中查找,并最终委托给了启动类加载器,而非应用类加载器,从这里可以看到,在判断类是否已经加载时,顶层类加载器不会询问底层类加载器。判断类是否加载时,应用类加载器会顺着双亲路径往上判断,直到启动类加载器。但是启动类加载器不会往下询问,这个委托路线是单向的。
双亲委托模式的弊端
之前说的,检查类是否已经加载的委托过程是单向的。这种方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中,提供了一个接口,该接口需要在应用类得以实现,该接口还绑定了一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
双亲委托模式的补充
在Java平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为Service Provider Interface.即SPI
我们来看一段这样的实现,它是实现javax.xml.parsers中XML文件解析功能的代码
在DocumentBuilderFactory中,这是一个抽象类,加载在启动类加载器中。
public static DocumentBuilderFactory newInstance() {
//根据抽象父类来获取一个子类的对象,抽象父类是通过启动类加载器加载,而子类是通过应用类加载器加载
return FactoryFinder.find(
/* The default property name according to the JAXP spec */
DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"
/* The fallback implementation class name */
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
}
在FactoryFinder中
private static final SecuritySupport ss = new SecuritySupport(); //在获取系统资源的时候用于检查访问控制的安全支持类
static <T> T find(Class<T> type, String fallbackClassName)
throws FactoryConfigurationError
{
//获取类的名称
final String factoryId = type.getName();
dPrint("find factoryId =" + factoryId);
// Use the system property first
try {
//从系统属性中获取资源名称
String systemProp = ss.getSystemProperty(factoryId);
//如果该资源名称不为空,则返回type类的实例化对象
if (systemProp != null) {
dPrint("found system property, value=" + systemProp);
return newInstance(type, systemProp, null, true);
}
}
catch (SecurityException se) {
if (debug) se.printStackTrace();
}
//尝试从$java.home/lib/jaxp.properties文件读取资源
try {
if (firstTime) {
synchronized (cacheProps) {
if (firstTime) {
String configFile = ss.getSystemProperty("java.home") + File.separator +
"lib" + File.separator + "jaxp.properties";
File f = new File(configFile);
firstTime = false;
if (ss.doesFileExist(f)) {
dPrint("Read properties file "+f);
cacheProps.load(ss.getFileInputStream(f));
}
}
}
}
final String factoryClassName = cacheProps.getProperty(factoryId);
//如果可以读取,则返回实例化type类的对象
if (factoryClassName != null) {
dPrint("found in $java.home/jaxp.properties, value=" + factoryClassName);
return newInstance(type, factoryClassName, null, true);
}
}
catch (Exception ex) {
if (debug) ex.printStackTrace();
}
//尝试用SPI方式
T provider = findServiceProvider(type);
if (provider != null) {
return provider;
}
if (fallbackClassName == null) {
throw new FactoryConfigurationError(
"Provider for " + factoryId + " cannot be found");
}
dPrint("loaded from fallback value: " + fallbackClassName);
return newInstance(type, fallbackClassName, null, true);
}
private static <T> T findServiceProvider(final Class<T> type) {
try {
return AccessController.doPrivileged(new PrivilegedAction<T>() {
public T run() {
//通过把type类实例,这里是DocumentBuilderFactory.class以及通过Thread.currentThread().getContextClassLoader()获取的应用类加载器
//放入serviceLoader对象的惰性迭代器lookupIterator属性中
final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
final Iterator<T> iterator = serviceLoader.iterator();
if (iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
}
});
} catch(ServiceConfigurationError e) {
// It is not possible to wrap an error directly in
// FactoryConfigurationError - so we need to wrap the
// ServiceConfigurationError in a RuntimeException.
// The alternative would be to modify the logic in
// FactoryConfigurationError to allow setting a
// Throwable as the cause, but that could cause
// compatibility issues down the road.
final RuntimeException x = new RuntimeException(
"Provider for " + type + " cannot be created", e);
final FactoryConfigurationError error =
new FactoryConfigurationError(x, x.getMessage());
throw error;
}
}
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取一个当前线程的上下文加载器,该上下文加载器就是应用类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private final Class<S> service; //已经被启动类加载器加载过的类,这里指的是DocumentBuilderFactory.class
private final ClassLoader loader; //类加载器,用于判定获取的是哪一种类加载器,如启动类加载器或应用类加载器
private final AccessControlContext acc; //访问控制权限
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果svc类已经被加载了,返回该类实例,否则抛出异常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//传入的类加载器为应用类加载器,如果该应用类加载器不为空,则为应用类加载器,否则为启动类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); //按照实例化顺序的缓存
private LazyIterator lookupIterator; //当前的惰性查找迭代器,ServiceLoader的内部类
public void reload() {
//清空加载缓存
providers.clear();
//将DocumentBuilderFactory.class以及应用类加载器传入惰性查找迭代器中生成对象
lookupIterator = new LazyIterator(service, loader);
}
我们来看一下LazyIterator整个的定义
private static final String PREFIX = "META-INF/services/";
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
//如果nextName不为空,表示有下一个类实例
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//获取META-INF/services/加类全名组成的路径
String fullName = PREFIX + service.getName();
//如果应用类加载器为空,从加载类的搜索路径查找指定名称的资源
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
//如果应用类加载器不为空,启用双亲加载器查找资源
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
/**
* 从资源文件加载子类
*/
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用应用类加载器loader加载子类c
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//如果c不是DocumentBuilderFactory.class的子类,抛出异常
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//将c的对象实例强转成DocumentBuilderFactory类型的对象
S p = service.cast(c.newInstance());
//添加进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<S> iterator() {
return new Iterator<S>() {
//通过缓存获取已加载的迭代器集合
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
/**
* 是否有加载的下一个类实例
*/
public boolean hasNext() {
//如果缓存中有下一个加载的类实例,返回true
if (knownProviders.hasNext())
return true;
//如果缓存中没有,返回惰性查找迭代器中查找结果
return lookupIterator.hasNext();
}
public S next() {
//如果缓存中有下一个加载的类实例,直接从缓存中获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//如果缓存中没有,返回惰性查找迭代器的下一个类实例
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
通过以上分析,我们可以看到,DocumentBuilderFactory.class的子类并不是由启动类加载器进行加载的,当然启动类加载器也加载不了这个子类,而是使用的Thread.currentThread().getContextClassLoader()上下文应用类加载器来进行加载的,该加载器成为了一个相对共享的实例。这样,即使是在启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了。
突破双亲的限制
当我们使用自定义类加载器的时候,当对类进行加载的时候,虽然它自己加载不了,会委托双亲应用类加载器进行加载,但是当我们来看这个类实际的加载器的时候,我们会看到是自定义加载器加载的。(有点玄幻)
首先我们先写一个OrderClassLoader继承于ClassLoader,重写父类的loadClass()和findClass()方法
@AllArgsConstructor
public class OrderClassLoader extends ClassLoader {
private String fileName;
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> re = findClass(name);
if (re == null) {
System.out.println("I can't load the class:" + name + " need help from parent");
return super.loadClass(name, resolve);
}
return re;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class<?> clazz = this.findLoadedClass(className);
if (clazz == null) {
try {
byte[] bytes = loadClassBytes(className);
clazz = defineClass(className,bytes,0,bytes.length);
} catch (Exception e) {
// e.printStackTrace();
return null;
}
}
return clazz;
}
private byte[] loadClassBytes(String className) throws ClassNotFoundException {
try {
//获取class文件的完整路径
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
FileChannel fileC = fis.getChannel();
ByteArrayOutputStream baos = new
ByteArrayOutputStream();
WritableByteChannel outC = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileC.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outC.write(buffer);
buffer.clear();
}
fis.close();
return baos.toByteArray();
} catch (IOException fnfe) {
// throw new ClassNotFoundException(className);
return null;
}
}
private String getClassFile(String name) {
StringBuilder sb = new StringBuilder(fileName);
name = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + name);
return sb.toString();
}
}
调用这个自定义加载器
public class OrderClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
OrderClassLoader myLoader = new OrderClassLoader("/Users/admin/Downloads/");
Class<?> clz = myLoader.loadClass("com.guanjian.calculate.test.HelloLoader");
System.out.println(clz.getClassLoader());
System.out.println("==== Class Loader Tree ====");
ClassLoader cl = clz.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
}
运行结果
I can't load the class:java.lang.Object need help from parent
com.guanjian.calculate.test.OrderClassLoader@85ede7b
==== Class Loader Tree ====
com.guanjian.calculate.test.OrderClassLoader@85ede7b
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@63961c42
从结果可以看出,光靠自定义加载器没办法把字节数组转换成Class实例,而且自定义加载器的defineClass()方法一直找到了HelloLoader的祖先类Object.class。最后使用super.loadClass(name, resolve)应用类加载器来加载成功。但我们打印clz.getClassLoader()得到的却是自定义加载器OrderClassLoader@85ede7b。
热替换
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。对于Java来说,热替换并不是天生就支持的特性,如果一个类已经加载到了系统中,通过修改类文件,并无法让系统再来加载并重新定义这个类。因此,在Java中实现这一功能的一个可行方法就是灵活运用ClassLoader。
我们还是用前面写的OrderClassLoader自定义加载器来说明。我们新写一个调用类
public class OrderClassLoaderTest1 {
public static void main(String[] args) {
while (true) {
try {
OrderClassLoader myLoader = new OrderClassLoader("/Users/admin/Downloads/");
Class<?> clz = myLoader.loadClass("com.guanjian.calculate.test.HelloLoader");
Object demo = clz.newInstance();
Method print = demo.getClass().getMethod("print", new Class[]{});
print.invoke(demo,new Object[]{});
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
运行以后会处于无限循环,每三秒打印一次
I can't load the class:java.lang.Object need help from parent
I can't load the class:java.lang.System need help from parent
I can't load the class:java.io.PrintStream need help from parent
I am in Boot ClassLoader
现在我们将工程中targets/classes里面的文件替换掉/Users/admin/Downloads/com/guanjian/calculate/test中的HelloLoader.class。现在我们在没有停止程序运行的情况下打印结果如下,每三秒打印一次
I can't load the class:java.lang.Object need help from parent
I can't load the class:java.lang.System need help from parent
I can't load the class:java.io.PrintStream need help from parent
I am in App ClassLoader
这里有一点需要注意,当我们实例化对象的时候不能写成如下的方式
public class OrderClassLoaderTest1 {
public static void main(String[] args) {
while (true) {
try {
OrderClassLoader myLoader = new OrderClassLoader("/Users/admin/Downloads/");
Class<?> clz = myLoader.loadClass("com.guanjian.calculate.test.HelloLoader");
HelloLoader demo = (HelloLoader)clz.newInstance();
Method print = demo.getClass().getMethod("print", new Class[]{});
print.invoke(demo,new Object[]{});
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这么写的话,会抛出如下的异常
java.lang.ClassCastException: com.guanjian.calculate.test.HelloLoader cannot be cast to com.guanjian.calculate.test.HelloLoader
at com.guanjian.calculate.test.OrderClassLoaderTest1.main(OrderClassLoaderTest1.java:11)
这是因为myLoader每一次都是一个新的自定义加载器对象,所以每次都是不同的加载器,不同的加载器加载同一个类会被虚拟机认为是不同的类。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有