关于代理出现的:类型转换常见错误
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,它的功能更强大些