一个关于IntroductionAdvisor的bug
public class TestMain {public static void main(String[] args) {// 1. 准备被代理的目标对象People peo = new People();// 2. 准备代理工厂ProxyFactory pf = new ProxyFactory();// 3. 准备introduction advice,advice 持有需要额外添加的接口Developer和Developer接口的实现类DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor((Developer) () -> System.out.println("编码"));// 4. 添加advice和代理对象需要继承的接口pf.addAdvice(dii);// 5. 设置被代理对象pf.setTarget(peo);// 6. 这里强制类型转换会失败,因为代理对象采用JDK进行动态代理,只实现了Developer接口和Spring AOP内部接口// 这里按理应该采用Cglib代理才对 !!!peo = (People) pf.getProxy();peo.drink();peo.eat();// 7. 强制转换为Developer接口,实际方法调用会被introduction advice拦截,调用请求转发给了advice内部持有的Developer接口实现类Developer developer = (Developer) peo;developer.code();}public static class People {void eat() {System.out.println("eat");}void drink() {System.out.println("drink");}}public interface Developer {void code();}
}
运行结果:
Exception in thread "main" java.lang.ClassCastException: class com.sun.proxy.$Proxy0 cannot be cast to class com.spring.TestMain$People (com.sun.proxy.$Proxy0 and com.spring.TestMain$People are in unnamed module of loader 'app')at com.spring.TestMain.main(TestMain.java:20)
这里原本是期望代理对象能够采用Cglib进行代理的,因为目标对象没有实现任何接口,但是却因为ProxyFactory特殊处理了类型为IntroductionAdvisor的切面,将IntroductionAdvisor提供的接口都加入到了AdvisedSupport的interfaces接口集合中;导致DefaultAopProxyFactory最终执行代理时,选择采用jdk而非cglib。
所以我们得到的代理对象实际采用jdk实现动态代理,实现了Spring AOP模块内部相关接口和Developer接口,当我们强制将代理对象转换为People类型时,会抛出类型转换异常。
Spring AOP 模块版本为: 5.3.9
原因:
AdvisedSupport 在添加advice的时候会特殊处理IntroductionInfo类型的Advice , 将其额外实现的接口添加到interfaces接口集合中去 :
@Overridepublic void addAdvice(Advice advice) throws AopConfigException {int pos = this.advisors.size();addAdvice(pos, advice);}@Overridepublic void addAdvice(int pos, Advice advice) throws AopConfigException {Assert.notNull(advice, "Advice must not be null");if (advice instanceof IntroductionInfo) {// We don't need an IntroductionAdvisor for this kind of introduction:// It's fully self-describing.addAdvisor(pos, new DefaultIntroductionAdvisor(advice, (IntroductionInfo) advice));}...}@Overridepublic void addAdvisor(int pos, Advisor advisor) throws AopConfigException {if (advisor instanceof IntroductionAdvisor) {validateIntroductionAdvisor((IntroductionAdvisor) advisor);}addAdvisorInternal(pos, advisor);}private void validateIntroductionAdvisor(IntroductionAdvisor advisor) {advisor.validateInterfaces();// If the advisor passed validation, we can make the change.Class<?>[] ifcs = advisor.getInterfaces();for (Class<?> ifc : ifcs) {addInterface(ifc);}}
此时即便目标对象没有实现接口,interfaces集合也不会为空:
private List<Class<?>> interfaces = new ArrayList<>();
这会导致DefaultAopProxyFactory选择是采用jdk还是cglib进行动态代理时,错误的选择JDK而非cglib进行动态代理,因此最终得到的代理对象不能够强制转换为目标对象类型,这与我们预期目标不符合:
@Overridepublic AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (!NativeDetector.inNativeImage() &&(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("TargetSource cannot determine target class: " +"Either an interface or a target is required for proxy creation.");}if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {return new JdkDynamicAopProxy(config);}return new ObjenesisCglibAopProxy(config);}else {return new JdkDynamicAopProxy(config);}}// interfaces集合此时不为空,所以会采用jdk进行动态代理private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {Class<?>[] ifcs = config.getProxiedInterfaces();return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));}
我不确定这边是否算是一个bug , 如果可以的话, 我更期望这边能够单独处理一下IntroductionAdvisor额外提供的接口列表,避免在目标对象没有实现接口的前提下,还是选择采用JDK动态代理。
笔者目前不太确定这是否算做一个bug,目前已将该问题反馈给Spring官方团队,Issue链接如下:
- A bug related to IntroductionAdvisor
关于IntroductionAdvisor的用法,可以参考我之前写的这篇文章进行学习:
- Seata 源码篇之AT模式启动流程 - 上 - 02
2023-09-26 Spring官方回复
简而言之就是确实存在这个bug,但是目前只能临时性强制采用cglib动态代理解决,后期会改进。
各位小伙伴使用IntroductionAdvisor的时候可以注意一下,不要踩了这个坑。