自定义注解不生效原因解析及解决方法
背景:
项目中,自己基于spring AOP实现了一套java缓存注解。但是最近出现一种情况:缓存竟然没有生效,大量请求被击穿到db层,导致db压力过大。现在我们看一下具体代码情形(代码为伪代码,只是为了说明一下具体情况)。
interface A {
int method1(..);
int method2(..);
... ...
}
class AImpl implements A {
@Override
@CacheMM(second=600) //这里的@CacheMM就是我实现的自定义缓存注解
public int method1(..) {
... ...
method2(..);
... ...
}
@Override
@CacheMM(second=600)
public int method2(..) {
... ...
}
}
如上代码,当调用method1时,发现method2注解并没有生效。
分析:
这是为什么呢?别急,我们带着这个问题去看了一下注解的实现类。(这里就不贴缓存注解的实现代码了)我的自定义注解是直接extends AbstractBeanFactoryPointcutAdvisor类然后实现其中的getPointcut() 和 getAdvice() 实现的。(其实这里可以直接使用aop环绕通知的,原理都差不多,我是为了熟悉源码才这样写的)。
接下来,我们继续往下分析,我们都知道基于spring aop实现的注解,在spring 中,如果有aop实现,那么容器注入的是该类的代理类,这里的代理类是aop 动态代理生成的代理类。Spring aop 的动态代理有两种:一种是jdk的动态代理,一种是基于CGLIB的。这两个的区别我就不多说了,如果你的业务类是基于接口实现的,则使用jdk动态代理,否则使用CGLIB动态代理。 我这里使用的是接口实现,所以我们就顺着思路去看一下jdk动态代理的具体实现。
上边的业务代码类我已经贴出。而需要生成代理对象(proxy),分成两步:
- 生成代理对象需要建立代理对象(proxy)和真实对象(AImpl)的代理关系
- 实现代理方法
在JDK动态代理中需要实现接口:java.lang.reflect.InvocationHandler.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class AProxy implements InvocationHandler
{
private Object target;
/**
* 生成代理对象,并和真实服务对象绑定.
* @param target 真实服务对象
* @return 代理对象 */
public Object bind(Object target)
{
this.target = target;
//生成代理对象,并绑定.
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), //类的加载器
target.getClass().getInterfaces(), //对象的接口,明确代理对象挂在哪些接口下
this);//指明代理类,this代表用当前类对象,那么就要求其实现InvocationHandler接口的invoke方法
return proxy;
}
/**
* 当生成代理对象时,第三个指定使用AProxy进行代理时,代理对象调用的方法就会进入这个方法。
* @param proxy 代理对象
* @param method 被调用的方法
* @param args 方法参数
* @return 代理方法返回。
* @throws Throwable 异常处理 */
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
System.err.println("反射真实对象方法前");
Object obj = method.invoke(target, args);//相当于AImpl类中对应方法调用.
System.err.println("反射真实对象方法后");
return obj;
}
}
代码中,Object obj = method.invoke(target,args) 通过反射调度真实对象的方法,这个很重要。我们知道其实虽然aop是通过代理对象去实现一些附加的操作的,但是真正的类方法调用还是通过反射调用真实对象的。这个时候,我们回头看一下问题,我们AImpl中有两个方法,其中method2是在method1内部调用的。当调用method1时,spring内部其实调用的是代理类AProxy类的invoke,这个时候在执行真实对象方法钱去执行method1中的一些附加操作。然后,在通过反射进入对应AImpl类中调用method1方法。注意,这个时候,已经不在代理对象中操作了,由于method2的调用是在method1内部调用的,所以在这里实际调用method2的是真实对象,并不是代理对象。 所以,就导致method2上的缓存注解没有生效。
解决:
好了,现在知道问题的原因后(动态代理的坑啊,内部调用不走代理类,所以实现的附加操作肯定不会执行了),我们来针对性的解决。我们现在知道这个其实是因为实际执行的不是代理类而导致的,那我们解决的思路就想办法让method2的调用走代理类就可以了。(就是这么简单)
AProxy类我们是可以在spring容器中得到的。下面是修改后的解决方案:
method1(..) {
... ...
// 如果希望调用的内部方法也被拦截,那么必须用过上下文获取代理对象执行调用,而不能直接内部调用,否则无法拦截
if(null != AopContext.currentProxy()){
AopContext.currentProxy().method2();
}else{
method2();
}
}
这里的AopContext.currentProxy() 拿到的实际就是代理对象了,这样通过代理对象去调用method2肯定就没有问题了。
还有一种解决方法就是不使用 动态代理织入,使用aspectJ织入,aspectJ直接在源类上进行字节码的插入,而不是以代理的方式进行。
这里可以参考一下
AspectJ 编译时织入(Compile Time Weaving, CTW)
因为这样改动比较大,所以目前我还是采用第一种方案解决问题了。至此,问题得到解决。
总结:
结合Spring aop动态代理的实现原理,提供两种动态代理:JDK代理和CGLIB代理
JDK代理只能对实现了接口的类生成代理,而不能针对类;
CGLIB是针对类实现代理的,主要对指定的类生成一个子类,并覆盖其中的方法,
因为是继承,所以不能使用final来修饰类或方法。所以该类或方法最好不要声明成final
更加详细的解释可以参考这篇博文 哪些方法不能实施Spring AOP事务