• 一个关于IntroductionAdvisor的bug


    一个关于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();
        }
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    运行结果:

    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)
    
    • 1
    • 2

    这里原本是期望代理对象能够采用Cglib进行代理的,因为目标对象没有实现任何接口,但是却因为ProxyFactory特殊处理了类型为IntroductionAdvisor的切面,将IntroductionAdvisor提供的接口都加入到了AdvisedSupport的interfaces接口集合中;导致DefaultAopProxyFactory最终执行代理时,选择采用jdk而非cglib。

    所以我们得到的代理对象实际采用jdk实现动态代理,实现了Spring AOP模块内部相关接口和Developer接口,当我们强制将代理对象转换为People类型时,会抛出类型转换异常。


    问题原因

    Spring AOP 模块版本为: 5.3.9

    在这里插入图片描述

    原因:

    AdvisedSupport 在添加advice的时候会特殊处理IntroductionInfo类型的Advice , 将其额外实现的接口添加到interfaces接口集合中去 :

    	@Override
    	public void addAdvice(Advice advice) throws AopConfigException {
    		int pos = this.advisors.size();
    		addAdvice(pos, advice);
    	}
    
    	@Override
    	public 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));
    		}
    		...
    	}
    
    	@Override
    	public 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);
    		}
    	}
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    ​ 此时即便目标对象没有实现接口,interfaces集合也不会为空:

    	private List<Class<?>> interfaces = new ArrayList<>();
    
    • 1

    这会导致DefaultAopProxyFactory选择是采用jdk还是cglib进行动态代理时,错误的选择JDK而非cglib进行动态代理,因此最终得到的代理对象不能够强制转换为目标对象类型,这与我们预期目标不符合:

    	@Override
    	public 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])));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我不确定这边是否算是一个bug , 如果可以的话, 我更期望这边能够单独处理一下IntroductionAdvisor额外提供的接口列表,避免在目标对象没有实现接口的前提下,还是选择采用JDK动态代理。


    反馈结果

    笔者目前不太确定这是否算做一个bug,目前已将该问题反馈给Spring官方团队,Issue链接如下:

    关于IntroductionAdvisor的用法,可以参考我之前写的这篇文章进行学习:

    2023-09-26 Spring官方回复

    在这里插入图片描述
    简而言之就是确实存在这个bug,但是目前只能临时性强制采用cglib动态代理解决,后期会改进。

    各位小伙伴使用IntroductionAdvisor的时候可以注意一下,不要踩了这个坑。


    解决方案

    当我们调用ProxyFactory的setTarget方法指定了需要代理的目标对象时,他不会帮我们判断目标对象是否实现了接口,此时会默认采用cglib执行动态代理;除非我们手动调用addInterface添加目标对象实现的接口,才会采用jdk动态代理:

    public class TestMain {
        public static void main(String[] args) {
            Stu s = () -> System.out.println("stu");
            ProxyFactory pf = new ProxyFactory();
            pf.setTarget(s);
            // 主动调用addInterface,proxyFactory才会采用jdk动态代理
            pf.addInterface(Stu.class);
            Stu stu = (Stu) pf.getProxy();
            stu.study();
        }
    
        public interface Stu {
            void study();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    上面举例的场景中,是否手动调用addInterface方法添加接口并不会有什么大问题,但是如果是下面这个场景,则存在问题:

    public class TestMain {
        public static void main(String[] args) {
            Stu s = new Stu();
            ProxyFactory pf = new ProxyFactory();
            pf.setTarget(s);
            pf.addInterface(Boy.class);
            Object proxy = pf.getProxy();
            Stu stu = (Stu) proxy;
            stu.study();
        }
    
        public static class Stu {
            void study() {
                System.out.println("study");
            }
        }
    
        public interface Boy {
            void boy();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    此时会抛出类型转换异常,因为最终采用了jdk动态代理,代理对象只实现了spring aop内部接口外加Boy接口,因此代理对象无法强制转换为Stu类型:

    Exception in thread "main" java.lang.ClassCastException: class com.sun.proxy.$Proxy0 cannot be cast to class com.spring.TestMain$Stu (com.sun.proxy.$Proxy0 and com.spring.TestMain$Stu are in unnamed module of loader 'app')
    	at com.spring.TestMain.main(TestMain.java:11)
    
    • 1
    • 2

    在这里插入图片描述
    上面一开始抛出的问题,也是由于同样的原因,只不过是由DelegatingIntroductionInterceptor间接调用的addInterface方法添加的额外接口。

    我觉得代理对象只是为了在目标对象基础上进行增强,并且代理对象本身需要能够强制转换为目标对象本身类型或者其继承的某个接口类型;而在该场景下,代理对象并不能强制转换为目标对象类型,这违背了其初衷。

    为了解决该场景下出现的这个问题,可以考虑在DefaultAopProxyFactory类的createAopProxy方法中判断一下目标对象是否存在实现了的接口,如果没有,则采用cglib执行动态代理:

    public class CustomDefaultAopProxyFactory implements AopProxyFactory, Serializable {
    
        @Override
        public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
            if (!NativeDetector.inNativeImage() &&
                    (config.isOptimize() || config.isProxyTargetClass() ||
     // 修改处: 如果目标对象没有实现任何接口,则依旧采用cglib执行动态代理  
     hashNoInterfaceImplement(config) || 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.");
                }
                // 此处可以确保targetClass为接口类型的前提下,依旧采用jdk执行动态代理
                if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                    return new JdkDynamicAopProxy(config);
                }
                return new ObjenesisCglibAopProxy(config);
            } else {
                return new JdkDynamicAopProxy(config);
            }
        }
        
        // 新增逻辑 !!!
        private boolean hashNoInterfaceImplement(AdvisedSupport config) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) return false;
            return targetClass.getInterfaces().length == 0;
        }
    
        private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
            Class<?>[] ifcs = config.getProxiedInterfaces();
            return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
        }
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    替换默认的DefaultAopProxyFactory实现,然后继续执行测试:

    public class TestMain {
        public static void main(String[] args) {
            Stu s = new Stu();
            ProxyFactory pf = new ProxyFactory();
            // 采用自定义的动态代理创建工厂
            pf.setAopProxyFactory(new CustomDefaultAopProxyFactory());
            pf.setTarget(s);
            pf.addInterface(Boy.class);
            Object proxy = pf.getProxy();
            Stu stu = (Stu) proxy;
            stu.study();
            // 输出当前代理对象实现的接口
            for (Class<?> anInterface : proxy.getClass().getInterfaces()) {
                System.out.println(anInterface);
            }
        }
    
        public static class Stu {
            void study() {
                System.out.println("study");
            }
        }
    
        public interface Boy {
            void boy();
        }
    }
    
    • 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

    输出结果:

    study
    interface com.spring.TestMain$Boy
    interface org.springframework.aop.SpringProxy
    interface org.springframework.aop.framework.Advised
    interface org.springframework.cglib.proxy.Factory
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面针对一开始给出的案例执行测试:

    public class TestMain {
        public static void main(String[] args) {
            People peo = new People();
            ProxyFactory pf = new ProxyFactory();
            // 一开始的测试用例,此处替换默认的代理对象创建工厂实现
            pf.setAopProxyFactory(new CustomDefaultAopProxyFactory());
            DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor((Developer) () -> System.out.println("编码"));
            pf.addAdvice(dii);
            pf.setTarget(peo);
            peo = (People) pf.getProxy();
            peo.drink();
            peo.eat();
            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();
        }
    }
    
    • 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
    • 28
    • 29
    • 30

    输出结果:

    drink
    eat
    编码
    
    • 1
    • 2
    • 3

    下面是该问题最终解决方案的pr链接:

    补充完整测试用例:

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.aop.framework.ProxyFactory;
    import org.springframework.aop.support.AopUtils;
    import org.springframework.aop.support.DelegatePerTargetObjectIntroductionInterceptor;
    import org.springframework.aop.support.DelegatingIntroductionInterceptor;
    
    /**
     * @author 占道宏
     * @create 2023/10/10 9:59
     */
    public class ProxyFactoryTests {
    
        /**
         * The target object does not implement any interfaces, and in this case, you want to use CGLIB for dynamic proxying.
         */
        @Test
        public void testDelegatingIntroductionInterceptorWithoutInterface() {
            People peo = new People();
            ProxyFactory pf = new ProxyFactory();
            DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor((Developer) () -> System.out.println("Coding"));
            pf.addAdvice(dii);
            pf.setTarget(peo);
    
            Object proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isCglibProxy(proxy));
            Assertions.assertTrue(proxy instanceof People);
            Assertions.assertTrue(proxy instanceof Developer);
    
            People people = (People) proxy;
            Assertions.assertDoesNotThrow(people::eat);
    
            Developer developer = (Developer) proxy;
            Assertions.assertDoesNotThrow(developer::code);
        }
    
        /**
         * The target object implements the Teacher interface, and in this case, you want to use JDK for dynamic proxying
         */
        @Test
        public void testDelegatingIntroductionInterceptorWithInterface() {
            Teacher teacher = () -> System.out.println("teach");
            ProxyFactory pf = new ProxyFactory();
            DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor((Developer) () -> System.out.println("Coding"));
            pf.addAdvice(dii);
            pf.addInterface(Teacher.class);
            pf.setTarget(teacher);
    
            Object proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isJdkDynamicProxy(proxy));
            Assertions.assertTrue(proxy instanceof Teacher);
            Assertions.assertTrue(proxy instanceof Developer);
    
            Teacher teacher1 = (Teacher) proxy;
            Assertions.assertDoesNotThrow(teacher1::teach);
    
            Developer developer = (Developer) proxy;
            Assertions.assertDoesNotThrow(developer::code);
        }
    
        /**
         * The target object does not implement any interfaces, and in this case, you want to use CGLIB for dynamic proxying.
         */
        @Test
        public void testDelegatePerTargetObjectIntroductionInterceptorWithoutInterface() {
            People peo = new People();
            ProxyFactory pf = new ProxyFactory();
            DelegatePerTargetObjectIntroductionInterceptor dii = new DelegatePerTargetObjectIntroductionInterceptor(DeveloperImpl.class, Developer.class);
            pf.addAdvice(dii);
            pf.setTarget(peo);
    
            Object proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isCglibProxy(proxy));
            Assertions.assertTrue(proxy instanceof People);
            Assertions.assertTrue(proxy instanceof Developer);
    
            People people = (People) proxy;
            Assertions.assertDoesNotThrow(people::eat);
    
            Developer developer = (Developer) proxy;
            Assertions.assertDoesNotThrow(developer::code);
        }
    
        /**
         * The target object implements the Teacher interface, and in this case, you want to use JDK for dynamic proxying
         */
        @Test
        public void testDelegatePerTargetObjectIntroductionInterceptorWithInterface() {
            Teacher teacher = () -> System.out.println("teach");
            ProxyFactory pf = new ProxyFactory();
            DelegatePerTargetObjectIntroductionInterceptor dii = new DelegatePerTargetObjectIntroductionInterceptor(DeveloperImpl.class, Developer.class);
            pf.addAdvice(dii);
            pf.addInterface(Teacher.class);
            pf.setTarget(teacher);
    
            Object proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isJdkDynamicProxy(proxy));
            Assertions.assertTrue(proxy instanceof Teacher);
            Assertions.assertTrue(proxy instanceof Developer);
    
            Teacher teacher1 = (Teacher) proxy;
            Assertions.assertDoesNotThrow(teacher1::teach);
    
            Developer developer = (Developer) proxy;
            Assertions.assertDoesNotThrow(developer::code);
        }
    
        /**
         * The target object does not implement any interfaces, so it is necessary to use CGLIB for proxying
         */
        @Test
        public void testProxyFactoryWithoutInterface() {
            People people = new People();
            ProxyFactory pf = new ProxyFactory();
            pf.setTarget(people);
            Object proxy = pf.getProxy();
    
            Assertions.assertTrue(AopUtils.isCglibProxy(proxy));
            Assertions.assertTrue(proxy instanceof People);
            Assertions.assertDoesNotThrow(((People)proxy)::eat);
    
            pf.addInterface(Teacher.class);
            proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isCglibProxy(proxy));
            Assertions.assertTrue(proxy instanceof Teacher);
            Assertions.assertTrue(proxy instanceof People);
            Assertions.assertDoesNotThrow(((People)proxy)::eat);
        }
    
        /**
         * When the target object implements the Teacher interface
         * but we have not explicitly called the addInterface method,
         * we expect to use CGLIB; however, after calling it, we expect to use JDK
         */
        @Test
        public void testProxyFactoryWithInterface() {
            Teacher teacher = () -> System.out.println("teach");
            ProxyFactory pf = new ProxyFactory();
            pf.setTarget(teacher);
            Object proxy = pf.getProxy();
    
            Assertions.assertTrue(AopUtils.isCglibProxy(proxy));
            Assertions.assertTrue(proxy instanceof Teacher);
            Assertions.assertDoesNotThrow(((Teacher)proxy)::teach);
    
            pf.addInterface(Teacher.class);
            proxy = pf.getProxy();
            Assertions.assertTrue(AopUtils.isJdkDynamicProxy(proxy));
            Assertions.assertTrue(proxy instanceof Teacher);
            Assertions.assertDoesNotThrow(((Teacher)proxy)::teach);
        }
    
        public static class People {
            void eat() {
                System.out.println("eat");
            }
        }
    
        public interface Teacher {
            void teach();
        }
    
        public interface Developer {
            void code();
        }
    
        public static class DeveloperImpl implements Developer {
            @Override
            public void code() {
                System.out.println("Coding");
            }
        }
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173

    问题更正前,上面六个测试用例只能通过三个,具体结果如下:

    success:
    testDelegatingIntroductionInterceptorWithInterface  
    testDelegatePerTargetObjectIntroductionInterceptorWithInterface
    testProxyFactoryWithInterface
    
    failure:
    testDelegatingIntroductionInterceptorWithoutInterface
    testDelegatePerTargetObjectIntroductionInterceptorWithoutInterface
    testProxyFactoryWithoutInterface
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    问题更正后,所有测试用例都正常通过

  • 相关阅读:
    常用的20个计算机视觉开源数据集总结
    Java设计模式之迭代器模式
    【C++编程入门】2022.8.3日讲给光明社区的小朋友
    深入了解Java 8 新特性:Stream流的实践应用(二)
    火狐浏览器翻译页面功能如何设置
    后端关卡18 了解JDBC
    如何使用SVN或者Git回滚代码,你必须知道的三种场景
    [iOS]-pthread、NSThread
    conky配置
    Linux服务器查看CPU相关信息
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/133242873