简单总结使用 ASM 时遇到匿名内部类时,如何对匿名内部类(一般来说接口)的方法实现插桩。
通过之前的 当 Java 字节码遇到 ASM 一文,对如何使用 ASM 已经有了初步的了解。这里再来看一种比较特殊的情况,当遇到匿名内部类时,如何确定 hack 结点。
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() 内实现插桩,该怎么办呢?回看一下上一篇中对方法的插桩。
@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 表达式。经历过曾经的 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 来确定要进行插桩的结点。