·

老生常谈系列之Aop--AspectJ

老生常谈系列之Aop--AspectJ

这篇文章的目的是大概讲解AspectJ是什么,所以这个文章会花比较长的篇幅去解释一些概念(这对于日常开发来说没一点卵用,但我就是想写),本文主要参考AspectJ官网,所以会有比较多的英文概念,介绍它的一些概念例如Join pointPointcut以及advice等。建议每一句英文原文都用心去体会一下它的描述,官方的永远是最原汁原味的定义。同时理清楚AspectJ和Spring Aop之间的关系,最后有一个简单的纯AspectJ实现切面的例子以及剖析AspectJ的实现原理。

AspectJ简介

首先看一下AspectJ的出处,网上各种说法层出不穷,这里干脆参考一下维基百科的说法。

AspectJ is an aspect-oriented programming (AOP) extension created at PARC for the Java programming language. It is available in Eclipse Foundation open-source projects, both stand-alone and integrated into Eclipse. AspectJ has become a widely used de facto standard for AOP by emphasizing simplicity and usability for end users. It uses Java-like syntax, and included IDE integrations for displaying crosscutting structure since its initial public release in 2001.

大概的意思就是AspectJ是由PARC组织创建的基于Java语言的AOP扩展。后来贡献给了Eclipse,它在 Eclipse Foundation 开源项目中可用,既可独立使用,也可集成到 Eclipse 中。由于AspectJ强调最终用户的简单性和可用性,AspectJ 已成为广泛使用的 AOP 事实标准。 在2001 年首次公开发布,它使用类似Java语法,提供展示横切结构的IDE集成。

英文水平有限,将就着看。根据发布时间来看,这玩意跟Spring不是一回事,也不属于Spring,只是Spring后面实现Aop的时候整合了AspectJ。AspectJ在2001年就公开发布了,而Spring在2004年3月,才发布了里程碑的版本1.0 。由于后面Spring迅速成为了Java开发方面的主流框架,所以AspectJ逐渐就被新手认为是Spring的内容,实际上不是。这段话是为了理清AspectJ和Spring的关系

接下来介绍Join pointpointcutadvice的概念。

Join point

接下来看一下AspectJ的定义,到官网截取一小段定义:

AspectJ extends Java by overlaying a concept of join points onto the existing Java semantics and adding a few new program elements to Java.

翻译一下: AspectJ 通过将连接点的概念覆盖到现有的 Java 语义上并向 Java 添加一些新的程序元素来扩展 Java。这一看十分的生硬,下面来解释一下。Joint point这个概念不是AspectJ特有的,而是在计算机中早就有这个概念,计算机中的概念为:

In computer science, a join point is a point in the control flow of a program where the control flow can arrive via two different paths. In particular, it's a basic block that has more than one predecessor.

翻译一下:在计算机科学中,连接点是程序控制流中的一个点,其中控制流可以通过两条不同的路径到达。 特别是,它是一个具有多个前身的基本块。

当然这是计算机的概念,AspectJ自己定义的Join point还不太一样,下面看看AspectJ中的Join point的定义:

A join point is a well-defined point in the execution of a program.

翻译一下: 连接点是程序执行中明确定义的点 。

这是AspectJ的定义,跟上面的是有些不同的,但是可以简单的理解为,就是程序中执行过程中的一个点。AspectJ的Join point不是随便定义的,它规定了在哪些地方才是Join point,当然这个定义是在AspectJ里面的,不是广义的计算机程序的Join point。看下AspectJ的描述:

While aspects define types that crosscut, the AspectJ system does not allow completely arbitrary crosscutting. Rather, aspects define types that cut across principled points in a program's execution. These principled points are called join points.

AspectJ就是说,切面定义了横切逻辑,但是也不是随便切的,需要符合它给的Join point定义。AspectJ能够定义的Join point如下,这是AspectJ支持的,是不是很多,那为啥Spring里用到的很少呢?因为实际上我们的业务用的最多的就是方法级别的切面,本着二八原则,这里已经满足了大部分业务需求。尽管原生的AspectJ功能繁多,然而Spring只支持了方法级别的Join point。

