【小家Spring】详解Spring AOP的底层代理JdkDynamicAopProxy和ObjenesisCglibAopProxy的源码分析(介绍CGLIB使用中的坑)(下)

简介: 【小家Spring】详解Spring AOP的底层代理JdkDynamicAopProxy和ObjenesisCglibAopProxy的源码分析(介绍CGLIB使用中的坑)(下)

关于代理出现的:类型转换常见错误


java.lang.ClassCastException: com.sun.proxy.$Proxy7 cannot be cast to XXXXX

这一看就知道是JDK的代理对象不能转换为xxx对象。这是由JDK动态代理可能导致的。比如:


public class Main {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory(new Demo());
        proxyFactory.addAdvice((MethodBeforeAdvice) (method, args1, target) -> {
                    System.out.println("你被拦截了:方法名为:" + method.getName() + " 参数为--" + Arrays.asList(args1));
                }
        );
        // 如果使用的是JDK的动态代理,这里若实现实现类接收,就报错:java.lang.ClassCastException: com.fsx.maintest.$Proxy0 cannot be cast to com.fsx.maintest.Demo
        //Demo demo = (Demo) proxyFactory.getProxy();
        DemoInter demo = (DemoInter) proxyFactory.getProxy();
        //你被拦截了:方法名为:hello 参数为--[]
        //this demo show
        demo.hello();
    }
}
// 不要再实现接口,就会用CGLIB去代理
class Demo implements DemoInter{
    @Override
    public void hello() {
        System.out.println("this demo show");
    }
}
interface DemoInter{
    void hello();
}


这个例子就好比我们经常使用@Autowired自动注入的时候,建议注入接口(因为注入实现类且是JDK动态代理的话,是会出现类似错误的)

    @Autowired
    private HelloService helloService; // 建议注入接口而不是实现类
    //@Autowired
    //private HelloServiceImpl helloServiceImpl; //不建议直接注入实现类


上面例子,若采用的是CGLIB动态代理,不管是用接口还是实现类,都不会有问题。因此可见总体来说,CGLIb还是更加强大一些的

CGLIB动态代理 字段为null 导致的坑


先不说别的,看下面例子:(一定让你摸不着头脑)


public class Main {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory(new Demo());
        proxyFactory.addAdvice((MethodBeforeAdvice) (method, args1, target) -> {
                    System.out.println("你被拦截了:方法名为:" + method.getName() + " 参数为--" + Arrays.asList(args1));
                }
        );
        Demo demo = (Demo) proxyFactory.getProxy();
        //你被拦截了:方法名为:setAge 参数为--[10]
        demo.setAge(10);
        //你被拦截了:方法名为:getAge 参数为--[]
        System.out.println(demo.getAge()); //10
        System.out.println(demo.age); //null 对你没看错,这里是null
        System.out.println(demo.findAge()); //null 对你没看错,这里是null
    }
}
// 不要再实现接口,就会用CGLIB去代理
class Demo {
    public Integer age;
    // 此处用final修饰了  CGLIB也不会代理此方法了
    public final Integer findAge() {
        return age;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
}


what?what a fuck?

是不是此时你有此种感觉:不再相信java了。明明我都用set把age赋值了,为毛我拿出来却仍旧为null呢?(但通过getAge()方法获取正常)


如何解决?

最直接也是最推荐的方案,就是遵从上面所说得规范,小心谨慎为事。

另外,本处我也提供一个简单的工具,配置上就能就绝get/set的问题。(但知其然必须知其所以然,才能更好的解决一类问题,而不是这一个问题)。仅供参考:

@Aspect
@Order(Integer.MIN_VALUE)
public class SetterAspect {
    // 切所有的set方法
    @After(value = "execution(* *.set*(*)) && args(value)", argNames = "value")
    public void after(JoinPoint jp, Object value) {
        Object proxy = jp.getThis();
        // 拿到目标对象
        Object target = jp.getTarget();
        if (AopUtils.isAopProxy(proxy)) {//只有代理对象才需要处理
            try {
                Class<?> proxyClass = proxy.getClass();
                Class<?> targetClass = target.getClass();
                String methodName = jp.getSignature().getName();
                Method m = BeanUtils.findDeclaredMethod(proxyClass, methodName, new Class[]{value.getClass()});
                PropertyDescriptor descriptor = BeanUtils.findPropertyForMethod(m);
                String propName = descriptor.getName();
                // 调用目标对象的set方法
                Field f = targetClass.getClass().getDeclaredField(propName);
                if (f != null) {
                    f.setAccessible(true);
                    f.set(proxy, value);
                }
            } catch (Exception e) {
                e.printStackTrace();//记录好异常进行处理  
            }
        }
    }
}  


解释:


若了解CGLIB的原理,甚至看过它生成的代理类的源码的话,此原因就可一句道破。


原理简述:假设有个类A,会在字节码的层面上动态生成一个类B并加载进JVM里面。B继承自A同时又有对A的引用,B会重写所有的A类里面的非Final、非private方法,从而可以在目标方法调用前后进行对应的增强了。


本文中:demo.setAge(10);执行的是代理对象的setAge()方法,所以set进去的值是给了代理对象的,目标对象仍然我null。而我们findAge()方法因为我标注了final,因此不能被CGLIB代理,所以只能从目标对象里拿值。因此它也只能拿到null


而我们调用的getAge()方法它被代理过,所以他能拿到正确的值:10。


备注:若采用JDK动态代理不回存在此种现象,因为接口代理的都是方法。

另外:建议若代理的时候,final慎用。同时大多数情况下字段还是private掉吧,然后暴露出get方法给外部调用比较好。

总结


JdkDynamicAopProxy 入口方法是动态代理的 invoke() 方法,CGLIB 使用的是 DynamicAdvisedInterceptor.intercept()方法


JdkDynamicAopProxy使用的MethodInvocation 是: ReflectiveMethodInvocation 子类,

CGLIB 使用的是CglibMethodInvocation


它俩都是ProxyMethodInvocation接口的实现类。并且CglibMethodInvocation是继承自ReflectiveMethodInvocation的


CGLib更适合代理不需要频繁实例化的类,而Spring绝大多数Bean都是单例的,因此在Spring AOP中我极力推荐使用CGLib,它的功能更强大些

相关文章
|
16天前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
27 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
7天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
10 1
|
8天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
15 1
|
1月前
|
缓存 JavaScript Java
Spring之FactoryBean的处理底层源码分析
本文介绍了Spring框架中FactoryBean的重要作用及其使用方法。通过一个简单的示例展示了如何通过FactoryBean返回一个User对象,并解释了在调用`getBean()`方法时,传入名称前添加`&`符号会改变返回对象类型的原因。进一步深入源码分析,详细说明了`getBean()`方法内部对FactoryBean的处理逻辑,解释了为何添加`&`符号会导致不同的行为。最后,通过具体代码片段展示了这一过程的关键步骤。
Spring之FactoryBean的处理底层源码分析
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
4天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
10 0
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析-代理设计模式(二)
spring源码设计模式分析-代理设计模式(二)
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
23天前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
125 2
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决