Чтобы дублировать произвольную последовательность значений в стеке операндов, невозможно временно сохранить их в новых локальных переменных. Затем pu sh все эти значения в стеке операндов, вызовите свой метод отчетности, pu sh их снова и вызовите исходный метод.
Простой подход, позволяющий избежать * затрат 1004 *, это проверка каждой инструкции, использующей локальную переменную, и запоминание первого свободного индекса. Для этого требуется, чтобы в исходном коде уже были допустимые фреймы карты стека, чтобы правильно обрабатывать обратные ветки за один проход посещения. Для кода, ориентированного на Java 7 или новее, это в любом случае обязательно.
Поскольку код для сообщения о вызове не содержит ветвей и впоследствии не требует временных переменных, нам даже не нужен дорогостоящий пересчет кадры карты стека, как и в точках слияния ветвей, нужны только исходные переменные. В пересчете требуется только максимальный стек и локальные переменные.
Чтобы вызвать метод отчета, мы можем использовать объект-держатель, как в ответ апангина , но в качестве альтернативы мы можем использовать java.lang.invoke
package , который позволяет нам на лету генерировать varargs сборщики с точными дескрипторами методов.
Следующий код получит MethodHandle
в PrintStream.printf(String,Object...)
с помощью одной инструкции ldc
, за которой следует привязка System.out
в качестве первого аргумента, за которой следует привязка константы String
, подходящей для текущего числа аргументов (и получатель для не - static
методы), затем адаптируйте вызов дескриптора .asVarargsCollector(Object[].class)
.asType(targetType)
. targetType
- это дескриптор типа метода с дополнительным первым типом параметра для нестатических c вызовов метода. Этот 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 с отложенной загрузкой , поэтому он может (пере) определить класс перед его фактическим использованием. Он может быть заменен реальным сценарием инструментария, поскольку он не имеет отношения к фактическому logi c преобразования. В моей настройке пример печатает
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();
}