Join Point Current Object Target Object Arguments
Method Call executing object* target object** method arguments
Method Execution executing object* executing object* method arguments
Constructor Call executing object* None constructor arguments
Constructor Execution executing object executing object constructor arguments
Static initializer execution None None None
Object pre-initialization None None constructor arguments
Object initialization executing object executing object constructor arguments
Field reference executing object* target object** None
Field assignment executing object* target object** assigned value
Handler execution executing object* executing object* caught exception
Advice execution executing aspect executing aspect advice arguments

Current Object是指当前代码执行的对象,Target Object是切点要执行的目标对象。这个点确实不太容易明白,可以看下面的两个例子This() vs Target() aspectjAspectJ: this() vs. target()。如果拎不清,在方法执行execution的场景下,在AspectJ里可以近似的认为这两个是同一个东西,但是在Spring Aop里面是不一样的,Spring Aop是基于代理的,this指向的是代理对象,而target指向的是被代理的对象,摘抄Spring官网原文如下。

Because Spring AOP limits matching to only method execution join points, the preceding discussion of the pointcut designators gives a narrower definition than you can find in the AspectJ programming guide. In addition, AspectJ itself has type-based semantics and, at an execution join point, both this and target refer to the same object: the object executing the method. Spring AOP is a proxy-based system and differentiates between the proxy object itself (which is bound to this) and the target object behind the proxy (which is bound to target).

看一下这表头的定义:

Each join point potentially has three pieces of state associated with it: the currently executing object, the target object, and an object array of arguments. These are exposed by the three state-exposing pointcuts, this, target, and args, respectively.

Informally, the currently executing object is the object that a this expression would pick out at the join point. The target object is where control or attention is transferred to by the join point. The arguments are those values passed for that transfer of control or attention.

Pointcut

截取AspectJ官网对pointcut的定义:

A pointcut is a program element that picks out join points and exposes data from the execution context of those join points. Pointcuts are used primarily by advice. They can be composed with boolean operators to build up other pointcuts.

翻译一下:切入点是一个程序元素,它挑选连接点并从这些连接点的执行上下文中暴露数据。 切入点主要用于通知。 它们可以与布尔运算符组合以构建其他切入点。

实不相瞒,A pointcut is a program element这一句我不知道怎么翻译,这是Google翻译的。但是大概的意思就是pointcut是用来挑选出你想要的join point,然后用于下一步的advice。怎么理解挑选join point呢?join point可以认为是程序中客观存在的点,它不需要人为再去定义。而怎么去找出想要的join point 则需要人为定义,例如:

@Pointcut("execution(* *.sayHello())")

这个pointcut的就是挑选出了所有sayHello()方法执行时的join point。这里解决了3w中的where问题,即在哪里执行。找出了地点,接下来看什么when时候执行,这就是advice

advice

截取一下AspectJ官网对advice的定义:

Advice defines crosscutting behavior. It is defined in terms of pointcuts. The code of a piece of advice runs at every join point picked out by its pointcut. Exactly how the code runs depends on the kind of advice.

advice定义了横切的逻辑,它是根据切点定义的。advice的代码会在当前pointcut选定的join point处执行。AspectJ定义了三种advice,分别为beforeafteraround

before的理解很清晰,就是在方法执行前。

  aspect A {
      
      pointcut publicCall(): call(public Object *(..));
      
      before(): publicCall() {
	  	System.out.println("before publicCall...");
      }
  }

after的理解也很清晰,就是在方法执行后。

after可以分为正常返回、异常返回和返回。所以after分为三种after returning after throwing 以及after,下面是AspectJ定义的例子

  aspect A {
      
      pointcut publicCall(): call(public Object *(..));
      
      after() returning (Object o): publicCall() {
	  	System.out.println("Returned normally with " + o);
      }
      after() throwing (Exception e): publicCall() {
	  	System.out.println("Threw an exception: " + e);
      }
      after(): publicCall(){
	  	System.out.println("Returned or threw an Exception");
      }
  }

before和after都好理解,那么around呢?截取一下around的定义:

Around advice runs in place of the join point it operates over, rather than before or after it. Because around is allowed to return a value, it must be declared with a return type, like a method.

翻译一下: 环绕通知代替它操作的连接点运行,而不是在它之前或之后运行。因为 around 可以返回一个值,所以它必须声明一个返回类型,就像一个方法。

也就是说, 在环绕通知的主体内,原始连接点的计算可以使用特殊语法执行,这个语法就是调用proceed()方法。切面代码里可以手动调用它,也可以不调用它。

  proceed(...)

