Java 动态字节码检测-问题
我有一个无法解决的问题。假设我们有以下两个类和一个继承关系:Java 动态字节码检测-问题,java,bytecode,instrumentation,agent,Java,Bytecode,Instrumentation,Agent,我有一个无法解决的问题。假设我们有以下两个类和一个继承关系: public class A { } public class B extends A { public void foo() {} } 我希望插入其他代码,使其如下所示: public class A { public void print() { } } public class B extends A { public void foo() { print(); } } 为了实现这个目标,我基于
public class A {
}
public class B extends A {
public void foo() {}
}
我希望插入其他代码,使其如下所示:
public class A {
public void print() { }
}
public class B extends A {
public void foo() { print(); }
}
为了实现这个目标,我基于java.lang.instrument
包,使用一个带有我自己的类文件转换器的代理来实现。这种机制也称为动态字节码检测
到目前为止是小菜一碟。
现在,我的测试方法执行以下操作:
代码:
由于插装包中的以下限制,这不起作用:当调用new B()
时,插装从类B开始,并在加载操纵类时导致编译错误,因为超级类a还没有print()方法!问题是,我是否以及如何在类B之前触发类A的插装。我的classfiletransformer的transform()方法应该用类A显式调用!于是我开始阅读并偶然发现:
java.lang.instrument.ClassFileTransformer.transform()
的javadoc说明:
变压器将被调用
每一个新的类定义和每一个
类别重新定义。请求
新的类定义是用
ClassLoader.defineClass。请求
对于类,使用
Instrumentation.class或其
本地等价物
transform方法随类加载器实例一起出现,所以我想,为什么不在B的指令插入开始时调用loadClass
方法(loadClass
调用defineClass
)。
我希望结果会调用instrument方法,但遗憾的是,事实并非如此。相反,类A
是在没有插入的情况下加载的。(代理不拦截加载进程,尽管它应该拦截)
有什么想法,如何解决这个问题?您是否看到了一个原因,即操纵某些字节码的代理不可能手动加载另一个类,然后希望该类也通过该代理/任何代理发送
请注意,以下代码工作正常,因为在操作B之前加载并检测了A
A a = new A();
B b = new B();
b.foo();
非常感谢 在Sun 1.6.0_15和1.5.0_17 JRE(我使用的)上,在A之前转换B时,我没有发现任何问题。我将通过外部运行转换代码并检查结果类(例如,使用javap)来再次检查转换代码。我还将检查您的类路径配置,以确保由于某种原因没有在您的代理之前加载(可能会使用签入您的premain)
编辑: 如果在代理中加载类
A
,如下所示:
Class.forName("A");
…然后引发异常:
Exception in thread "main" java.lang.NoSuchMethodError: B.print()V
这是有意义的-A
成为代理的依赖项,代理插入自己的代码是没有意义的。您将得到一个无限循环,导致堆栈溢出。因此,ClassFileTransformer
不会处理A
为了完整性,这里是我的测试代码,它可以毫无问题地工作。如前所述,它取决于ASM库 代理人:
public class ClassModifierAgent implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("transform: " + className);
if ("A".equals(className)) {
return new AModifier().modify(classfileBuffer);
}
if ("B".equals(className)) {
return new BModifier().modify(classfileBuffer);
}
return classfileBuffer;
}
/** Agent "main" equivalent */
public static void premain(String agentArguments,
Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassModifierAgent());
}
}
B
的方法替换程序:
public class BModifier extends Modifier {
@Override
protected ClassVisitor createVisitor(ClassVisitor cv) {
return new BVisitor(cv);
}
class BVisitor extends ClassAdapter {
public BVisitor(ClassVisitor cv) { super(cv); }
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if ("foo".equals(name)) {
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "()V",
null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "B", "print", "()V");
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
return new EmptyVisitor();
} else {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
}
}
通用基码:
public abstract class Modifier {
protected abstract ClassVisitor createVisitor(ClassVisitor cv);
public byte[] modify(byte[] data) {
ClassReader reader = new ClassReader(data);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = writer;
visitor = new CheckClassAdapter(visitor);
visitor = createVisitor(visitor);
reader.accept(visitor, 0);
return writer.toByteArray();
}
}
对于一些可见的结果,我添加了一个System.out.println('X')代码>到A.print()
在该代码上运行时:
public class MainInstrumented {
public static void main(String[] args) {
new B().foo();
}
}
…它产生以下输出:
transform: MainInstrumented
transform: B
transform: A
X
谢谢你的回答。让我们从不同的角度来看待这个问题。假设两个类A和B都是空的。在转换方法的开头和结尾添加一个日志,这样我们就可以看到代理加载了哪个类以及在什么时间点加载了哪个类。execute:new B()结果应该是:类B由代理加载,之后是类A。当B通过时,您现在可以尝试在您的代理中使用Classloader.loadClass()方法手动加载类A吗?结果是:B是通过代理加载的,A不是!对吗?首先,非常感谢你的回答和努力。我很感激。你是对的!我使用Javassist进行任何转换。Javassist重新编译更改。这将导致上述编译失败。ASM直接处理字节码,不需要重新编译。关于在代理中加载类:我不理解你的答案。我使用eclipse,如果我添加代理:>//如果类名是B>class.forName(“A”);我在调试器中跟踪执行过程,没有抛出异常,代理也没有进入增益,你是对的。你提到的例外情况是正确的。让我们让事情变得更简单:这两个类都没有方法,也根本不打算被检测。代理应该做的唯一一件事是:如果class B通过代理,class.forName(“A”);应调用。这应该触发类加载的正确顺序(首先是A,然后是B)。试试这个例子。您将看到只有B通过代理!因此,问题就出现了,当作为代理的一部分调用时,为什么A不传递代理。
public class MainInstrumented {
public static void main(String[] args) {
new B().foo();
}
}
transform: MainInstrumented
transform: B
transform: A
X