0%

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

前言

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

痛点

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

接口作为匿名内部类实现

接口 Callback
1
2
3
4
5
6
7
8
9
10
package com.asm.internal;

import com.asm.Music;

public interface Callback {

void noParams();

void withParams(int a, Music music);
}

WithAnonymousClass.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 开始,看看是否可以直接访问这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}
}

我们可看一下输出日志:

1
2
3
4
5
6
7
8
9
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 的匿名实现类当中的方法。
这里为什么方位不到匿名内部类的方法呢?道理其实很简单,举个简单的例子就明白了。

匿名内部类的编译结果

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//
}
});
}
}

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

1
javac Main.java

编译完成后,可以看到

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 可以用于访问内部类。

1
2
3
4
5
@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);
}

产生输出:

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

可以看到

1
com/asm/WithAnonymousClass$1

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

1
com/asm/WithAnonymousClass$n

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

判断某类是否实现了指定接口集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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;
}

}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

输出:

1
2
3
4
5
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 表达式的实现。比如

1
2
3
4
5
6
7
8
public void justRun() {
Thread thread = new Thread(() -> System.out.println("just run"));
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 来确定要进行插桩的结点。

加个鸡腿呗.