转载

ASM 匿名内部类 & Lambda 表达式的处理

简单总结使用 ASM 时遇到匿名内部类时,如何对匿名内部类(一般来说接口)的方法实现插桩。

痛点

通过之前的 当 Java 字节码遇到 ASM 一文,对如何使用 ASM 已经有了初步的了解。这里再来看一种比较特殊的情况,当遇到匿名内部类时,如何确定 hack 结点。

接口作为匿名内部类实现

接口 Callback
package com.asm.internal;

import com.asm.Music;

public interface Callback {

    void noParams();

    void withParams(int a, Music music);
}

WithAnonymousClass.java

public class WithAnonymousClass {

    public String name = "with";

    public void justCallback(World world) {

        world.setCallback(new Callback() {
            @Override
            public void noParams() {
                System.out.println("红桃四");
            }

            @Override
            public void withParams(int a, Music music) {
                System.out.println("a==" + a + " music is " + music);
            } 
        });
        System.out.println("call back");
    }

    public void foo(int a) {
        System.out.println("foo method");
    }
}

WithAnonymousClass 内部 justCallback 方法,通过匿名内部类的方法实现了这个接口,假设现在需要在 noParams() 和 withParams() 内实现插桩,该怎么办呢?回看一下上一篇中对方法的插桩。

visitMethod 方法
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    

    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

    if (name.equals("run")) {
        mv = new MyMethodVisitor(Opcodes.ASM6, mv);
    }

    if (name.equals("getValue")) {
        mv = new MyMethodVisitorWithReturn(Opcodes.ASM6, mv);
    }

    return mv;
}

在 visitMethod 方法里是根据方法名确定 hack 结点的,那么对于匿名内部类这样的方法可行吗?这里首先从 ClassVisitor 的 visitMethod 开始,看看是否可以直接访问这些方法。

public class WithAnonymousClassVisitor extends ClassVisitor {
    private static final String TAG = "WithAnonymous";


    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        Log.d(TAG, "visit() called with: version = [" + version + "], access = [" + access + "], name = [" + name + "], signature = [" + signature + "], superName = [" + superName + "], interfaces = [" + interfaces + "]");
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        Log.d(TAG, "visitMethod() called with: access = [" + access + "], name = [" + name + "], desc = [" + desc + "], signature = [" + signature + "], exceptions = [" + exceptions + "]");
        return super.visitMethod(access, name, desc, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        Log.d(TAG, "visitEnd() called");
        super.visitEnd();
    }
}

我们可看一下输出日志:

