在Java中,是否有可能知道一个类是否已经被加载?

50

有没有可能知道一个Java类是否被加载,而不需要试图加载它? Class.forName会尝试加载该类,但我不想产生这种副作用。还有其他方法吗?

(我不想覆盖类加载器。我正在寻找一种相对简单的方法。)

5个回答

44

(感谢Aleksi)此代码:

public class TestLoaded {
     public static void main(String[] args) throws Exception {
          java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
          m.setAccessible(true);
          ClassLoader cl = ClassLoader.getSystemClassLoader();
          Object test1 = m.invoke(cl, "TestLoaded$ClassToTest");
          System.out.println(test1 != null);
          ClassToTest.reportLoaded();
          Object test2 = m.invoke(cl, "TestLoaded$ClassToTest");
          System.out.println(test2 != null);
     }
     static class ClassToTest {
          static {
               System.out.println("Loading " + ClassToTest.class.getName());
          }
          static void reportLoaded() {
               System.out.println("Loaded");
          }
     }
}

生成:

false
Loading TestLoaded$ClassToTest
Loaded
true
请注意,示例类不在包中。需要完整的二进制名称
二进制名称的一个示例是"java.security.KeyStore$Builder$FileBuilder$1"

2
只是一点小提示:在调用方法时,请记得使用规范的名称 package.subpackage.ClassName。由于该类不在包中,因此本答案未展示此要求。 - Aleksi Yrttiaho
3
谢谢您添加这个注释。虽然您确实需要该包,但所需的是“二进制名称”。示例中显示的类的规范名称为“TestLoaded.ClassToTest”,与二进制名称不同...我会编辑答案以澄清/链接。 - Stephen Denne
感谢@spdenne和@Aleksi。我曾经尝试过这个,但是失败了,虽然我不确定原因。你们上面的代码对我有效。顺便说一句,你可能想检查一下McDowell关于仪器化的回复。非常感谢。 - Hosam Aly
应使用Thread.currentThread().getClassLoader()来获取当前类加载器。并非所有的类都是由系统类加载器加载的。 - Martin Kersten
3
如果没有 "permit-illegal-access" 权限,这将会在Java平台模块系统中停止工作。 - Luna
在JDK17中,这是不可能的,因为findLoadedClass是一个final类。 - undefined

33

1
很遗憾,findLoadedClasses是受保护的,这意味着您必须子类化ClassLoader才能访问它。 - staffan
1
通过反射调用这个方法是可能的吗?(抑制安全检查) - Hosam Aly
3
方法 m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Array[]{String.class}); m.setAccessible(true); m.invoke(t, "Stirng");翻译: // 获取 ClassLoader 类的 findLoadedClass(String) 方法 Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class}); // 设置该方法可访问 m.setAccessible(true); // 调用该方法,查找名为 "String" 的类 m.invoke(t, "String"); - Elbek

6

一种实现此目的的方法是使用instrumentation API编写Java代理。这将允许您记录JVM加载类的过程。

public class ClassLoadedAgent implements ClassFileTransformer {

    private static ClassLoadedAgent AGENT = null;

    /** Agent "main" equivalent */
    public static void premain(String agentArguments,
            Instrumentation instrumentation) {
        AGENT = new ClassLoadedAgent();
        for (Class<?> clazz : instrumentation.getAllLoadedClasses()) {
            AGENT.add(clazz);
        }
        instrumentation.addTransformer(AGENT);
    }

    private final Map<ClassLoader, Set<String>> classMap = new WeakHashMap<ClassLoader, Set<String>>();

    private void add(Class<?> clazz) {
        add(clazz.getClassLoader(), clazz.getName());
    }

    private void add(ClassLoader loader, String className) {
        synchronized (classMap) {
            System.out.println("loaded: " + className);
            Set<String> set = classMap.get(loader);
            if (set == null) {
                set = new HashSet<String>();
                classMap.put(loader, set);
            }
            set.add(className);
        }
    }

    private boolean isLoaded(String className, ClassLoader loader) {
        synchronized (classMap) {
            Set<String> set = classMap.get(loader);
            if (set == null) {
                return false;
            }
            return set.contains(className);
        }
    }

    @Override
    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {
        add(loader, className);
        return classfileBuffer;
    }

    public static boolean isClassLoaded(String className, ClassLoader loader) {
        if (AGENT == null) {
            throw new IllegalStateException("Agent not initialized");
        }
        if (loader == null || className == null) {
            throw new IllegalArgumentException();
        }
        while (loader != null) {
            if (AGENT.isLoaded(className, loader)) {
                return true;
            }
            loader = loader.getParent();
        }
        return false;
    }

}

META-INF/MANIFEST.MF:

Manifest-Version: 1.0 
Premain-Class: myinstrument.ClassLoadedAgent

缺点是在启动JVM时必须加载代理程序:
java -javaagent:myagent.jar ....etcetera

谢谢!这是我第一次接触仪器API。那么你建议使用哪种方式?仪器还是通过ClassLoader.findLoadedClass反射? - Hosam Aly
使用最适合您应用程序的那个。我不会期望任何一个在100%的情况下都能正常工作。 - McDowell
你能解释一下清单文件中的 myinstrument 是什么意思吗? - user489041

5
如果您控制着您感兴趣的类的源代码,并且想知道它们是否已加载(我觉得可能性不大,但是您没有在问题中陈述),那么您可以在静态初始化器中注册您的加载。
public class TestLoaded {
    public static boolean loaded = false;
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(loaded);
        ClassToTest.reportLoaded();
        System.out.println(loaded);
    }
    static class ClassToTest {
        static {
            System.out.println("Loading");
            TestLoaded.loaded = true;
        }
        static void reportLoaded() {
            System.out.println("Loaded");
        }
    }
}

输出:

false
Loading
Loaded
true

谢谢。这确实是我自己的课程中使用的好方法。但是我希望找到更通用的方法,以防我无法控制类。 - Hosam Aly
4
你混淆了类加载和初始化。当你执行这样的代码 Object o = ClassToTest.class; 时,类会被加载但不会被初始化。我猜,提问者并不在意这个细节;只是想搞清楚而已。 - maaartinus

1
最近我遇到了一个类似的问题,我怀疑我的用户通过-classpath或类似的方式加载了一些与我后来加载到自己的类加载器中的类冲突的类。
在尝试了这里提到的一些方法后,以下方法对我很有效。我不确定它是否适用于所有情况,可能只适用于从jar文件加载的Java类。
InputStream is = getResourceAsStream(name);

name 是类文件的路径,例如 com/blah/blah/blah/foo.class

当类没有被加载到我的类加载器或系统类加载器中时,getResourceAsStream 返回 null,当类已经被加载时返回非空值。


3
这个检查是否类资源在类路径上(尽管前面缺少了一个 /)。它不会检查该类是否由当前类加载器加载。 - Lukas Eder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接