这不是bug,而是java注解机制与spring代理模型共同作用的预期行为:注解需显式声明在实现类方法上、或启用cglib代理、或通过元注解+处理器搬运,三者缺一不可。

这个问题不是 bug,是 Java 注解机制和 Spring 代理模型共同作用下的预期行为。关键不在“跨多层级”,而在于注解是否被声明为可继承、是否在接口上定义、Spring 是否能通过代理匹配到该注解——三者缺一不可。
注解本身不带 @Inherited 时,类/方法上的注解默认不继承
Java 原生的 @Inherited 只对类级别(TYPE)注解生效,且仅限于子类直接 extends 父类时传递;它对方法、字段、接口、实现类完全无效。也就是说:
- 父类方法上标注
@MyAnnotation,子类重写该方法 → 子类方法上不会自动拥有这个注解 - 接口方法上标注
@Retryable,实现类重写该方法 → 实现类方法上反射查不到该注解(即使没加@Inherited,它本来就不支持方法继承) - 想让子类方法“带”上父类或接口的注解,必须显式在子类方法上再写一遍
Spring 的 JDK 动态代理无法识别接口注解的根源
Spring 默认对接口实现类使用 JDK 动态代理(生成一个新类,实现相同接口),但该代理类不继承实现类的任何注解,也不复制接口方法上的注解。所以:
- 切面(如
@Retryable、@Transactional)靠扫描目标方法上的注解来决定是否织入增强逻辑 - 代理对象调用的是实现类的方法,而实现类方法上没有注解 → 切点不匹配 → 不触发重试或事务
- 这不是反射“捕获不到”,而是 Spring 根本没把注解当回事——因为注解不在它检查的位置
真正可行的解决路径
不要试图让反射“穿透”接口去读注解,而是让注解落到 Spring 能看见的地方:
- 方案一(推荐):注解写在实现类方法上 —— 最直接、最可控。哪怕接口已定义,也在实现类对应方法上重复标注一次
-
方案二:改用 CGLIB 代理 —— 在配置中启用
@EnableAspectJAutoProxy(proxyTargetClass = true),让 Spring 代理实现类本身(而非接口),此时可扫描到实现类方法上的注解(但注意:CGLIB 无法代理 final 方法或类) -
方案三:自定义元注解 + 注解处理器 —— 定义一个带
@AliasFor的组合注解,用 AOP Advisor 或BeanPostProcessor在启动时扫描接口+实现类,将接口方法上的注解“搬运”到实现类对应方法上(需配合ReflectionUtils.doWithLocalMethods和AnnotationUtils.synthesizeAnnotation)
额外提醒:别混淆“继承注解”和“代理生效”
有人误以为只要父类方法有 @Transactional,子类重写后就自动生效——其实不成立。Spring 的事务切面只认当前被调用方法所在类的注解。如果子类重写了方法且没加注解,即使父类有,也不会回溯查找。同理,接口上的注解只是契约声明,不是运行时指令。










