所以我有一个类加载器 (MyClassLoader),它在内存中维护一组“特殊”类。这些特殊类被动态编译并存储在 MyClassLoader 内部的字节数组中。当 MyClassLoader 被请求一个类时,它首先检查它的 specialClasses
是否在委托(delegate)给系统类加载器之前,字典包含它。它看起来像这样:
class MyClassLoader extends ClassLoader {
Map<String, byte[]> specialClasses;
public MyClassLoader(Map<String, byte[]> sb) {
this.specialClasses = sb;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (specialClasses.containsKey(name)) return findClass(name);
else return super.loadClass(name);
}
@Override
public Class findClass(String name) {
byte[] b = specialClasses.get(name);
return defineClass(name, b, 0, b.length);
}
}
如果我想对 specialClasses
执行转换(例如检测) , 我可以通过修改 byte[]
来做到这一点在我打电话之前 defineClass()
在上面。
我还想转换系统类加载器提供的类,但系统类加载器似乎没有提供任何访问原始 byte[]
的方法。它提供的类,并给了我 Class
直接对象。
我可以使用 -javaagent
检测加载到 JVM 中的所有类,但这会增加我不想检测的类的开销;我真的只希望对 MyClassLoader 加载的类进行检测。
- 有没有办法检索原始
byte[]
父类加载器提供的类,所以我可以在定义自己的副本之前检测它们? - 或者,是否有任何方法可以模拟系统类加载器的功能,就其获取
byte[]
的位置而言。来自,以便 MyClassLoader 可以检测和定义它自己的所有系统类(对象、字符串等)的副本?
编辑:
所以我尝试了另一种方法:
- 使用
-javaagent
, 捕获byte[]
加载的每个类并将其存储在哈希表中,以类名作为键。 - MyClassLoader 不是将系统类委托(delegate)给它的父类加载器,而是使用类名从这个哈希表加载它们的字节码并定义它
理论上,这会让 MyClassLoader 定义它自己的系统类版本,并带有检测。但是,它失败了
java.lang.SecurityException: Prohibited package name: java.lang
显然 JVM 不喜欢我定义 java.lang
我自己上课,即使它(理论上)应该来自同一个 byte[]
引导加载的类应该来自的来源。继续寻找解决方案。
编辑2:
我为这个问题找到了一个(非常粗略的)解决方案,但如果有人比我更了解 Java 类加载/检测的复杂性,可以想出一些不那么粗略的方法,那就太棒了。
最佳答案
所以我找到了解决这个问题的方法。这不是一个非常优雅的解决方案,它会在代码审查时引起很多愤怒的电子邮件,但它似乎有效。基本要点是:
Java代理
使用 java.lang.instrumentation
和一个 -javaagent
存储 Instrumentation
稍后使用的对象
class JavaAgent {
private JavaAgent() {}
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Agent Premain Start");
Transformer.instrumentation = inst;
inst.addTransformer(new Transformer(), inst.isRetransformClassesSupported());
}
}
类文件转换器
添加 Transformer
到 Instrumentation
仅适用于标记的类。有点像
public class Transformer implements ClassFileTransformer {
public static Set<Class<?>> transformMe = new Set<>()
public static Instrumentation instrumentation = null; // set during premain()
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] origBytes) {
if (transformMe.contains(classBeingRedefined)) {
return instrument(origBytes, loader);
} else {
return null;
}
}
public byte[] instrument(byte[] origBytes) {
// magic happens here
}
}
类加载器
在类加载器中,显式标记每个加载的类(甚至是加载委托(delegate)给父类的类),方法是将其放置在 transformMe
中。在询问 Instrumentation
之前改造它
public class MyClassLoader extends ClassLoader{
public Class<?> instrument(Class<?> in){
try{
Transformer.transformMe.add(in);
Transformer.instrumentation.retransformClasses(in);
Transformer.transformMe.remove(in);
return in;
}catch(Exception e){ return null; }
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return instrument(super.loadClass(name));
}
}
...瞧! MyClassLoader
加载的每个类被 instrument()
转化方法,包括所有系统类,如 java.lang.Object
和 friend ,而所有由默认 ClassLoader 加载的类都保持不变。
我已经尝试使用内存分析 instrument()
方法,它插入回调 Hook 以跟踪检测字节码中的内存分配,并可以确认 MyClassLoad
类在它们的方法运行时触发回调(甚至系统类),而“普通”类则没有。
胜利!
当然,这是糟糕的代码。无处不在的共享可变状态、非本地副作用、全局变量,以及您可能想象到的一切。可能也不是线程安全的。但它表明这样的事情是可能的,您可以确实有选择地检测类的字节码,甚至是系统类,作为自定义 ClassLoader 操作的一部分,同时让程序的“其余部分”保持不变。
开放问题
如果其他人有任何想法如何让这段代码不那么糟糕,我会很高兴听到。我想不出一个办法:
- 制作
Instrumentation
仅 通过retransformClasses()
按需提供的乐器类(class)并且不以其他方式加载仪器类 - 在每个
Class<?>
中存储一些元数据允许Transformer
的对象在没有全局可变哈希表查找的情况下判断它是否应该被转换。 - 在不使用
Instrumentation.retransformClass()
的情况下转换系统类方法。如前所述,任何动态defineClass
的尝试一个byte[]
进入java.lang.*
由于 ClassLoader.java 中的硬编码检查,类失败。
如果有人能找到解决这些问题中的任何一个的方法,它就会变得不那么粗略。无论如何,我猜测能够检测(例如用于分析)某些子系统(即您感兴趣的子系统),同时保持 JVM 的其余部分不变(没有检测开销)将对其他人有用我,就在这里。
关于java - Java Classloader 能否重写系统类(仅它们的副本)的字节码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13032918/