分析java 中AspectJ切面执行两次的原因

分析java 中AspectJ切面执行两次的原因

背景

转眼之间,发现博客已经将近半年没更新了,甚是惭愧。话不多说,正如标题所言,最近在使用AspectJ的时候,发现拦截器(AOP切面)执行了两次了。我们知道,AspectJ是AOP的一种解决方案,本质上是通过代理类在目标方法执行通知(Advice),然后由代理类再去调用目标方法。所以,从这点讲,拦截器应该只会执行一次。但是在测试的时候发现拦截器执行了两次。

问题重现

既然问题已经明了,那么可以通过代码简单重现这个问题,从而更深层次分析到底是什么原因导致的。

定义一个注解:

package com.rhwayfun.aspect;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface StatsService {
}

为该注解定义切面:

package com.rhwayfun.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
public class StatsServiceInterceptor {

  private static Logger log = LoggerFactory.getLogger(StatsServiceInterceptor.class);

  @Around("@annotation(StatsService)")
  public Object invoke(ProceedingJoinPoint pjp) {
    try {
      log.info("before invoke target.");
      return pjp.proceed();
    } catch (Throwable e) {
      log.error("invoke occurs error:", e);
      return null;
    } finally {
      log.info("after invoke target.");
    }
  }

}

方法测试:

package com.rhwayfun;

import com.rhwayfun.aspect.StatsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;

public class AspectTest {

  private static Logger log = LoggerFactory.getLogger(AspectTest.class);

  public static void main(String[] args) {
    AspectTest.print();
  }

  @StatsService
  public static void print(){
    log.info("Now: {}", LocalDateTime.now());
  }
}

输出结果:

debug分析

由于是静态织入,所以可以通过反编译工具查看编译后的文件,如下:

public class AspectTest
{
  private static Logger log;
  private static final /* synthetic */ JoinPoint$StaticPart ajc$tjp_0;
  private static final /* synthetic */ JoinPoint$StaticPart ajc$tjp_1;

  public static void main(final String[] args) {
    StatsServiceInterceptor.aspectOf().invoke(((AroundClosure)new AspectTest$AjcClosure1(new Object[] { Factory.makeJP(AspectTest.ajc$tjp_0, (Object)null, (Object)null) })).linkClosureAndJoinPoint(0));
  }

  @StatsService
  public static void print() {
    StatsServiceInterceptor.aspectOf().invoke(((AroundClosure)new AspectTest$AjcClosure3(new Object[] { Factory.makeJP(AspectTest.ajc$tjp_1, (Object)null, (Object)null) })).linkClosureAndJoinPoint(65536));
  }

  static {
    ajc$preClinit();
    AspectTest.log = LoggerFactory.getLogger((Class)AspectTest.class);
  }

  private static /* synthetic */ void ajc$preClinit() {
    final Factory factory = new Factory("AspectTest.java", (Class)AspectTest.class);
    ajc$tjp_0 = factory.makeSJP("method-call", (Signature)factory.makeMethodSig("9", "print", "com.rhwayfun.AspectTest", "", "", "", "void"), 17);
    ajc$tjp_1 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("9", "print", "com.rhwayfun.AspectTest", "", "", "", "void"), 22);
  }
}

请注意两个连接点:ajc$tjp_0和ajc$tjp_1,这两个连接点是产生两次调用的关键,问题注解明明是加上print()方法上的,为什么main()方法也被注入了通知呢?正因为main()方法也织入了通知,所以就形成了A call B, B call print()的调用链,有两次method-call,一次method-execution,method-execution才是我们的目标方法print(),所以我们才看到了两次输出。

method-call和method-execution都是连接点ProceedingJoinPoint的kind属性

其实,这属于Ajc编译器的一个Bug,详见Ajc-bug

所以,到这一步,问题就很清晰了,因为Ajc编辑器的bug,导致了在main方法中也织入了通知,所以在执行的时候,输出了两次日志。

解决方法

方案一

因为两次调用的kind属性不一样,所以可以通过kind属性来判断时候调用切面。这样显得不优雅,而且如果切面有更多的逻辑的话,需要加各种if-else的判断,所以不推荐。

