我有以下场景,我有一个类加载器和一个加载的类,现在我需要该类的字节码。这是我到目前为止所尝试的:
Field f = ClassLoader.class.getDeclaredField("classes");
f.setAccessible(true);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Vector<Class> classes = (Vector<Class>) f.get(classLoader);
for(Class loadedClass : classes)
{
String className = loadedClass.getName();
String classFileResourcePath = "/" + className.replace(".", "/") + ".class";
InputStream inputStream = classLoader.getResourceAsStream(classFileResourcePath);
System.out.println(">>>> " + className + " => " + classFileResourcePath + " => " + inputStream);
}
此代码为每个类文件打印null
。但是当我将其更改为classLoader.getClass().getResourceAsStream(classFileResourcePath)
时,如果在IDE中的独立Main类中运行它会起作用,但是当我到达需要它的实际上下文时,这也会返回null,大概是因为有&#34 ;特殊&#34;罐子里发生的事情和幕后的课程。如果不能讨论这些细节,只需说出我所拥有的是一个类和加载它的类加载器,现在我需要字节代码。我该怎么做呢?如果在Java层中无法做到这一点,我可能会获取原始的Jar本身并将其作为zip文件读取,但这是最后的选择。
答案 0 :(得分:3)
您的代码示例实际上存在几个问题:
首先,您访问&#34;类&#34; java.lang.ClassLoader
类的字段,用于确定已加载的类。这是一个私有字段,如果您让代码在使用专用类加载器的环境中运行(java.lang.ClassLoader的子类),您或多或少都不知道该字段中包含的内容。
使用ClassLoader.getResourceAsStream,
在路径前加上&#34; /&#34;,这是不正确的。 ClassLoader.getResourceAsStream
期望绝对路径,路径以第一个分段的名称开头,例如使用ClassLoader.getResourceAsStream("java/lang/ClassLoader.class")
代替ClassLoader.getResourceAsStream("/java/lang/ClassLoader.class")
。
使用Class.getResourceAsStream
,您可以提供以&#34; /&#34;开头的绝对路径,或提供相对于相关课程的路径,而不是以&#34; /&#34开头;。例如。 ClassLoader.class.getResourceAsStream("ClassLoader.class")
或ClassLoader.class.getResourceAsStream("/java/lang/ClassLoader.class")
通常都会授予您访问课程的权限。字节码。
但是,这两种方法都要求使用Java运行时环境的标准命名约定将类文件作为类路径上的资源使用。不要求Java运行时环境必须以这种方式运行。 Java类可以动态生成,使类加载器知道它们,但不受持久字节代码的支持。专有类加载器也不需要在类名和资源路径之间使用与标准类加载器相同的映射。
Java类加载器也不提供访问类的公共API&#39;字节码。如果您在一个&#34;本机代码部分中分离VM&#34;和#34; Java代码部分&#34;,很明显,VM通常不需要引用来自&#34; Java代码部分&#34;的原始字节代码。
依赖于标准类加载器使用的约定,您可以使用您的方法,它将主要在独立应用程序中工作。但是,正如您自己发现的那样,如果您在不同的环境中运行代码,则可能会失败,例如:部署到应用程序服务器时或使用OSGi等打包框架时。
答案 1 :(得分:1)
首选方法是Class.getResource
或Class.getResourceAsStream
。这将自动使用正确的ClassLoader
(如果ClassLoader.getSystemResource()
为ClassLoader
,则使用null
)。它还将解析类package
内的资源,除非您使用'/'
添加资源名称。
因此,对于不代表嵌套类的Class
对象,您可以使用theClass.getResourceAsStream(theClass.getSimpleName()+".class")
如果您需要正确处理内部类,您将通过Class.getName()
获取合格的名称,并使用'/'+name.replace('.', '/')+".class")
或name.substring(name.lastIndexOf('.')+1)+".class")
如果失败,ClassLoader
不支持获取类字节码,或者该类已经动态生成并添加,而不是以ClassLoader
可以使用的方式记录字节代码。
如果您希望能够检索字节代码,即使是这样的类,也需要一个支持Instrumentation的JVM。 ClassFileTransformer
将获得字节代码作为输入,因此可以将其存储在某处而不实际转换它,如果这是意图。
另请参阅Instrumentation.getInitiatedClasses(java.lang.ClassLoader)
了解获取特定ClassLoader
类的可靠方法。
但是,您应该知道这不一定是传递给defineClass
的字节代码,因为JVM可能会剥离与执行无关的信息,并且还会以优化的形式存储数据,从而创建等效但不完全相同的数据在将其转换回来以将其传递给变换器时匹配字节代码。
另一个警告是,如果在JVM中注册了其他变换器,例如如果您同时使用仪表分析仪,则无法精确控制变压器的顺序。即第一个变换器将看到等效于存储在磁盘上的代码的字节代码,而链的最后一个将看到与JVM最终执行的代码等效的代码,而中间变换器会看到可能与它们都不匹配的东西。
请注意,即使使用getResourceAsStream
,字节代码也不需要匹配,例如如果自调用defineClass
以来已修改了底层资源。原则上,ClassLoader
不会以一致的方式强制实施loadClass
/ findClass
和getResourceAsStream
。
答案 2 :(得分:1)
如@jarnbjo所述,这不是一种普遍有效的方法。我一直在寻找一种通用的方法。我发现了两种有前景的方法,只有一种实际工作方法:
一个。 Instrumentation API。这有效。我决定不使用它因为在尝试修改某些类时遇到困难。检测代理程序在同一个JVM中运行,当它尝试检测它所依赖的类时,可能会发生一些非常奇怪的异常。 (我学到了一些新的异常类型.Ehm,java.lang.ClassCircularityError ...)
但如果您承认在JVM启动时添加了一个检测代理(通过JVM args),那么这对您来说可能没问题。你似乎只需要读取字节码,所以你永远不应该遇到这样的麻烦
湾JDI,Java调试接口。这看起来很有希望。我已经开始编写一个从JDI API重构字节码的脚本。几乎所有我需要的东西,除了异常表。所以它不是很有用。如果您拥有所有指令,但没有ExceptionTable属性,则无法进行任何流分析,反编译源等。一些异常处理程序看起来像没有ExceptionTable的死代码。你可以只看到字节码中的当前位置,而没有一些重要的信息。
答案 3 :(得分:-1)
您应该查看ASM library。
使用库,您可以像这样访问字节码:
ClassReader cr = new ClassReader("java.lang.Runnable");
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
然后,您可以使用ClassNode
的getter访问信息对象。
也可以使用访客进行基于事件的分析。
请注意,您可以使用输入流或字节数组来实例化ClassReader
,而不是类名。