先问一个问题:你有没有遇到过这种诡异场景——明明IDE里Override标注得好好的,代码编译也通过,但到了线上,通过反射或某些框架去扫方法时,突然多出一个你根本没写过、签名也怪怪的同名方法?如果你还做过一点字节码相关的工作,可能还会看到方法上挂着 ACC_BRIDGE、ACC_SYNTHETIC 这样的标志位。这正是Java泛型里最经典的隐藏机制之一:桥方法(bridge method)。这篇内容我会把Java泛型擦除之后,编译器如何用桥方法“缝合”多态、覆写和泛型签名之间的缝隙,从字节码到反射,再到框架源码和面试题,完整拆一遍。
看完你会明白几个东西:桥方法到底是怎么生成的、长什么样、为什么没有它Java的泛型多态就坍了,以及为什么Spring、MyBatis这类框架在扫描方法时总喜欢拿 isBridge() 做过滤。全文用源码、javap反编译和实战案例说话,适合准备Java面试、写框架或者单纯对JVM字节码好奇的同学。
1. 桥方法的出场时刻:一个能把程序员“送走”的诡异现象
1.1 一次来自 IDE 的“假覆写”与运行时的“真异常”
先看一段很常见的代码:
java复制public class BridgeDemo {
static class Parent<T> {
public void set(T value) {
System.out.println("Parent.set: " + value);
}
}
static class Child extends Parent<String> {
@Override
public void set(String value) {
System.out.println("Child.set(String): " + value);
}
}
public static void main(String[] args) {
Parent<String> parent = new Child();
parent.set("hello");
}
}
这段代码运行结果符合预期:Child.set(String): hello,多态生效了。
但如果你用反射把这俩类的方法列表打出来,问题就来了:
java复制for (var method : Child.class.getDeclaredMethods()) {
System.out.println(method + " | bridge=" + method.isBridge());
}
输出会是这样:
code复制public void BridgeDemo$Child.set(java.lang.String) | bridge=false
public void BridgeDemo$Child.set(java.lang.Object) | bridge=true
Child 类里明明只写了一个 set(String),但字节码层面多了一个 set(Object)。这个多余的、参数类型是 Object 的方法,就是编译器生成的桥方法。
很多程序员第一次碰到这里会懵:我没写这个方法啊?IDE没显示啊?甚至手写反射代码时,按参数类型 Object.class 去 getMethod,还能拿到一个方法。你猜怎么着?还真能拿到,而且在某些场景下你调用的根本不是你写的那个业务方法,而是这个编译器抄的“影子方法”。
1.2 泛型的运行时真相:类型擦除带来的双签名矛盾
Java泛型从诞生那天起,就选择了“编译期类型检查 + 运行期类型擦除”的路线。什么意思呢?你在源码里写得再漂亮:
java复制Parent<String> parent = new Child();
编译成字节码之后,泛型信息基本就没了,class Parent<T> 里的 T 会被擦除到它的上界。如果上界没指定,擦除结果就是 Object。这一点是理解桥方法的前提。
那问题出在哪呢?我们把上面的 Parent 和 Child 设想成编译后的“裸”类:
Parent编译后是class Parent { public void set(Object value) }Child编译后是class Child extends Parent { public void set(String value) }
两个方法的参数类型不一致:一个是 Object,一个是 String。按照Java方法覆写规则,子类方法的参数类型必须和父类完全一致才算覆写,不一致只能是重载。也就是说,类型擦除之后,Child.set(String) 和 Parent.set(Object) 在JVM看来根本不构成覆写关系。
但你明明写了 @Override,而且JVM多态调用也确实走到了 Child.set(String)——这不矛盾吗?是的,矛盾,所以编译器不得不额外做点手脚来“圆谎”。
1.3 桥方法的定义:编译器偷偷生成的“翻译官”
桥方法,英文叫 bridge method,是编译器自动生成的一个合成方法。它的任务相当于“翻译官”:把擦除后的父类方法签名(比如 set(Object))和子类真正的业务方法签名(比如 set(String))之间,搭一座桥。
从字节码角度看,桥方法具备三个特征:
- 方法名和业务方法一致。
- 参数类型和“父类泛型擦除后的类型”一致,返回值同理。
- 方法体内部会做一次强制类型转换(
checkcast),然后把调用转发给真正的业务方法。
桥方法在字节码层面的访问标志带上了 ACC_BRIDGE 和 ACC_SYNTHETIC,这也是反射API里 isBridge()、isSynthetic() 能判断出来的底层依据。
你可以这么理解桥方法的价值:没有它,Parent<String> p = new Child(); p.set("hello") 这种写法,在JVM运行时根本没法把调用路由到 Child.set(String)。因为JVM的方法查找是按“方法名 + 参数类型 + 返回值”来匹配的,set(String) 和父类方法表中的 set(Object) 对不上号。而有了桥方法之后,子类的方法表里就有了一个和父类签名完全一致的 set(Object),方法调用能沿着正确的签名找到它,再由它转发到真正干活的 set(String)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从字节码看桥方法:亲手反编译验证
2.1 最基础示例:泛型父类 + String 子类
理论知识说了这么多,不如直接看字节码。JDK自带的 javap 就是最好的“照妖镜”。我们把前面的 BridgeDemo 编译后再反编译:
bash复制javac BridgeDemo.java
javap -p -v Child
重点看 Child 类的输出,我挑关键部分给你拆开看。
第一段是我们自己写的方法:
code复制public void set(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
0: getstatic #2 // Field java/lang/System.out
3: ldc #3 // String Child.set(String):
5: invokevirtual #4 // Method java/io/PrintStream.println
8: return
第二段是编译器生成的桥方法:
code复制public void set(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
0: aload_0
1: aload_1
2: checkcast #7 // class java/lang/String
5: invokevirtual #8 // Method set:(Ljava/lang/String;)V
8: return
注意 flags 那一行,0x1041 就是 ACC_PUBLIC(0x0001) + ACC_BRIDGE(0x0040) + ACC_SYNTHETIC(0x1000) 的和。这段字节码干了三件事:
aload_0:把this压入操作数栈。aload_1:把方法参数(Object类型)压栈。checkcast #7:把参数强制转成String,如果类型不匹配,这里直接抛ClassCastException。invokevirtual #8:调用真正的set(String)方法。
这基本就是所有泛型桥方法的长相:参数能转就转,转完立刻转发。
2.2 桥方法与协变返回类型的“双胞胎”关系
除了泛型方法覆写,还有一类场景也会生成桥方法——协变返回类型。看这段代码:
java复制static class Parent2 {
public Object get() {
return null;
}
}
static class Child2 extends Parent2 {
@Override
public String get() {
return "hello";
}
}
Java允许子类覆写父类方法时,把返回类型改成父类返回类型的子类型,这叫“协变返回”。但JVM方法描述符里,返回值也是签名的一部分!Parent2.get() 的返回值是 Object,Child2.get() 的返回值是 String,严格来说这是两个不同的方法。所以编译器也会在 Child2 里生成一个桥方法:
code复制public java.lang.String get();
...
public java.lang.Object get();
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
0: aload_0
1: invokevirtual #N // Method get:()Ljava/lang/String;
4: areturn
这个桥方法没有 checkcast,因为它内部调用的 get() 本来就会返回 String,而 String 是 Object 的子类,JVM天然允许把引用变量返回给父类型引用。所以它只做了一件事:调用业务方法,然后把返回值原样返回。
从这里你可以看出一个规律:桥方法生成的本质,是把“签名不完全一致但语义上属于覆写”的方法,在字节码层面补出一个和父类完全匹配的版本。泛型覆写是参数类型不匹配,协变返回是返回类型不匹配,桥方法就是来处理这种不匹配的。
2.3 三种常见桥方法生成场景:泛型覆写、协变返回、泛型接口实现
我把实际开发中最常遇到的桥方法生成场景汇总一张表:
| 场景 | 示例 | 生成的桥方法特征 |
|---|---|---|
| 泛型父类覆写 | Child extends Parent<String>,覆写 set(String) |
生成 set(Object),内部 checkcast 到 String 后转发 |
| 协变返回类型 | Parent.get() 返回 Object,Child.get() 返回 String |
生成 get():Object,内部转发 get():String |
| 泛型接口实现 | class C implements Comparator<String>,实现 compare(String, String) |
生成 compare(Object, Object),内部 checkcast 后转发 |
第三种场景在实际代码里最隐蔽,我把代码也贴出来:
java复制class StringComparator implements Comparator<String> {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
}
反编译后你会看到,除了 compare(String, String) 之外,还有一个:
code复制public int compare(java.lang.Object, java.lang.Object);
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
0: aload_0
1: aload_1
2: checkcast // class java/lang/String
5: aload_2
6: checkcast // class java/lang/String
9: invokevirtual // Method compare:(Ljava/lang/String;Ljava/lang/String;)I
12: ireturn
Comparator<T> 擦除后是 Comparator,接口方法签名是 compare(Object, Object)。你的实现类写的是 compare(String, String),如果没有桥方法,编译器根本没法向JVM证明这个类完整实现了 Comparator 接口。所以桥方法在这里更是必不可少的“补丁”。
注意:只有当子类/实现类把泛型参数具体化成某个类型,导致方法签名和擦除后的父类/接口不一致时,编译器才需要生成桥方法。如果子类覆写时也写成
set(Object)或者compare(Object, Object),桥方法就不会出现。
3. 桥方法是如何参与多态分派的
3.1 JVM 方法查找与 invokevirtual 的完整路径
光知道桥方法长什么样还不够,还得说清楚它怎么参与运行时多态。这得回到JVM的方法调用机制。
当代码里出现 Parent<String> p = new Child(); p.set("hello") 时,编译阶段编译器只知道 p 的静态类型是 Parent<String>,而这个类型擦除后的方法签名是 set(Object)。所以编译器生成的调用指令是:
code复制invokevirtual #N Method Parent.set:(Ljava/lang/Object;)V
运行时,JVM拿到调用点的方法签名 set(Object),去实际对象 Child 的方法表里查找签名匹配的方法。Child 的方法表里,set(Object) 正好有一个,但它不是我们手写的那个业务方法,而是编译器生成的桥方法。
等JVM调用桥方法之后,桥方法内部再做一次 checkcast 和 invokevirtual,通过阿调用真正的 Child.set(String)。这样,一次看似普通的泛型多态调用,实际在JVM内部走了两跳:
code复制invokevirtual Parent.set(Object) // 调用点
-> Child.set(Object) // 编译器生成的桥方法
-> Child.set(String) // 手写的业务方法
理解这条链路,你就能解释之前那个诡异现象:为什么通过父类引用调用,子类的覆写方法能跑起来,而直接看方法签名却发现两者对不上。不是JVM特殊开恩,而是编译器和JVM之间通过桥方法达成了一个默契。
3.2 桥方法内部到底是什么样的“转发代码”
再看一眼桥方法的字节码,其实它的逻辑非常简单,就是一个“参数转换 + 转发调用”的薄壳:
code复制aload_0
aload_1
checkcast java/lang/String
invokevirtual set:(Ljava/lang/String;)V
return
重点在 checkcast。这个指令不是凭空来的,它会根据业务方法的参数类型生成。如果业务方法是 set(String),checkcast 的目标就是 String;如果是 set(Integer),目标就是 Integer;如果业务方法参数本身就是擦除后的 Object,那就没有 checkcast,桥方法可能都不需要生成。
这里有个面试里常问的坑:桥方法内部的 checkcast 是可能抛异常的。如果通过反射强行把一个类型不匹配的值塞进桥方法:
java复制Parent<String> p = new Child();
Method m = Child.class.getMethod("set", Object.class);
m.invoke(p, 123); // 编译能过,运行直接炸
你会看到异常栈指向:
code复制java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at BridgeDemo$Child.set(BridgeDemo.java:...)
乍一看像是你方法写的锅,但实际上抛异常的位置在桥方法里,那个 checkcast 就是类型安全的一道关卡。编译器在调用点把泛型类型检查提前到了编译期,运行期的这道 checkcast 是兜底防线。
3.3 边界类型不是 Object 时:桥方法参数类型也跟着变
上面所有例子里的泛型参数都是无界 T,擦除结果自然是 Object。但Java允许给泛型参数加边界,比如:
java复制static class NumberParent<T extends Number> {
public void set(T value) {
}
}
static class IntChild extends NumberParent<Integer> {
@Override
public void set(Integer value) {
}
}
这种场景下,NumberParent<T extends Number> 里的 T 擦除不会擦到 Object,而是擦到边界类型 Number。因此编译器生成的桥方法签名是:
code复制public void set(java.lang.Number);
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
0: aload_0
1: aload_1
2: checkcast // class java/lang/Integer
5: invokevirtual // Method set:(Ljava/lang/Integer;)V
也就是说,桥方法的参数类型不是拍脑袋定的,它严格等于“泛型参数擦除后的边界类型”。如果边界是 Number,桥方法就是 set(Number);如果边界是 Comparable<T>,桥方法参数就是 Comparable。这个细节很多人会搞错,以为桥方法参数永远是 Object,实际上得看父类泛型的上界声明。
排查技巧:真正写框架的时候,如果你想通过反射找到“真实的业务方法”,不能只按方法名找,还得把桥方法参数类型和业务方法参数类型一起比对,否则很容易找成桥方法。
4. 桥方法与反射、框架源码的爱恨情仇
4.1 Method.isBridge() / isSynthetic() 的正确用法
桥方法对普通业务代码基本透明,但一旦你踩进反射、字节码、序列化、AOP这些“底层工具”的领域,桥方法就会变成你面前的一根刺。
Java反射API提供了两个判断方法:
Method.isBridge():判断方法是否是桥方法。Method.isSynthetic():判断方法是否是编译器生成的合成方法。
桥方法同时满足这两个条件:isBridge() == true、isSynthetic() == true。但注意,isSynthetic() 为 true 的不一定都是桥方法。比如内部类访问外部类私有字段时,编译器会生成 access$000 这样的合成方法,它也是 synthetic,但不是 bridge。所以判断时最好用 isBridge(),而不是 isSynthetic()。
在反射遍历方法时,最稳妥的过滤写法是:
java复制for (Method method : clazz.getDeclaredMethods()) {
if (method.isBridge() || method.isSynthetic()) {
continue;
}
// 这才是真正的业务方法
}
如果你不过滤,后面处理参数或返回值时,很容易把桥方法也当成一个正常方法去解析,导致逻辑重复,甚至处理到 set(Object) 这种伪方法时出现 ClassCastException。
4.2 Spring、MyBatis等框架如何过滤桥方法
这几年我读过的框架源码里,桥方法的“戏份”相当多。最有代表性的就是Spring。
Spring在处理 @Controller 里的 HandlerMethod 时,会扫描控制器类的方法。如果控制器继承了泛型父类,并且覆写了泛型方法,Spring拿到的候选方法集合里就会混入桥方法。如果不去掉桥方法,Spring MVC会以为你注册了重复的处理器映射。Spring 内部有一个 BridgeMethodResolver.findBridgedMethod() 方法,专门把传入的桥方法解析成它背后真正调用的业务方法。虽然Spring后来重构成用 MergedAnnotations 来解析注解,但过滤桥方法的逻辑始终存在。
MyBatis处理 Mapper 接口时也类似。MapperMethod 需要解析方法返回值的真实泛型类型(比如 List<User> 里的 User),如果只用 method.getReturnType(),拿到的只会是擦除后的 List,所以它内部用 GenericTypeResolver.resolveReturnType() 这类工具去拿泛型签名。而这个过程中方法必须是真正的业务方法,不是桥方法,否则参数的泛型解析结果也会是错的。
还有Spring的 @Transactional 注解。如果一个方法通过桥方法被调用,事务切面按方法签名匹配切点时可能匹配到桥方法;反过来,切点表达式里写的是具体参数类型,又匹配不到桥方法。很多“为什么我的事务没生效”的诡异问题,根子就在这。
4.3 手写反射工具时最容易踩的三个坑
我自己写一个小型ORM框架时,就栽过这上面的跟头。当时我需要根据方法名+参数类型从类里找一个“setter”方法,写出来的代码是这样的:
java复制Method m = clazz.getMethod("setValue", Object.class);
结果因为类实现了泛型接口,getMethod("setValue", Object.class) 命中的是桥方法,而这个桥方法内部会 checkcast 到具体类型。我的调用代码传了一个类型不匹配的对象进去,抛出来的异常信息极其迷惑。
后来我总结出了三个高频的坑,先说结论:
第一个坑:用 Object.class 或擦除类型去匹配泛型方法,可能匹配到桥方法。解决方案是拿到所有方法后,过滤掉 isBridge()。
第二个坑:直接遍历 getMethods() 时遇到重名方法,不知道选哪个。可以用“先排除桥方法,再按参数类型精确匹配”的策略。
第三个坑:处理泛型返回值时,如果用的是 getReturnType() 而不是 getGenericReturnType(),会把桥方法里返回的 Object 当成最终类型,从而丢失真正的 String、Integer 等类型信息。正确写法是:
java复制Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType pt) {
Type[] actualTypes = pt.getActualTypeArguments();
// actualTypes[0] 才是真正的泛型实参
}
调这一类API时,我的习惯是:先 isBridge() 过滤,再 getGenericParameterTypes() 拿参数,最后 getGenericReturnType() 拿返回值。这个顺序能避开90%的桥方法坑。
5. 面试与源码高频考点:能说清楚才叫真正掌握
5.1 一份可直接背诵的桥方法问答清单
桥方法在Java八股文里出现的频率不低,但真正能讲到字节码层面的人不多。我把面试官最常问的几个问题整理成一个速查表,背下来能应急,理解之后再消化成自己的话。
| 常见问题 | 回答要点 |
|---|---|
| 桥方法是什么 | 编译器生成的合成方法,用于保持泛型擦除后的多态语义,方法名与业务方法一致,参数/返回值类型对应擦除后的父类签名 |
| 桥方法何时生成 | 子类用具体类型覆写泛型父类方法时;子类协变返回类型时;类实现泛型接口时 |
| 桥方法有什么标志 | ACC_BRIDGE + ACC_SYNTHETIC,可通过 Method.isBridge() 判断 |
| 桥方法会不会抛异常 | 会,内部 checkcast 类型不匹配时抛 ClassCastException |
| 桥方法参数是什么类型 | 与泛型参数擦除后的边界类型一致,无界泛型是 Object,有界泛型是边界类型 |
| 手写的重载和桥方法有什么区别 | 手写 set(Object) 和 set(String) 只是重载,不构成覆写;桥方法是编译器附加到子类上的“覆写代理” |
这里再补充一个更底层的理解:桥方法本质上是“类型擦除”和“面向对象多态”这两个设计目标之间的调和产物。Java既要保证泛型在编译期做类型检查,又不希望引入运行时的类型参数占用额外内存,所以选择擦除;但擦除之后,原本靠“签名一致”维系的覆写关系被破坏,编译器就必须在字节码层面“多造”一个签名一致的方法出来。桥方法就是那个“再造出来的方法”。
5.2 从桥方法看 Java 编译器的多态补偿设计思路
如果你对JVM和编译器设计感兴趣,桥方法是个绝佳观察窗口。它揭示了一个深刻的取舍:Java选择了擦除式泛型,而不是像C++模板那样为每种类型都生成一份独立代码,也不是像C#那样在运行时保留泛型信息。擦除的代价,就是程序里存在“两套方法签名”——源码层面一套、字节码层面一套。为了让两套签名共处,编译器就得在中间层偷偷加胶水。
那为什么不直接在调用点把 set("hello") 翻译成 set((Object)"hello") 然后调用 set(String)?因为调用点的静态类型是父类,JVM方法分派是按实际对象的方法表来的,如果子类方法表里没有和父类相同签名的条目,分派就断了。桥方法相当于子类对外“注册”一个兼容父类签名的入口,真正的业务逻辑藏在里面。
这个设计思路还延伸到其他语言。比如Kotlin编译器处理泛型和协变时,也会生成类似桥方法的东西;Scala在JVM上做特化也有类似概念。理解了Java桥方法,你再去看其他JVM语言的编译器输出,会有一种“原来都是同一套把戏”的通透感。
观点:桥方法不是Java的缺陷,而是一种精心设计的“妥协”。面试时如果能把“擦除导致签名不匹配 -> 编译器生成桥方法 -> JVM方法分派链路 -> 反射框架的过滤逻辑”这条线讲完整,远比背多少概念都有说服力。
6. 常见问题与排查技巧实录
6.1 案例一:ClassCastException 出现在“不该出现”的桥方法内部
有一次线上接口报错,日志里的异常栈指向了一个 checkcast 指令,但对应的业务代码那行怎么看都不像会抛类型转换错误。当时排查了半天,最后才发现异常发生在我们自己封装的一个反射调用工具里。
核心问题就是工具方法用 getMethod() 按擦除类型匹配到了一个桥方法,然后传进去一个类型不匹配的值,桥方法内部的 checkcast 直接拦住了。理解桥方法之后,这个问题就很好复现、也很好修:
java复制Method m = clazz.getMethod("set", Object.class);
// 反射拿到桥方法,但桥方法期望的是 String
m.invoke(obj, 123); // 抛 ClassCastException
修复方式有两种:一是调用前先判断 method.isBridge(),然后通过参数类型找到真正的业务方法;二是在反射工具中统一过滤桥方法。两者选哪一种取决于你的场景。如果你要模拟编译器“向上转型调用”,桥方法其实是合理入口;如果你要调业务逻辑,那就必须绕过桥方法。
6.2 案例二:反射找不到按参数类型匹配的方法
另一个常见问题是反向的:我用 clazz.getMethod("compare", Object.class, Object.class) 去找方法,找到了,但对结果的处理全部基于 String 类型进行,结果拿到了一个 Comparator<String> 的泛型信息就丢了。
原因还是桥方法。getMethod() 返回的可能是桥方法,它的参数类型是 Object,泛型签名里根本看不到 String 这种具体类型。对于框架代码,正确的做法是拿方法后先用 isBridge() 过滤,如果确实是桥方法,就通过 getGenericParameterTypes() 或者 Method 的桥接关系反查真正的业务方法。这一步不能省。
另外,getMethods() 和 getDeclaredMethods() 都会返回桥方法。如果你在一个类里遍历方法做校验,看到两个同名同返回值的不同参数方法,别急着定义“重复方法”,先想想是不是桥方法在作怪。
6.3 案例三:字节码增强工具重复处理泛型方法
用ASM或者ByteBuddy这类字节码库做插桩时,桥方法也会出来刷存在感。因为编译器生成的桥方法是真实存在于class文件里的,字节码增强工具如果不加判断,会把桥方法和业务方法当成两个独立方法分别处理,导致逻辑被增强两次,甚至出现栈帧错乱。
用ASM时,我一般会对 MethodVisitor 的访问做过滤:
java复制private boolean isBridge(int access) {
return (access & Opcodes.ACC_BRIDGE) != 0;
}
在 ClassReader 解析方法时,直接跳过带 ACC_BRIDGE 的方法,或者在生成时保留原有标志位。如果目标是增强业务逻辑,跳过桥方法的同时,还要保证桥方法内对业务方法的调用不会出问题——因为桥方法在增强后的字节码里仍然会 invokevirtual 到业务方法,这一跳不受影响。
这个坑在Spring AOP + 泛型 Controller 的“经典”组合里也出现过。切面给一个泛型方法做了增强,但由于匹配到了桥方法,导致环绕通知的逻辑执行了两次。修复方法通常是让切点表达式匹配具体参数类型,或者让AOP内部对桥方法做幂等处理。
6.4 排查桥方法问题的三板斧
如果你怀疑当前的诡异问题跟桥方法有关,我给你一套实操排查流程:
第一步,确认方法是不是桥方法。写一行代码打印所有方法,或者用 javap -p -v 看字节码里的 flags,如果看到了 ACC_BRIDGE, ACC_SYNTHETIC,恭喜,破案了。
第二步,确认你是想“找业务方法”还是“找调用入口”。校验、序列化、转换等场景,一律绕过桥方法,用 isBridge() 过滤;模拟父类多态调用、做代理转发时,桥方法反而是应该被调用的目标,别错误绕过。
第三步,处理泛型参数/返回值时,不要只看 getParameterTypes() 和 getReturnType(),换成 getGenericParameterTypes() 和 getGenericReturnType(),配合 ParameterizedType 解析真实类型。
这套三板斧我在排查Spring MVC的方法解析、MyBatis 的 mapper 方法解析和自研ORM的字段映射时都用过,基本能覆盖90%以上的桥方法引发的诡异问题。
最后再分享一个我个人的习惯:写框架代码时,我给自己定了一条规矩——所有扫描到的方法,先问一句“这是桥方法吗?”如果不是,继续处理;如果是,要么跳过,要么找到它桥接的真实方法。有了这个习惯,后来遇到的重名方法、类型转换异常、泛型信息丢失等问题都少了很多。这个判断成本极低,却能在关键时候给你省下好几个小时的排障时间。
