前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个关于IntroductionAdvisor的bug

一个关于IntroductionAdvisor的bug

作者头像
大忽悠爱学习
发布2023-10-11 09:23:28
1720
发布2023-10-11 09:23:28
举报
文章被收录于专栏:c++与qt学习
一个关于IntroductionAdvisor的bug

问题描述

代码语言:javascript
复制
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();
    }
}

运行结果:

代码语言:javascript
复制
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接口集合中去 :

代码语言:javascript
复制
	@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);
		}
	}

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

代码语言:javascript
复制
	private List<Class<?>> interfaces = new ArrayList<>();

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

代码语言:javascript
复制
	@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])));
	}

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


反馈结果

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

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

2023-09-26 Spring官方回复

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

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


解决方案

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

代码语言:javascript
复制
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();
    }
}

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

代码语言:javascript
复制
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();
    }
}

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

代码语言:javascript
复制
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)

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

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

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

代码语言:javascript
复制
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])));
    }
}

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

代码语言:javascript
复制
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();
    }
}

输出结果:

代码语言:javascript
复制
study
interface com.spring.TestMain$Boy
interface org.springframework.aop.SpringProxy
interface org.springframework.aop.framework.Advised
interface org.springframework.cglib.proxy.Factory

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

代码语言:javascript
复制
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();
    }
}

输出结果:

代码语言:javascript
复制
drink
eat
编码

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

补充完整测试用例:

代码语言:javascript
复制
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");
        }
    }
}

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

代码语言:javascript
复制
success:
testDelegatingIntroductionInterceptorWithInterface  
testDelegatePerTargetObjectIntroductionInterceptorWithInterface
testProxyFactoryWithInterface

failure:
testDelegatingIntroductionInterceptorWithoutInterface
testDelegatePerTargetObjectIntroductionInterceptorWithoutInterface
testProxyFactoryWithoutInterface

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个关于IntroductionAdvisor的bug
  • 问题描述
  • 问题原因
  • 反馈结果
  • 解决方案
相关产品与服务
云顾问
云顾问(Tencent Cloud Smart Advisor)是一款提供可视化云架构IDE和多个ITOM领域垂直应用的云上治理平台,以“一个平台,多个应用”为产品理念,依托腾讯云海量运维专家经验,助您打造卓越架构,实现便捷、灵活的一站式云上治理。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档