Zacard's Notes

分布式追踪系统x-apm开发拾遗之synthetic与bridge方法

背景

之前开发x-apm的时候,自定义增强一个spring bean的时候,出现了一个奇怪的异常 – 找不到无参构造方法,导致bean初始化失败。原来是bytebuddy这个类库在增强类的时候,会自动增加一个synthetic的构造方法,导致spring无法找打正确的构造方法初始化。这里记录下synthetic方法和bridge方法。

synthetic方法

synthetic方法是什么呢?先来看个实际例子:

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
public class SyntheticTest {
private int i;
public void setI(int i) {
this.i = i;
}
public class A {
private int j;
public int sum() {
return j + i;
}
public void setJ(int j) {
this.j = j;
}
}
public static void main(String[] args) {
SyntheticTest syntheticTest = new SyntheticTest();
A a = syntheticTest.new A();
a.setJ(1);
System.out.println("sum=" + a.sum());
}
}

当你创建一个嵌套类(内部类)时,顶层类的私有属性和私有方法对内部类是可见的。然而jvm是如何处理这种情况的呢?jvm可不清楚什么是内部嵌套类,什么是顶层类。jvm对所有的类都一视同仁,它都认为是顶级类。所有类都会被编译成顶级类,而那些内部类编译完后会生成…$… class的类文件,如下javac编译:

1
2
3
$ javac SyntheticTest.java
$ ls
SyntheticTest$A.class SyntheticTest.class SyntheticTest.java

当你创建内部类时,他会被编译成顶级类。那顶层类的私有属性和私有方法是如何被外部类访问的呢?

javac是这样解决这个问题的,对于任何private的字段,方法或者构造函数,如果它们也被其它顶层类所使用,就会生成一个synthetic方法。这些synthetic方法是用来访问最初的私有变量/方法/构造函数的。这些方法的生成也很智能:只有确实被外部类用到了,才会生成这样的方法

通过反编译SyntheticTest$A.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SyntheticTest$A {
private int j;
public SyntheticTest$A(SyntheticTest var1) {
this.this$0 = var1;
}
public int sum() {
return this.j + SyntheticTest.access$000(this.this$0);
}
public void setJ(int var1) {
this.j = var1;
}
}

有个奇怪的方法SyntheticTest.access$000(this.this$0),这个就是java的synthetic方法。可以用java的反射再次验证这个问题:

1
2
3
4
5
public static void main(String[] args) {
for (Method method : SyntheticTest.class.getDeclaredMethods()) {
System.out.println(method.getName() + " is synthetic method:" + method.isSynthetic());
}
}

输出:

1
2
3
main is synthetic method:false
access$000 is synthetic method:true
setI is synthetic method:false

可以看到access$000这个方法,确实是一个synthetic方法

synthetic方法就是java编译器(例如javac)为了实现特定需求增加的方法,不存在源码中的

bridge方法

bridge方法又是什么呢?看个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BridgeTest {
public static class MyLink extends LinkedList {
@Override
public String get(int index) {
return "my list " + index;
}
}
public static void main(String[] args) {
for (Method method : MyLink.class.getDeclaredMethods()) {
System.out.println(method.toString() + " is bridge method:" + method.isBridge());
}
}
}

输出如下:

1
2
public java.lang.String com.zacard.algorithm.test.BridgeTest$MyLink.get(int) is bridge method:false
public java.lang.Object com.zacard.algorithm.test.BridgeTest$MyLink.get(int) is bridge method:true

可以看到,多出来一个方法签名一致,返回类型为Object的bridge方法,这在java语言中是不合法的,不过在jvm中是允许的。那这个bridge方法到底作了什么呢?反编译看看:

1
javap -c BridgeTest\$MyLink

输出如下:

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
public class com.zacard.algorithm.test.BridgeTest$MyLink extends java.util.LinkedList {
public com.zacard.algorithm.test.BridgeTest$MyLink();
Code:
0: aload_0
1: invokespecial #1 // Method java/util/LinkedList."<init>":()V
4: return
public java.lang.String get(int);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #4 // String my list
9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: iload_1
13: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
16: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
19: areturn
public java.lang.Object get(int);
Code:
0: aload_0
1: iload_1
2: invokevirtual #8 // Method get:(I)Ljava/lang/String;
5: areturn
}

可以看到,这个birdge不干别的,仅仅就是调用了原始的那个方法。所以这个方法到底有什么用,为什么需要bridge方法?看一下java手册的说明:

When compiling a class or interface that extends a parameterized class or implements a parameterized interface, the compiler may need to create a synthetic method, called a bridge method, as part of the type erasure process. You normally don’t need to worry about bridge methods, but you might be puzzled if one appears in a stack trace.

如果一个类继承了一个范型类或者实现了一个范型接口, 那么编译器在编译这个类的时候就会生成一个叫做桥接方法的混合方法(混合方法简单的说就是由编译器生成的方法, 方法上有synthetic修饰符), 这个方法用于范型的类型安全处理,用户一般不需要关心桥接方法

其实是java为了泛型的向下兼容的一种手段。我们看下另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BridgeTest2 {
public static class Node<T>{
private T data;
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public static class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
}

这个类在泛型擦除后,变成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BridgeTest2 {
public static class Node{
private Object data;
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public static class MyNode extends Node {
@Override
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
}

子类的setData方法签名和父类的已经不一致了。因此,MyNode.setData方法其实已经不再重写(override)父类Node.setData方法了。

为了解决这个问题,并且维持泛型类在泛型擦除后的多态性,java编译器会生成一个bridge方法

坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章