1

自定义注解不生效原因解析及解决方法

背景:

项目中,自己基于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),分成两步:

  1. 生成代理对象需要建立代理对象(proxy)和真实对象(AImpl)的代理关系
  2. 实现代理方法

在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事务


丶木叶
112 声望5 粉丝

我始终相信比你优秀的人过着比你更努力的生活。