桥接方法的英文说明:https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html。总结起来大概是:因为java代码编译后都会泛型擦除,所以泛型方法中的泛型参数和泛型返回值在class文件中都是object。接口或者类中泛型方法的签名会变成参数为object,而实现类中的方法签名中参数是具体的方法。对泛型方法的重写更像是重载。简单翻译下上面英文,再说说因为这个问题导致的一个线上问题。
如下两个类:
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
泛型擦除编译后的代码为:
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
执行下面代码,在第三行会抛出类型强转错误。
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = mn.data;
因为根据方法签名,子类MyNode中的setData方法并没有重新父类的setData方法,为了保持泛型在类型擦除后的多态性,java编译器会在子类中添加一个桥接方法,如下。因为上面代码段第三行中的n是MyNode类型,所有调用的是MyNode类中的Java编译器生成的setData方法,也就是桥接方法。这个方法会进行类型强转然后调用子类泛型具体化的setData方法,强转时抛了类型强转异常。
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
在java代码开发工具及编译器中,是调用不到一个泛型实现类中的桥接方法(idea中不会显示mn中有一个setData(Objecet data)方法可以调用,直接调用也编译器也会报错)。但是通过class的getDeclaredMethods()方法可以拿到桥接方法,并且子类具体化方法上的注解桥接方法上也存在,也就是说如果在MyNode中setData(Integer data)方法上添加方法注解,那么MyNode中的桥接方法setData(Objecet data)上也有这个注解。我们线上问题就出现在这。
我们通过netty建立长连接,在客户端会根据方法上的注解(自己定义的注解,注解中有唯一标识方法的参数tpye)拿到方法和方法所属的对象和参数,将方法及对象、参数封装成对象targetMethod,以type为key,targetMethod为value进行存储。这个过程是spring容器启动时候做的。我们在调用时候就会偶发性的遇到ClassCastException异常。但在工程启动到工程结束的整个过程中表现一致,也就是第一次调用抛异常后续调用就会抛异常,直到重启才有可能正常调用。这就是因为在spring启动时我们在map中存储的可能是这个泛型实现类中的桥接方法,桥接方法参数是object,我们用json将传入的字符串转换为参数时,转换的也是object,然后桥接方法进行类型强转调用子类具体化方法时报错。