方法二

更优雅的方案是修改@Around("@annotation(StatsService)")的逻辑,改为@Around("execution(* *(..)) && @annotation(StatsService)")。

重新运行上面的测试类,结果如下:

如有疑问请留言或者到本站社区交流讨论,感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • JAVA 枚举单例模式及源码分析的实例详解

    JAVA 枚举单例模式及源码分析的实例详解 单例模式的实现有很多种,网上也分析了如今实现单利模式最好用枚举,好处不外乎三点: 1.线程安全 2.不会因为序列化而产生新实例 3.防止反射攻击但是貌似没有一篇文章解释ENUM单例如何实现了上述三点,请高手解释一下这三点: 关于第一点线程安全,从反编译后的类源码中可以看出也是通过类加载机制保证的,应该是这样吧(解决) 关于第二点序列化问题,有一篇文章说枚举类自己实现了readResolve()方法,所以抗序列化,这个方法是当前类自己实现的(解决) 关于

  • 简述Java编程语言中的逃逸分析

    大家一般认为new出来的对象都是被分配在堆上,但这并不是完全正确,通过对Java对象分配过程分析,我们发现对象除了可以被分配在堆上,还可以在栈或TLAB中分配空间.而栈上分配对象的技术基础是逃逸分析和标量替换,本文主要介绍下逃逸分析. 1.逃逸分析的定义 逃逸分析:是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法. 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上. Java在Java SE

  • java注解的全面分析

    全面解析java注解 Java中的常见注解 a.JDK中的注解 @Override 覆盖父类或者父接口的方法     @Deprecated 表示方法已经过时     @SuppressWarnings("deprecation") 忽略方法过时警告 b.常见的第三方注解 例如Spring中的@Autowired(自动注入) 注解的分类 a.按照运行机制分 1.源码注解         注解只在源码中存在,编译成class文件就不存在了 2.编译时注解         注解在源码和cl

  • 用Java打印九九除法表代码分析 原创

    可能你已经学会了如何在Java中用循环语句打印九九乘法表,但学习是一个需要能够举一反三的事情,接下来,我们就来看看如何使用for循环语句打印九九除法表. 代码(九九除法表): public class TestNineNine { public static void main(String[] args) { for(int b=1;b<=9;b++) { for(int a=1;a<=9;a++) { int c = a*b; System.out.print(c+"/"

  • Java正则验证正整数的方法分析【测试可用】

    本文实例讲述了Java正则验证正整数的方法.分享给大家供大家参考,具体如下: package des; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Num { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Num p=new Num(); S

  • Java编程中的检查型异常与非检查型异常分析

    对于因为编程错误而导致的异常,或者是不能期望程序捕获的异常(解除引用一个空指针,数组越界,除零,等等),为了使开发人员免于处理这些异常,一些异常被命名为非检查型异常(即那些继承自 RuntimeException 的异常)并且不需要进行声明. Checked Exception和Unchecked Exception的几点不同之处         方法签名是否需要声明exception,调用该方法时是否需要捕获exception,exception产生的时候JVM控制程序的状态. Sun 的"T

  • 分析java 中AspectJ切面执行两次的原因

    分析java 中AspectJ切面执行两次的原因 背景 转眼之间,发现博客已经将近半年没更新了,甚是惭愧.话不多说,正如标题所言,最近在使用AspectJ的时候,发现拦截器(AOP切面)执行了两次了.我们知道,AspectJ是AOP的一种解决方案,本质上是通过代理类在目标方法执行通知(Advice),然后由代理类再去调用目标方法.所以,从这点讲,拦截器应该只会执行一次.但是在测试的时候发现拦截器执行了两次. 问题重现 既然问题已经明了,那么可以通过代码简单重现这个问题,从而更深层次分析到底是什么

  • 分析Java中为什么String不可变

    常量池 Java中我们创建String对象有两种基本方法. String str1 = "zxhtom"; String str2 = new String("zxhtom"); 上面两种方式我们创建了两个String变量 . 但是第一种通过双引号创建的zxhtom这个对象我们称之为常量 . 在JVM中是存储在一块叫[常量池]中的.而第二种str2是我们称之为普通变量.new一次就在JVM中开辟一块内存. [常量池]的作用就是复用,当同样的内容再次被通过常量方式创建

  • 分析java中全面的单例模式多种实现方式

    一.单例模式的思想 想整理一些 java 并发相关的知识,不知道从哪开始,想起了单例模式中要考虑的线程安全,就从单例模式开始吧.以前写过单例模式,这里再重新汇总补充整理一下,单例模式的多种实现. 单例模式的主要思想是: 将构造方法私有化( 声明为 private ),这样外界不能随意 new 出新的实例对象: 声明一个私有的静态的实例对象,供外界使用: 提供一个公开的方法,让外界获得该类的实例对象 这种说法看上去没错,但也好像不太准确.其实,就算外界能随意 new 出新的实例对象,但只要我们保证

  • 分析Java中Map的遍历性能问题

    一.引言 我们知道java HashMap的扩容是有成本的,为了减少扩容的次数和成本,可以给HashMap设置初始容量大小,如下所示: HashMap<string, integer=""> map0 = new HashMap<string, integer="">(100000); 但是在实际使用的过程中,发现性能不但没有提升,反而显著下降了!代码里对HashMap的操作也只有遍历了,看来是遍历出了问题,于是做了一番测试,得到如下结果:

  • 分析Java中的类加载问题

    目录 一.Java类的加载顺序 二.类加载过程 三.被动引用中和类静态初始化的关系 四.类加载器双亲委派 一.Java类的加载顺序 引用1个网上的经典例子,并做稍许改动,以便大家更好地理解. public class Animal { private int i = test(); private static int j = method(); static { System.out.println("a"); } Animal(){ System.out.println("

  • java中for循环执行的顺序图文详析

    for循环基础 for循环是最灵活也是最常用的循环结构,表达式一般如下: for(条件表达式1:条件表达式2:条件表达式3){ 语句块: } 接下来详细介绍Java for循环执行顺序的相关内容,先看看一道面试题, 来自小米笔试 static boolean foo(charc) { System.out.print(c); return true; } public static void main(String[] args) { int i =0; for(foo('B');foo('A'

  • java 中ArrayList迭代的两种实现方法

    java 中ArrayList迭代的两种实现方法 Iterator与for语句的结合来实现,代码很简单,大家参考下. 实现代码: package cn.us; import java.util.ArrayList; import java.util.Iterator; //ArrayList迭代的两种方法 //Iterator与for语句的结合 public class Test1 { public static void main(String[] args) { ArrayList arra

  • 浅谈Java中实现深拷贝的两种方式—clone() & Serialized

    clone() 方法麻烦一些,需要将所有涉及到的类实现声明式接口 Cloneable,并覆盖Object类中的clone()方法,并设置作用域为public(这是为了其他类可以使用到该clone方法). 序列化的方法简单,需要将所有涉及到的类实现接口Serializable package b1ch06.clone; import java.io.Serializable; class Car implements Cloneable, Serializable { private String

  • 浅谈Java中浮点型数据保留两位小数的四种方法

    目录 一.String类的方式 二.DecimalFormat类 三.BigDecimal类进行数据处理 四.NumberFormat类进行数据处理 总结一下 今天在进行开发的过程中遇到了一个小问题,是关于如何将double类型的数据保留两位小数.突然发现这方面有一点欠缺,就来总结一下. 一.String类的方式 该方式是是使用String的format()方法来实现的,该方法的作用就是规范数据的格式,第一个参数传入一个字符串来表示输出的数据格式,如保留两位小数就使用"%.2f",第二

  • java中Servlet Cookie取不到值原因解决办法

    java中Servlet Cookie取不到值原因解决办法 现象: 在测试带Cookie的HTTP请求时发现,服务端用request.getHeader("cookie")可以去到值; 但是用request.getCookies()却不行 Cookie mycookies[] = request.getCookies(); 中永远获取不到uid的值:(80端口号除外) 原因: 查了浏览器的具体cookie值,发现 http://localhost:8080/ 访问的时候,cookie值

随机推荐