Java 不同类加载器如何使用被其他类加载器加载过的类?

请教各位大佬:

class A 和 class B 是由各自的类加载器加载的,已知A先加载,B后加载。
class C 与 B 在同一jar内,同一个类加载器加载。
当我在B中调用A的一个方法,参数是C的新实例,就报ClassNotFoundException C,请问该如何解决?谢谢!

补充一点,我在 B 中调用的 A 的方法的参数是 Object 类型的,A本身是完全不知道有C的存在的。

经过实际验证,发现 A 的 ClassLoader 是 B 的 ClassLoder 的 父加载器。

阅读 10.4k
5 个回答

再次首先感谢各位的回答与讨论,@临风 的答案与实际情况是最接近的。
我的解决办法如下:

Class B:

static {
        try {
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            method.setAccessible(true);
            URL url = B.class.getProtectionDomain().getCodeSource().getLocation();// B和C 所在的 jar
            method.invoke(A.class.getClassLoader(), url);// 让 A 的类加载器去 BC.jar 里加载
            // 其中,只要在这里(B中)显式地加载了 Class C,仍会报错。
            Class<?> clazz = Class.forName("C", true, A.class.getClassLoader());
            System.out.println(clazz);
            A.method(clazz.getConstructor().newInstance());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

@chenshun 的回答思路可以参考,但结论是错的。
首先在B中调用A的一个方法说明ClassALoaderClassBLoader的父加载器,这样就排除了 @chenshun 回答的第二种可能。而第一种可能确实不会报错。但是也有报错的可能,比如在ClassA中显式的加载ClassC,所以我怀疑题主的问题就出在这。真实原因还得看代码。

Demo验证:

/**
 * 将该类Class文件放在 D:\temp\a\segmentfault\Question1010000017720845目录下
 */
public class ClassA {

    public void method(ClassC c) {
        System.out.println("调用ClassA.method()");
        System.out.println(c);
        //打印的是ClassB和ClassC的加载器 segmentfault.Question1010000017720845.MyClassLoader_2@23fc625e
        System.out.println(c.getClass().getClassLoader());
        //只有在ClassA中显式的加载ClassC才会报 classC not found
        try {
            Class classC = this.getClass().getClassLoader().loadClass("segmentfault.Question1010000017720845.ClassC");
        } catch (ClassNotFoundException e) {
            System.out.println("classC not found");
        }

    }
}

/**
 * 将该类Class文件放在 D:\temp\b\segmentfault\Question1010000017720845目录下
 */
public class ClassB {
    public void method(){
        System.out.println("调用ClassB...");
        ClassA a = new ClassA();
        //ClassC not found 也有可能在这里就报了,比如说ClassC确实没找到
        ClassC c = new ClassC();
        //这里调用了ClassA的方法,必然需要先加载ClassA,
        //说明ClassA的加载器是ClassB加载器的父加载器
        a.method(c);
    }
}

/**
 * 将该类Class文件放在 D:\temp\b\segmentfault\Question1010000017720845目录下
 */
public class ClassC {
    public void method(){
        System.out.println("调用ClassC.method()");
    }
}


/**
 * ClassLoaderDemo.java
 * 注意:ClassA,ClassB,ClassC三者不能出现在classpath下
 */
class MyClassLoader_1 extends ClassLoader {

    public MyClassLoader_1(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) {
        String myPath = "file:/D:/temp/a/" + name.replace(".","/") + ".class";
        System.out.println("classLoader_1 :" + myPath);
        byte[] cLassBytes = null;
        Path path = null;
        try {
            path = Paths.get(new URI(myPath));
            cLassBytes = Files.readAllBytes(path);
        } catch (IOException | URISyntaxException e) {
//            e.printStackTrace();
            System.out.println("找不到资源");
            return null;
        }
        Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
        return clazz;
    }
}

/**
 * 加载ClassB和ClassC
 */
class MyClassLoader_2 extends ClassLoader {
    public MyClassLoader_2(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) {
        String myPath = "file:/D:/temp/b/" + name.replace(".","/") + ".class";
        System.out.println("classLoader_2 :" + myPath);
        byte[] cLassBytes = null;
        Path path = null;
        try {
            path = Paths.get(new URI(myPath));
            cLassBytes = Files.readAllBytes(path);
        } catch (IOException | URISyntaxException e) {
//            e.printStackTrace();
            System.out.println("找不到资源");
            return null;
        }
        Class clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);
        return clazz;
    }
}

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyClassLoader_1 classLoader_1 = new MyClassLoader_1(ClassLoaderDemo.class.getClassLoader());
        MyClassLoader_2 classLoader_2 = new MyClassLoader_2(classLoader_1);
        Class classB = classLoader_2.loadClass("segmentfault.Question1010000017720845.ClassB");
        Object classBInstance = classB.newInstance();
        Method method = classB.getMethod("method");
        method.invoke(classBInstance);
    }

}

