要在操作数堆栈上复制任意序列的值,没有办法将它们临时存储到新的局部变量中。然后,将所有这些值推送到操作数堆栈,调用您的报告方法,再次推送它们,然后调用原始方法。
一种避免LocalVariablesSorter 开销的简单方法是检查每条使用局部变量的指令并记住第一个空闲索引。这要求源代码已经具有有效的堆栈映射帧,以便在单个访问过程中正确处理后向分支。对于面向 Java 7 或更高版本的代码,无论如何这是强制性的。
由于报告调用的代码没有分支并且之后不需要临时变量,我们甚至不需要昂贵的堆栈映射帧重新计算,因为在分支合并点,只需要原始变量.只有最大堆栈和局部变量需要重新计算。
要调用报告方法,我们可以使用 holder 对象,例如 apangin’s answer,但也可以使用 java.lang.invoke package,它允许我们生成具有精确方法的 varargs 收集器动态描述符。
以下代码将通过单个ldc指令获取MethodHandle到PrintStream.printf(String,Object...),然后是bindingSystem.out作为第一个参数,然后绑定一个适合当前数量的常量String参数(以及非static 方法的接收者),然后调整调用.asVarargsCollector(Object[].class).asType(targetType) 的句柄。 targetType 是方法类型描述符,带有用于非静态方法调用的附加第一个参数类型。这个MethodType 也加载了一条ldc 指令。然后,可以通过调用invokeExact 使用与堆栈上的实际调用相同的参数来使用句柄。
仅对于构造函数调用,接收者对象被省略,因为我们不允许在对象初始化之前使用它。
{?store n + i } for each argumentᵢ
ldc MethodHandle invokeVirtual java/io/PrintStream.printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
getstatic java/lang/System.out Ljava/io/PrintStream;
invokevirtual java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
ldc String containing <method name> and as many %s place holders as needed
invokevirtual java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
ldc class [Ljava/lang/Object;
invokevirtual java/lang/invoke/MethodHandle.asVarargsCollector(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;
ldc MethodType («actual argument types»)Ljava/io/PrintStream;
invokevirtual java/lang/invoke/MethodHandle.asType(Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
{?load n + i } for each argumentᵢ
invokevirtual java/lang/invoke/MethodHandle.invokeExact(«actual argument types»)Ljava/io/PrintStream;
pop // remove the PrintStream return by printf
{?load n + i } for each argumentᵢ
invoke... original method
转换代码及示例
public class LogMethodCalls {
public static void main(String[] args) throws IOException, IllegalAccessException {
MethodHandles.lookup().defineClass(instrument(LogMethodCalls.class
.getResourceAsStream("LogMethodCalls$ToInstrument.class")));
runInstrumented();
}
private static void runInstrumented() {
new ToInstrument().run();
}
static class ToInstrument implements Runnable {
@Override
public void run() {
double min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
for(double i: List.of(4, 2, 9, 6)) {
min = Math.min(min, i);
max = Math.max(max, i);
}
System.out.printf("min %.0f, max %.0f%n", min, max);
}
}
static byte[] instrument(InputStream is) throws IOException {
ClassReader cr = new ClassReader(is);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
cr.accept(new ClassVisitor(Opcodes.ASM7, cw) {
@Override
public MethodVisitor visitMethod(int access, String name,
String descriptor, String signature, String[] exceptions) {
return new LogInjector(
super.visitMethod(access, name, descriptor, signature, exceptions),
access, descriptor);
}
}, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
static class LogInjector extends MethodVisitor {
static final String PS_T = "java/io/PrintStream", PS_S = "L" + PS_T + ";";
static final String PRINTF_DESC="(Ljava/lang/String;[Ljava/lang/Object;)"+PS_S;
static final String MH_T="java/lang/invoke/MethodHandle", MH_S="L" + MH_T + ";";
private int firstUnusedVar;
public LogInjector(MethodVisitor mv, int acc, String desc) {
super(Opcodes.ASM7, mv);
int vars = Type.getArgumentsAndReturnSizes(desc) >> 2;
if((acc & Opcodes.ACC_STATIC) != 0) vars--;
firstUnusedVar = vars;
}
@Override
public void visitFrame(int type,
int numLocal, Object[] local, int numStack, Object[] stack) {
super.visitFrame(type, numLocal, local, numStack, stack);
firstUnusedVar = Math.max(firstUnusedVar, numLocal);
}
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
if(opcode == Opcodes.LSTORE || opcode == Opcodes.DSTORE) var++;
if(var >= firstUnusedVar) firstUnusedVar = var + 1;
}
@Override
public void visitMethodInsn(int opcode,
String owner, String name, String descriptor, boolean isInterface) {
Type[] arg = Type.getArgumentTypes(descriptor);
int[] vars = storeArguments(arg, opcode, name, owner);
String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);
mv.visitLdcInsn(new Handle(Opcodes.H_INVOKEVIRTUAL,
PS_T, "printf", PRINTF_DESC, false));
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", PS_S);
bindTo();
mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
bindTo();
mv.visitLdcInsn(Type.getObjectType("[Ljava/lang/Object;"));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
"asVarargsCollector", "(Ljava/lang/Class;)"+MH_S, false);
mv.visitLdcInsn(Type.getMethodType(reportDesc));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
"asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
pushArguments(arg, vars);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
MH_T, "invokeExact", reportDesc, false);
mv.visitInsn(Opcodes.POP);
pushArguments(arg, vars);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
String getReportDescriptor(
String owner, String descriptor, Type[] arg, int[] vars) {
StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
sb.append('(');
if(arg.length != vars.length) {
if(owner.charAt(0) == '[') sb.append(owner);
else sb.append('L').append(owner).append(';');
}
sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
return sb.append(PS_S).toString();
}
int[] storeArguments(Type[] arg, int opcode, String name, String owner) {
int nArg = arg.length;
boolean withThis = opcode != Opcodes.INVOKESTATIC && !name.equals("<init>");
if(withThis) nArg++;
int[] vars = new int[nArg];
int slot = firstUnusedVar;
for(int varIx = nArg-1, argIx = arg.length-1; argIx >= 0; varIx--,argIx--) {
Type t = arg[argIx];
mv.visitVarInsn(t.getOpcode(Opcodes.ISTORE), vars[varIx] = slot);
slot += t.getSize();
}
if(withThis)
mv.visitVarInsn(Opcodes.ASTORE, vars[0] = slot);
return vars;
}
private void bindTo() {
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
"bindTo", "(Ljava/lang/Object;)"+MH_S, false);
}
private void pushArguments(Type[] arg, int[] vars) {
int vIx = 0;
if(arg.length != vars.length)
mv.visitVarInsn(Opcodes.ALOAD, vars[vIx++]);
for(Type t: arg)
mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), vars[vIx++]);
}
private String messageFormat(int opcode, String owner, String name, Type[] arg){
StringBuilder sb = new StringBuilder();
switch(opcode) {
case Opcodes.INVOKESPECIAL:
if(name.equals("<init>")) {
name = Type.getObjectType(owner).getClassName();
break;
}
// else no break
case Opcodes.INVOKEINTERFACE: // no break
case Opcodes.INVOKEVIRTUAL:
sb.append("[%s].");
break;
case Opcodes.INVOKESTATIC:
sb.append(Type.getObjectType(owner).getClassName()).append('.');
break;
}
sb.append(name);
if(arg.length == 0) sb.append("()%n");
else {
sb.append('(');
for(int i = arg.length; i > 1; i--) sb.append("%s, ");
sb.append("%s)%n");
}
return sb.toString();
}
}
}
示例调用使用 Java 9 并依赖于延迟加载的 JVM,因此它可以在实际使用之前(重新)定义类。它可能会被实际的 Instrumentation 场景所取代,因为它与实际的转换逻辑无关。在我的设置中,示例打印
java.lang.Object()
java.lang.Integer.valueOf(4)
java.lang.Integer.valueOf(2)
java.lang.Integer.valueOf(9)
java.lang.Integer.valueOf(6)
java.util.List.of(4, 2, 9, 6)
[[4, 2, 9, 6]].iterator()
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[4].intValue()
java.lang.Math.min(2.147483647E9, 4.0)
java.lang.Math.max(-2.147483648E9, 4.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[2].intValue()
java.lang.Math.min(4.0, 2.0)
java.lang.Math.max(4.0, 2.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[9].intValue()
java.lang.Math.min(2.0, 9.0)
java.lang.Math.max(4.0, 9.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[6].intValue()
java.lang.Math.min(2.0, 6.0)
java.lang.Math.max(9.0, 6.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
java.lang.Double.valueOf(2.0)
java.lang.Double.valueOf(9.0)
[java.io.PrintStream@6e5e91e4].printf(min %.0f, max %.0f%n, [Ljava.lang.Object;@2cdf8d8a)
min 2, max 9
请注意,您的用例可能更简单。如果您的日志记录方法是static 方法,不需要PrintStream,则不需要绑定它。当它没有返回值时,你也不需要弹出它。当它接受可变参数时,它会更简单,包括格式字符串或方法名称。然后,我们可以像普通参数一样传递字符串,而不是绑定它,并且由于方法句柄现在没有修改,当目标方法是 varargs 时,它已经是一个 varargs 收集器 方法:
static void yourLog(Object... arg) {
String name = (String) arg[0]; // or format string
arg = Arrays.copyOfRange(arg, 1, arg.length);
…
}
@Override
public void visitMethodInsn(int opcode,
String owner, String name, String descriptor, boolean isInterface) {
Type[] arg = Type.getArgumentTypes(descriptor);
int[] vars = storeArguments(arg, opcode, name, owner);
String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);
mv.visitLdcInsn(new Handle(Opcodes.H_INVOKESTATIC,
YOUR_TARGET_TYPE, "yourLog", "([Ljava/lang/Object;)V", false));
mv.visitLdcInsn(Type.getMethodType(reportDesc));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
"asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
pushArguments(arg, vars);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T, "invokeExact", reportDesc, false);
pushArguments(arg, vars);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
String getReportDescriptor(String owner, String descriptor, Type[] arg, int[] vars) {
StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
sb.append("(Ljava/lang/String;");
if(arg.length != vars.length) {
if(owner.charAt(0) == '[') sb.append(owner);
else sb.append('L').append(owner).append(';');
}
sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
return sb.append('V').toString();
}