Spring AOP 注解方式实现的一些“坑”

1. 首先仔细检查一下,Aspect类是不是少了@Component注解(这一点很重要!),只有一个@Aspect注解是不行的。

2. 拦截指定注解的规则怎么写?如拦截所有带了RequestMapping的方法

首先声明一个写入点来匹配所有要拦截的方法名
假如拦截所有方法

@Pointcut("execution(public * *(..))")
private void aspectMethod(){}

然后配置通知方法

@Before(value = "aspectMethod() && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public String doSomething() {}

3. 拦截指定参数名的规则该怎么写?如拦截所有方法参数中有int page的方法

参数拦截可以用args或@args,但是匹配的时候跟顺序和数量有关!
如果只有page一个参数

@Before(value = "aspectMethod()  &&  args(page)")
public String doSomething(int page) {
}

说明:
参数的配置可以用占位符*和..。
*的意思是任意类型任意名称的一个参数
..的意思是任意类型,任意多个参数,并且只能放到args的后面。
通知方法的参数需要跟args中的固定参数page一致!
如果要获取拦截方法的所有参数,可以用JoinPoint或ProceedingJoinPoint,两者的区别:
JoinPoint是父类,提供获取拦截方法的信息的功能,如所有参数:jp.getArgs()
ProceedingJoinPoint是子类,只能用在@Around中,除了提供JoinPoint的所有功能外,还能提供方法的运行pjp.proceed()和pjp.proceed(args)功能。
这里注意,*用在返回值和方法参数上并不是一个或多个,而是指任意一个,如用在返回值上,表示任意类型的返回值;用在方法的参数上,指任意类型的一个参数。官方也给出了解释,在这里可能会有很多坑,大家可以注意一下!

4. @Transactional @Async等注解不起作用

为什么会出现这些情况呢?因为这些注解的功能实际上都是Spring AOP实现的,而其实现原理是通过代理实现的。
JDK动态代理
以一个简单的例子理解一下JDK动态代理的基本原理:

//目标类接口
public interface JDKProxyTestService {
  void run();
}

//目标类
public class JDKProxyTestServiceImpl implements JDKProxyTestService {
  public void run(){
    System.out.println("do something...");
  }
}

//代理类
public class TestJDKProxy implements InvocationHandler {

  private Object targetObject; //代理目标对象

  //构造代理对象
  public Object newProxy(Object targetObject) {
    this.targetObject = targetObject;
    return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
        targetObject.getClass().getInterfaces(), this);
  }

  //利用反射,在原逻辑上进行逻辑增强
  public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
    //模拟事务开始
    assumeBeginTransaction();
    //原执行逻辑
    Object ret = method.invoke(targetObject, args);
    //模拟事务提交
    assumeCommitTransaction();
    return ret;
  }

  private void assumeBeginTransaction() {
    System.out.println("模拟事务开始...");
  }

  private void assumeCommitTransaction() {
    System.out.println("模拟事务提交...");
  }
}

//测试
public class Test {
  public static void main(String[] args) {
    TestJDKProxy jdkProxy = new TestJDKProxy();
    JDKProxyTestService proxy = (JDKProxyTestService) jdkProxy.newProxy(new JDKProxyTestServiceImpl());
    proxy.run();
  }
}

上面的例子应该能够清楚的解释JDK动态代理的原理了。它利用反射机制,生成了一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。我们通过代理类对象调用方法时,实际上会先调用其invoke方法,里面再调用原方法。这样我们可以在原方法逻辑的前后统一添加处理逻辑。

Spring还有一种动态代理方式是CGLIB动态代理。它是把代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。虽然处理方式不一样,但是代理的思想都是一致的。

如果被代理的目标对象实现了接口,那么Spring会默认使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。

Spring AOP注解失效及解决

基于以上对于动态代理原理的分析,我们来看以下两个常见的问题:
同一个类中,方法A调用方法B(方法B上加有注解),注解无效,
针对所有的Spring AOP注解,Spring在扫描bean的时候如果发现有此类注解,那么会动态构造一个代理对象。

如果你想要通过类X的对象直接调用其中带注解的A方法,此注解是有效的。因为此时,Spring会判断你将要调用的方法上存在AOP注解,那么会使用类X的代理对象调用A方法。

但是假设类X中的A方法会调用带注解的B方法,而你依然想要通过类X对象调用A方法,那么B方法上的注解是无效的。因为此时Spring判断你调用的A并无注解,所以使用的还是原对象而非代理对象。接下来A再调用B时,在原对象内B方法的注解当然无效了。

解决方法:
最简单的方式当然是可以让方法A和B没有依赖,能够直接通过类X的对象调用B方法。
但是很多时候可能我们的逻辑拆成这样写并不好,那么就还有一种方法:想办法手动拿到代理对象。AopContext类有一个currentProxy()方法,能够直接拿到当前类的代理对象。那么以上的例子,就可以这样解决:

// 在A方法内部调用B方法
// 1.直接调用B,注解失效。
B()
// 2.拿到代理类对象,再调用B。
((X)AopContext.currentProxy()).B()

等等,突然脑洞大开,既然这样可以,那是不是直接applicationContext.getBean()也可以呢?实验过后得到的结果是可行,而且配置中的expose-proxy也不用设置成true。

5. AOP注解方法里使用@Autowired对象为null

在之前的使用中,出现过在加上注解的方法中,使用其他注入的对象时,发现对象并没有被注入进来,为null。

最终发现,导致这种情况的原因是因为方法为private。因为Spring不管使用的是JDK动态代理还是CGLIB动态代理,一个是针对实现接口的类,一个是通过子类实现。无论是接口还是父类,显然都不能出现private方法,否则子类或实现类都不能覆盖到。

如果方法为private,那么在代理过程中,根本找不到这个方法,引起代理对象创建出现问题,也导致了有的对象没有注入进去。

所以如果方法需要使用AOP注解,请把它设置为非private方法。

参考:
https://www.jianshu.com/p/def4c497571c
https://www.jb51.net/article/137255.htm
https://blog.csdn.net/a1063916463/article/details/53761243

猜你喜欢

转载自www.cnblogs.com/john8169/p/9281628.html