既然各自的类加载器所加载的类(a b)可以互相访问,那么两个加载器应该是互通的。所以是不是c原本就不能在jar包外访问?

新手上路,请多包涵

这不是“类在不同类加载器加载的类之间的使用”,而是“实例在不同类加载器加载的类之间的使用”问题,标题不对吧

就以典型的tomcat为例说明:
class A 位于xxa.jar中
class B 和 Class C 位于 xxbc.jar中

不论A先还是B先,肯定都是存在引用导致了classloader去加载了这个类,你现在是b中调用A的一个方法,以下是几种情况

class A 和 class B 是由各自的类加载器加载的,已知A先加载,B后加载。class C 与 B 在同一jar内,同一个类加载器加载。当我在B中调用A的一个方法,参数是C的新实例,就报ClassNotFoundException C

1、A被commonClassloader加载,b/c被webappClassloader加载,很显然,会抛出classNotFoundException,具体原因就是当前类中的数据只能由加载该类的类加载器加载。

2、A被webappClassloader加载,b/c被commonClassloader加载,b调用A的一个方法,参数是C的实例,这时爆出A not found,除非使用上下文加载器实例化A.

想在你的评论下放图的,放不了。就发这里了

clipboard.png

该图是我用Springboot的一个starter放置于tomcat/lib目录下(8.5.35版本),然后用Spring的component-scan扫描,通过@Configuration注册,随后通过我自定义的BeanFactoryPostProcessor修改bean信息,这个可以模拟A和B/C jar包的问题吧。

@Configuration
@ConditionalOnClass(name = {"org.springframework.beans.factory.config.BeanFactoryPostProcessor"})
public class MonitorAutoConfigure {

    @Bean
    public static BeanFactoryPostProcessor my() {
        String lowerCase = MonitorAutoConfigure.class.getClassLoader().toString().toLowerCase();
        System.out.println(lowerCase + "\tchen");
        return new MyBeanFactoryPostProcessor(lowerCase.contains("webappclassloader") && !lowerCase.startsWith("org.springframework.boot"));
    }

}

@临风 代码就只能放这么点了,涉及隐私,不好意思.

@临风 不好意思,没找到你的邮箱

先由webapp加载器将webinf/lib下的Class加载好,并实例化,然后传给由父加载器(common)加载的类的实例方法

这句话其实本身就是错误的,因为还没有等到你传递的过程,类加载的过程就已经失败了,具体原因就不说了,在添张图吧,具体的原因深入了解java虚拟机已经描述了。

环境:tomcat8.5.35(非插件) 启动Spring 5.0.7.RELEASE

clipboard.png

报错图

clipboard.png

实际代码

clipboard.png

asm字节码

{
            mv = cw.visitMethod(ACC_PUBLIC, "googo", "(Lorg/springframework/boot/ApplicationArguments;)Ljava/lang/Object;", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(42, l0);
            mv.visitInsn(ICONST_1);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
            mv.visitInsn(ARETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lboot/raycloud/project/monitor/common/MonitorAutoConfigure;", null, l0, l1, 0);
            mv.visitLocalVariable("applicationArguments", "Lorg/springframework/boot/ApplicationArguments;", null, l0, l1, 1);
            mv.visitMaxs(1, 2);
            mv.visitEnd();
        }

@代码宇宙 喷人就拿点真才实学。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题