自定义Classloader致使ClassCastException

背景

java.lang.ClassCastException: cn.com.nightfield.Plugin cannot be cast to cn.com.nightfield.Plugin

相同的class,居然不能cast?这是什么鬼?java

问题描述

自定义类加载器(Classloader)是很常见的,它可让咱们从自定义的文件系统目录网络甚至是数据库的各类文件类型(jar, war, zip等)中加载class文件。 咱们项目中使用了一个开源的类管理工具PF4J,来加载指定目录下的class文件。但奇怪的是,当咱们把class加载进来以后,将它强转为目标类型,却报了java.lang.ClassCastException,二者明明是同一个classgit

问题分析

先说明,错误是跟自定义类加载器有关。上一个小demo来模拟一下上述错误:github

package cn.com.nightfield.jvm.classloader;
// 在class path下定义一个类
public class Plugin {}
package cn.com.nightfield.jvm.classloader;

import java.net.URL;
import java.net.URLClassLoader;
// 自定义一个类加载器
public class CustomizedClassLoader extends URLClassLoader {

    public CustomizedClassLoader(URL[] urls) {
        super(urls);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 若是不是自定义目录下的class,统一委托给AppClassloader去加载
            if (!name.startsWith("cn.com.nightfield.jvm.classloader")) {
                return super.loadClass(name, resolve);
            }
            // 若是是自定义目录下的class,直接加载,此处违反了双亲委派模型
            else {
                Class<?> c = findClass(name);
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }
}
package cn.com.nightfield.jvm.classloader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException {
        // 指定类加载器的加载路径
        URL url = new File("/Users/zhochi/demo/target/classes").toURI().toURL();
        ClassLoader customizedClassLoader = new CustomizedClassLoader(new URL[]{url});
        // 用自定义类加载器加载Plugin class
        Class clz = customizedClassLoader.loadClass("cn.com.nightfield.jvm.classloader.Plugin");
        System.out.println(clz.getClassLoader());
        Object pluginInstance = clz.newInstance();
        // pluginInstance instanceof Plugin”输出false
        System.out.println("pluginInstance instanceof Plugin: " + (pluginInstance instanceof Plugin));
        // 报java.lang.ClassCastException错误
        Plugin plugin = (Plugin) clz.newInstance();
    }
}

控制台输出以下:数据库

cn.com.nightfield.jvm.classloader.CustomizedClassLoader@60e53b93
pluginInstance instanceof Plugin: false
Exception in thread "main" java.lang.ClassCastException: cn.com.nightfield.jvm.classloader.Plugin cannot be cast to cn.com.nightfield.jvm.classloader.Plugin
	at cn.com.nightfield.jvm.classloader.ClassLoaderTest.main(ClassLoaderTest.java:19)

要想知道错误的根源,须要了解对象能够被cast的前提:对象必须是目标类的实例。从上述输出也能够看到,instance instanceof Plugin的结果是false,为何呢?由于对于任意一个类,都须要由它的类加载器和这个类自己,共同确立其在JVM中的惟一性,也就是说,JVM中两个类是否相等,首先要看它们是否是由同一个类加载器加载的。若是不是的话,即便这两个类来自于同一个class文件,它们也不相等。安全

上例中,Plugin类处于class path下,默认是由AppClassloader来加载的;可是pluginInstance倒是由CustomizedClassLoader加载出来的class的实例。JVM尝试将CustomizedClassLoader.Plugin转成AppClassloader.Plugin,必然会报错。网络

问题解决

其实究其缘由,是咱们在自定义类加载器CustomizedClassLoader中,违反了双亲委派模型。 咱们都知道,Java中有三大类加载器:BootstrapClassLoaderExtClassLoaderAppClassLoader,它们在组合上构成父子关系,前者是后者的"父亲",而且有各自的“领地”:BootstrapClassLoader负责加载 Java核心类库如JRE中的rt.jarresource.jarExtClassLoader负责加载{java.home}/lib/extjava.ext.dirs系统目录下的classAppClassLoader则是加载class path路径下,也就是咱们本身写的class文件。 所谓双亲委派模型,指的是当Classloader收到一个加载class请求的时候,首先会委托给其父亲去加载,若是父亲加载不成功,本身才会尝试去加载。双亲委派的机制是JVM中类的安全性的一大保障:就算有人恶意自定义了一个String.class,最终由类加载器加载到的依然是rt.jar中的String。如下是loadClass的部分源码:jvm

public abstract class ClassLoader {
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 若是类已经被加载过了,直接返回
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    // 2. 委托父类去加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 这种状况指的就是委托BootstrapClassLoader去加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 3. 尝试本身加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

不过,双亲委派模型并非一个强制的约束,而是Java推荐的模式,因此咱们在自定义类加载器的时候,推荐重写findClass()方法,而不是loadClass()方法。工具

回到最开始的问题,分析了一下PF4J的源码,能够猜到,它也定义了本身的类加载器PluginClassLoader,且它重写的loadClass()方法的默认实现,为了防止class的版本问题,违反了双亲委派模型url

总结

Java中的类加载器,至关因而其加载的class的命名空间,两个类相等,首先要保证它们是由同一个类加载器加载的。 在实现自定义类加载器的时候,除非你对类加载机制有着深入的认知且知道本身在作什么,不然不要违反双亲委派模型.net

相关文章
相关标签/搜索