around的意思就是在join point的前后都可以添加特定的逻辑,调用proceed()的时候可以改变参数,也能修改返回值,这个操作空间非常大。下面这个例子就是修改了参数,也修改了返回值,但是一般的业务逻辑我们不会这样做。

The proceed form takes as arguments the context exposed by the around's pointcut, and returns whatever the around is declared to return. So the following around advice will double the second argument to foo whenever it is called, and then halve its result:

  aspect A {
      int around(int i): call(int C.foo(Object, int)) && args(i) {
	  int newi = proceed(i*2)
	  return newi/2;
      }
  }

至此,这三个概念已经介绍完。一言以蔽之,就是解决了when,where,how问题,在哪执行where?join point。什么时候执行when?before,after和around。怎么执行,执行什么how?执行advice里的代码。

AspectJ实现切面

理清楚这三个概念后,接下来的问题是怎么写出一个符合自己业务需要的切面。即如何匹配到自己想要切入的点,在切入点执行什么逻辑。假设有一段业务逻辑calculation()方法,需要对这个方法的耗时做一个统计,显然在原有代码里加入耗时调用是可以的,但是这样会显得冗余,这是典型的切面应用场景。接下来用AspectJ实现一个切面,计算调用calculation()方法的时间。

首先定义一个业务方法

/**
 * @author Codegitz
 * @date 2021/12/15 16:43
 **/
public class DemoService {
    public void calculation() throws InterruptedException {
        System.out.println("calculation execute...");
        // 模拟业务逻辑
        Thread.sleep((long) (Math.random() * 1000));
    }
}

然后定义一个切面,这里用的是around切面,用于方法执行前后的时间。

public aspect CalculationAspect {
    void around() : execution(* *.calculation()){
        long start = System.currentTimeMillis();
        proceed();
        System.out.println("calculation() total execute time: " + (System.currentTimeMillis() - start) + "ms");
    }
}

编写测试方法

    @Test
    public void testCalculation() throws InterruptedException {
        DemoService demoService = new DemoService();
        demoService.calculation();
    }

测试结果,实现了切面功能,这是原生AspectJ的实现,并没有涉及一点儿Spring的内容。

1640075427719

你以为搞个demo就完事儿了?没这么简单,接下来才是文章真正的开始,AspectJ的实现。

AspectJ的实现方式

实现原理

AspectJ原生的ajc编译器是经典的静态代理实现,也就是在编译时(CTW--Compiler Time Weaver)就会把切面代码织入到你的逻辑里面。Spring使用的是加载期织入(LTW--Loading Time Weaver)。还是根据上面的例子,我们编写一个before和after的切面逻辑,分析其织入的方式。

CalculationAspect增加两个切面逻辑,分别是beforeafter

public aspect CalculationAspect {
    void around() : execution(* *.calculation()){
        long start = System.currentTimeMillis();
        proceed();
        System.out.println("calculation() total execute time: " + (System.currentTimeMillis() - start) + "ms");
    }
	// 新增加的切面
    before() : execution(* *.calculation()){
        System.out.println("before calculation() ...");
    }
    after() : execution(* *.calculation()){
        System.out.println("after calculation() ...");
    }
}

其他的测试代码不变,打开target目录下的classes文件。找到DemoService.class字节码文件,打开查看反编译后的代码,可以看到如下:


import io.codegitz.aspectj.AspectDemoByAspectJ;
import io.codegitz.aspectj.CalculationAspect;
import org.aspectj.runtime.internal.AroundClosure;

public class DemoService {
    public DemoService() {
    }

    public void calculation() throws InterruptedException {
        try {
           // 织入before逻辑             CalculationAspect.aspectOf().ajc$before$io_codegitz_aspectj_CalculationAspect$1$1b4c2769();
            // 织入around逻辑,注意around同上述两个切面的区别,around对原方法进行了替换
            calculation_aroundBody1$advice(this, CalculationAspect.aspectOf(), (AroundClosure)null);
        } catch (Throwable var2) {
           // 织入after逻辑 CalculationAspect.aspectOf().ajc$after$io_codegitz_aspectj_CalculationAspect$3$1b4c2769();
            throw var2;
        }

       // 织入after逻辑 CalculationAspect.aspectOf().ajc$after$io_codegitz_aspectj_CalculationAspect$3$1b4c2769();
    }
}

结合上面的注释来分析一下织入方式、

