Li *_*oyi 14 java bytecode metaprogramming class classloader
所以我有一个类加载器(MyClassLoader),它在内存中维护一组"特殊"类.这些特殊类是动态编译的,并存储在MyClassLoader内的字节数组中.当MyClassLoader被要求提供类时,它首先检查它的specialClasses字典是否包含它,然后再委托给System类加载器.它看起来像这样:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
如果我想在其上执行转换(例如检测)specialClasses,我可以简单地通过修改byte[]之前调用defineClass()它来实现.
我还想转换由System类加载器提供的类,但是System类加载器似乎没有提供任何方式来访问byte[]它提供的类的原始类,并Class直接给我提供了对象.
我可以使用一个-javaagent仪器加载到JVM中的所有类,但这会增加我不想检测的类的开销; 我只是想要MyClassLoader加载的类进行检测.
byte[]父类加载器提供的类的原始,所以我可以在定义自己的副本之前检测它们?byte[],以便MyClassLoader可以检测和定义自己的所有System类(Object,String等)的副本?编辑:
所以我尝试了另一种方法:
-javaagent,捕获byte[]已加载的每个类,并将其存储在哈希表中,并以类的名称键入.理论上,这将让MyClassLoader使用检测来定义自己的系统类版本.然而,它失败了
java.lang.SecurityException: Prohibited package name: java.lang
Run Code Online (Sandbox Code Playgroud)
很明显,JVM不喜欢我java.lang自己定义类,即使它(理论上)应该来自byte[]引导加载类应该来自同一个源.寻求解决方案仍在继续.
EDIT2:
我为这个问题找到了一个(非常粗略)的解决方案,但是如果有人比我更了解Java类加载/检测的复杂性,那么可能会提出一些不那么粗略的东西,那就太棒了.
Li *_*oyi 11
所以我找到了解决方案.这不是一个非常优雅的解决方案,它会在代码审查时产生很多愤怒的电子邮件,但似乎有效.基本点是:
使用a java.lang.instrumentation和a -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());
}
}
Run Code Online (Sandbox Code Playgroud)
添加Transformer到Instrumentation仅适用于标记类的a.就像是
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
}
}
Run Code Online (Sandbox Code Playgroud)
在类加载器中,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));
}
}
Run Code Online (Sandbox Code Playgroud)
......瞧!加载的每个类都被方法MyClassLoader转换instrument(),包括所有系统类java.lang.Object和朋友,而默认的ClassLoader加载的所有类都保持不变.
我已经尝试使用内存分析instrument()方法,它插入回调挂钩来跟踪检测字节码中的内存分配,并且可以确认MyClassLoad类在其方法运行时触发回调(甚至系统类),而"正常"类是不.
胜利!
当然,这是可怕的代码.到处共享可变状态,非本地副作用,全局,你可以想象的一切.可能也不是线程安全的.但它表明这样的事情是可能的,你可以选择性地检测类的字节码,甚至系统类,作为自定义ClassLoader操作的一部分,同时保持程序的"休息"不变.
如果其他人有任何想法如何使这个代码不那么糟糕,我会很高兴听到它.我无法想出办法:
Instrumentation 唯一的乐器类,retransformClasses()而不是加载乐器类Class<?>对象中存储一些元数据,Transformer以便判断是否应该转换它.Instrumentation.retransformClass()方法的情况下转换系统类.如所提到的,任何企图动态defineClass一个byte[]成java.lang.*类失败,因为在硬编码ClassLoader.java检查.如果任何人都可以找到解决这些问题的方法,那么这将使这一点不那么粗略.无论如何,我猜测能够检测(例如,用于分析)某个子系统(即您感兴趣的子系统),同时保持JVM的其余部分不受影响(没有任何检测开销)将对其他人有用.我,所以在这里.
首先是没有ClassFileTransformer的解释:
Oracle JRE/JDK的许可证包括你无法更改java.*包,以及你在尝试更改java.lang中的内容时所显示的内容,它们包含了一个测试并抛出安全性异常,如果你试试.
话虽如此,您可以通过编译替代方法并使用JRE -Xbootclasspath/p CLI选项引用它来更改系统类的行为.
在查看通过该方法可以实现的功能后,我希望您必须更多地工作并编译OpenJDK的自定义版本.我期待这一点,因为Bootstrap类加载器(从我读过的)是本机实现.
有关我最喜欢的类加载器概述,请参见http://onjava.com/pub/a/onjava/2005/01/26/classloading.html.
现在使用ClassFileTransformer:
如您所示,您可以更新方法程序(以及预加载类的某些特定其他方面).回答您提出的问题:
按需检测:这里重要的是每个加载的类都有一个与之关联的唯一Class实例; 因此,如果你想要定位一个特定的加载类,你必须注意它是什么实例,这可以通过各种方式找到,包括与每个类名相关联的成员'class',如Object.class.
它是线程安全的吗:不,两个线程可以同时更改集合,你可以通过多种方式解决这个问题; 我建议使用Set的并发版本.
Globals等:我认为全局变量是必要的(我认为你的实现可以做得更好一些),但是很可能没有问题,你将学会如何更好地为Java编写代码(我编写了大约12年了,你不会相信使用这种语言的一些微妙的事情.
类实例中的元数据:在我使用Java的所有时间里,附加元数据并不是很自然,并且可能是有充分理由的; 为特定目的保留地图很好,并且记住它只是指向实例的指针和元数据之间的映射,所以它实际上不是内存耗尽.