WithAnonymous ==> visit() called with: version = [52], access = [33], name = [com/asm/WithAnonymousClass], signature = [null], superName = [java/lang/Object], interfaces = [[Ljava.lang.String;@2ff4acd0]

WithAnonymous ==> visitMethod() called with: access = [1], name = [<init>], desc = [()V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [justCallback], desc = [(Lcom/asm/internal/World;)V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [foo], desc = [(I)V], signature = [null], exceptions = [null]

WithAnonymous ==> visitEnd() called

可以看到,visitMethod 只访问了 WithAnonymousClass 内的方法(包括默认的构造函数),并没有访问到 Callback 的匿名实现类当中的方法。

这里为什么方位不到匿名内部类的方法呢?道理其实很简单,举个简单的例子就明白了。

匿名内部类的编译结果

public class Main {
  public static void main(String[] args) {
      Thread thread = new Thread(new Runnable() {
          @Override
          public void run() {
              // 
          }
      });
  }
}

上面这个类很简,Thread 需要一个 Runnable 接口的实现,这里采用了匿名内部类的方式。执行命令

javac Main.java

编译完成后,可以看到

-<%>- ls
Main$1.class Main.class   Main.java

除了预期的 Main.class 之外,还生成了一个额外的 Main$1.class 的 class。这就是 java 编译器的规则,对当前类内部的匿名内部类会生成单独的一个类。如果有多个匿名类,会依次按 $n 生成多个类。当然,如果当前类直接 implements 改接口,就没有这种现象了。关于这一点,我们从类的 class 文件也可以看到。

public class com/asm/WithAnonymousClass {

  // compiled from: WithAnonymousClass.java
  // access flags 0x0
  INNERCLASS com/asm/WithAnonymousClass$1 null null

  // access flags 0x1
  public Ljava/lang/String; name

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    ...
}

解决方法

好了,找到了问题的根源,我们就可以从内部类开始找出口。ClassVisitor 提供了 visitInnerClass 可以用于访问内部类。

@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
    Log.d(TAG, "visitInnerClass() called with: name = [" + name + "], outerName = [" + outerName + "], innerName = [" + innerName + "], access = [" + access + "]");
    super.visitInnerClass(name, outerName, innerName, access);
}

产生输出:

WithAnonymous ==> visitInnerClass() called with: name = [com/asm/WithAnonymousClass$1], outerName = [null], innerName = [null], access = [0]

可以看到

com/asm/WithAnonymousClass$1

这个类名和 javac 编译的结果是一致的(有兴趣同学可以自己验证一下,这里就不详细展开了)。 这个类就我们代码中 Callback 对应的匿名内部类吗?刚才也说了,如果有多个匿名内部的实现,会生成多个这样的

com/asm/WithAnonymousClass$n

这里就产生了一个有意思的问题, 如何确定一个类是否实现了某个接口或某些接口 。好在这个问题已经被前人解决了,我们再一次可以站在巨人的肩膀上继续前行 :grin::grin:。

判断某类是否实现了指定接口集合
public class SpecifiedInterfaceImplementionChecked {

    /**
     * 判断是否实现了指定接口
     *
     * @param reader       class reader
     * @param interfaceSet interface collection
     * @return check result
     */
    public static boolean hasImplSpecifiedInterfaces(ClassReader reader, Set<String> interfaceSet) {
        if (isObject(reader.getClassName())) {
            return false;
        }
        try {
            if (containedTargetInterface(reader.getInterfaces(), interfaceSet)) {
                return true;
            } else {
                ClassReader parent = new ClassReader(reader.getSuperName());
                return hasImplSpecifiedInterfaces(parent, interfaceSet);
            }
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * 检查当前类是 Object 类型
     *
     * @param className class name
     * @return checked result
     */
    private static boolean isObject(String className) {
        return "java/lang/Object".equals(className);
    }

    /**
     * 检查接口及其父接口是否实现了目标接口
     *
     * @param interfaceList 待检查接口
     * @param interfaceSet  目标接口
     * @return checked result
     * @throws IOException exp
     */
    private static boolean containedTargetInterface(String[] interfaceList, Set<String> interfaceSet) throws IOException {
        for (String inter : interfaceList) {
            if (interfaceSet.contains(inter)) {
                return true;
            } else {
                ClassReader reader = new ClassReader(inter);
                if (containedTargetInterface(reader.getInterfaces(), interfaceSet)) {
                    return true;
                }
            }
        }
        return false;
    }

}

好了,一旦可以确定某个匿名内部类是否实现了某个接口,那么后续流程,就又回到了我们熟悉得节奏。

@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
    
    super.visitInnerClass(name, outerName, innerName, access);

    HashSet<String> set = new HashSet<>();
    set.add("com/asm/internal/Callback");
    try {
        ClassReader reader = new ClassReader(name);
        if (SpecifiedInterfaceImplementionChecked.hasImplSpecifiedInterfaces(reader, set)) {
            Log.d(TAG, "visitInnerClass: find it");

            ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            ClassVisitor visitor = new InterfaceVisitor(writer);
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

当这个匿名内部类确定是实现了我们期望的接口时,就可以把他当做普通类来处理了,这样的流程就是上一篇讲得内容。我们看一下 InterfaceVisitor

public class InterfaceVisitor extends ClassVisitor {
    private static final String TAG = "InterfaceVisitor";
    public InterfaceVisitor(ClassVisitor cv) {
        super(Opcodes.ASM6, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        Log.d(TAG, "visitMethod() called with: access = [" + access + "], name = [" + name + "], desc = [" + desc + "], signature = [" + signature + "], exceptions = [" + exceptions + "]");
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
}

输出:

InterfaceVisitor ==> visitMethod() called with: access = [0], name = [<init>], desc = [(Lcom/asm/WithAnonymousClass;)V], signature = [null], exceptions = [null]

InterfaceVisitor ==> visitMethod() called with: access = [1], name = [noParams], desc = [()V], signature = [null], exceptions = [null]

InterfaceVisitor ==> visitMethod() called with: access = [1], name = [withParams], desc = [(ILcom/asm/Music;)V], signature = [null], exceptions = [null]

可以看到,现在 ClassVistor 的 visitMethod 方法已经可以正常访问到接口中的方法了(也就是我们之前匿名内部类当中的方法),这样这个 hack 结点就获取到了,就可以为所欲为了。

Lambda 表达式

再来看一种似乎很特殊的情况,Lambda 表达式。经历过曾经的 RxJava 和现在的 Kotlin 的洗礼 ,我们的代码中一定有很多 Lambad 表达式的实现。比如

public void justRun() {
    Thread thread = new Thread(() -> System.out.println("just run"));
}

public void justCallable() {
    // 只用举例,无实际意义
    FutureTask futureTask = new FutureTask(() -> "null");
}

Lambda 表达式的写法,你可以当做是对匿名内部类的简化。那么这些方法结点的获取是不是和匿名内部类一样呢?我们可以先看一下日志。

WithAnonymous ==> visitMethod() called with: access = [1], name = [<init>], desc = [()V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [justRun], desc = [()V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [justCallable], desc = [()V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [justCallback], desc = [(Lcom/asm/internal/World;)V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [1], name = [foo], desc = [(I)V], signature = [null], exceptions = [null]

WithAnonymous ==> visitMethod() called with: access = [4106], name = [lambda$justCallable$1], desc = [()Ljava/lang/Object;], signature = [null], exceptions = [[Ljava.lang.String;@3a71f4dd]

WithAnonymous ==> visitMethod() called with: access = [4106], name = [lambda$justRun$0], desc = [()V], signature = [null], exceptions = [null]

WithAnonymous ==> visitEnd() called

哈哈,原来lambda 表达式的是可以直接被访问到的,因此我们就可以通过方法 desc 确定要进行插桩的方法了。

总结

通过对 ASM 使用过程中,接口作为匿名内部类使用时,其方法是无法直接通过外部类(这里相对于匿名内部类)直接访问到的,因此需要通过 visitInnerClass 方法找到并确定匿名类是否实现了特定的接口,然后把这个 javac 生成的中间类当做一个普通的类,按照常规流程再次通过 ClassVistor 的一系列 API 来确定要进行插桩的结点。

原文  https://rebooters.github.io/2020/01/12/ASM-匿名内部类的处理/
正文到此结束
Loading...