Before与After:Before与After只是在方法被调用前和调用之后添加JoinPoint和通知方法(直接插入原程序方法体中),调用AspectJ程序定义的Advise方法,它并不替代原方法,是在方法call之前和之后做一个插入操作。After分为returnning和throwing两类,前者是在正常returning之后调用,后者是在throwing发生之后调用。默认的After是在finally处调用,因此它包含了前面的两种情况。

Around原理:目标方法体被Around方法替换,原方法重新生成,名为XXX_aroundBody(),如果要调用原方法需要在AspectJ切面代码的Around方法体内调用joinPoint.proceed()还原方法执行,这样达到调用原方法的目的。所以我们在编写around切面代码的时候,还需要手动调用proceed方法来调用闭包执行原方法。

为什么调用闭包可以执行原方法呢?达到这个目的需要双方互相引用,桥梁便是Aspect类,目标程序插入了Aspect类所在的包获取引用。AspectJ通过在目标类里面加入Closure(闭包)类,该类构造函数包含了目标类实例、目标方法参数、JoinPoint对象等信息,同时该类作为切点原方法的执行代理,该闭包通过Aspect类调用Around方法传入Aspect切面。这样便达到了关联的目的,便可以在Aspect切面中监控和修改目标代码。

反编译字节码分析

before & after

上面大概说了实现的原理,那么怎么验证是不是就是这样呢?接下来来看一下分析一下切面类的反编译代码。

// before逻辑
CalculationAspect.aspectOf().ajc$before$io_codegitz_aspectj_CalculationAspect$1$1b4c2769();

从上面可以看到,执行的代码都是这种结构,那么就去到CalculationAspect类的反编译代码观察一下aspectOf()方法,代码如下:

    public static CalculationAspect aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("io_codegitz_aspectj_CalculationAspect", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

可以看到这里返回一个经过ajc处理的CalculationAspect实例,然后实例调用ajc$before$io_codegitz_aspectj_CalculationAspect$1$1b4c2769方法,这个方法是什么呢?正是我们在类CalculationAspectbefore切面里写的逻辑,可以观察一下方法名,这跟上面织入的方法名相同,到这里就可以串起来了。after也是一样的逻辑,这里就不再分析,但是around的有所不同,接下来分析一下around。

    @Before(
        value = "execution(* *.calculation())",
        argNames = ""
    )
    public void ajc$before$io_codegitz_aspectj_CalculationAspect$1$1b4c2769() {
        System.out.println("before calculation() ...");
    }
around

从代码执行来看,可以看到around的逻辑已经被替换了,DemoService类的class文件里是这么调用的

 calculation_aroundBody1$advice(this, CalculationAspect.aspectOf(), (AroundClosure)null);

可以看到这个方法加入了闭包类,封装了方法参数。

那么这个方法是什么时候被执行的呢?由于字节码没办法调试,我们又知道around会执行joinPoint.proceed()方法,所以直接去到ProceedingJoinPoint#proceed()}查看具体逻辑。
1640422244287
可以看到这里会用到了一个名为arc的变量
1640422406044
可以看到,这是一个要执行的闭包AroundClosurearc是在set$AroundClosure()方法里被赋值的,继续跟踪set$AroundClosure()方法的调用
1640422487939
可以是在linkClosureAndJoinPoint()方法里被调用的,根据方法名可以看到,这是个链接闭包和Join point的方法,由于跟进一步的调试无法进行,这是最后能跟踪到的方法了。大概推测

calculation_aroundBody1$advice(this, var1, AspectDemoByAspectAnnotation.aspectOf(), (ProceedingJoinPoint)var1)进去后会进行闭包和join point的链接,然后通过执行ProceedingJoinPoint#proceed()方法执行原有逻辑,在闭包里完成around的切面逻辑。想一下,闭包的理解是不是也是一种Aop,当然这是我瞎说的,个人感觉。

总结

这一篇主要是讲了一下AspectJ的实现和简单原理分析,由于这一块我平时看得不多,可能有很多错漏,并且最近比较忙,每天写几句,前后的连贯性非常差。今天总算是抽了个空写了一下,算是写完了,但是应该还有地方需要调整,罢了。

另外,今天圣诞节,圣诞快乐🔥.

posted @ 2021-12-25 17:20  Codegitz  阅读(528)  评论(1编辑  收藏  举报