跟我学AspectJ(三)

本文详细介绍了AspectJ中的切点与通知概念,包括如何定义切点来捕获程序执行过程中的连接点,以及如何定义不同类型的通知来执行特定逻辑。通过具体的例子,帮助读者深入理解AspectJ的核心机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引语
       在本系列的前一章中,我们简要的说明了 AspectJ 语言的总揽。为了理解 AspectJ 的语法和语义,你应该阅读本章。这一部分包括了前述的一些材料,但是将更加完整和更多的讨论细节。文章将由一个具体方面的例子开始,这个方面包括了一个切点,一个类型间声明和两个通知,这个例子将给我们一些讨论的话题。
分析方面 (The Anatomy of an Aspect)
       首先给出我们定义的方面。
1 aspect FaultHandler {
 2
 3   private boolean Server.disabled = false;
 4
 5   private void reportFault() {
 6     System.out.println("Failure! Please fix it.");
 7   }
 8
 9   public static void fixServer(Server s) {
10     s.disabled = false;
11   }
12
13   pointcut services(Server s): target(s) && call(public * *(..));
14
15   before(Server s): services(s) {
16     if (s.disabled) throw new DisabledException();
17   }
18
19   after(Server s) throwing (FaultException e): services(s) {
20     s.disabled = true;
21     reportFault();
22   }
23 }
FaultHandler 包括一个在 Server 上的类型间字段声明(第 3 行),两个方法 (5-7 行和 9-11 行 ) ,切点定义( 13 行),两个通知( 15-17 行和 19-22 行)。这些覆盖了方面能够包括的基本信息。通常来说,方面包括其他程序实体、不同的变量和方法、切点定义、类型间声明和通知(可能有 before 、 after 或 around )。文章的余下部分将逐一讨论这些横切相关的构造。
 
    切点(Pointcuts)
AspectJ 的切点定义为切点取名。切点自己捕捉连接点集合,例如,程序执行中的赶兴趣的点集合。这些连接点可以是方法或者构造子的调用或执行,异常处理,字段的赋值和读取等等。举个例子,在 13 行的切点定义:
pointcut services(Server s): target(s) && call(public * *(..))
这个切点,被命名为 services ,捕捉当 Server 对象的公共方法被调用时程序执行过程中的连接点。它同样允许使用 services 切点的任何人访问那些方法被调用的 Server 对象。 FaultHandler 方面的这个切点背后的思想是指错误处理相关的行为必须由公共方法的调用来触发。例如, server 可能因为某些错误不能处理请求。因此,对那些方法的调用是该方面感兴趣的事件,当这些事件发生时,对应的错误相关的事情也将发生。
事件发生时的部分上下文由切点的参数暴露。在这个例子中,就是指 Server 类型的对象。这个参数在切点声明的右边被使用,以便指明哪个事件与切点相关。在例子中, services 切点包括两部分,它是由捕捉那些以 Server 为目标对象的操作 (target(S)) 的连接点,与那些捕捉 call 的连接点 (call(..)) 组合起来 (&&, 意为逻辑与 and) 形成的切点。调用连接点 (calls) 通过方法签名描述,在此例中,使用了几个通配符表达。在返回类型的位置是 (*), 在方法名的位置是 (*) 而在参数列表位置是 (..); 只指定了一个具体信息,就是 public 。
切点捕捉程序中的大量连接点。但是它们仅仅捕捉几种连接点。这些种类的连接点代表了 Java 语言中的一些重要概念。这里是对于这些概念的不完整列表:方法调用、方法执行、异常处理、实例化、构造子执行以及字段的访问。每一种连接点都由特定的切点捕捉,你将在本文的其他部分学到它们。
 
   通知(Advice)
       一个通知由切点和通知代码组成,它定义了在切点捕捉到的连接点处运行的逻辑。例如, 15-17 行的通知中的代码 if (s.disabled) throw new DisabledException(); 当 Server 的实例调用它的公共方法时执行。 19-22 行定义的另一个通知在同一个切点 (services) 处执行。
{
 s.disabled = true;
 reportFault();
}
当时第二个通知只有在程序抛出 FaultException 时才执行。有三类的 after 通知:分别基于方法正常结束、方法异常结束和方法一任何方式结束。
 
连接点和切点 (Join Points and Pointcuts)
考虑下面的 Java 类
class Point {
    private int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
 
    void setX(int x) { this.x = x; }
    void setY(int y) { this.y = y; }
 
    int getX() { return x; }
    int getY() { return y; }
}
为了能够理解 AspectJ 的连接点和切点概念,让我们回顾以下 Java 语言的一些基本原理。考虑类 Point 中的方法声明        void setX(int x) { this.x = x; } 这个程序片段说明当 Point 类实例调用名为 setX 有一个整型参数的方法时,程序执行方法体 { this.x=x;} 。与此类似的是,类的构造子表明如果当 Point 类使用两个整型参数实例化时,构造子体内的 {this.x=x;this.y=y;} 将被执行。用一句话总结就是:当一些事发生时,就有一些东西被执行。在面向对象程序中,有一些种类的“发生的事”是由语言本身所决定。我们把这些称为 Java 的连接点。连接点有一些像方法调用、方法执行、对象实例化、构造子执行、字段引用以及异常处理等组成。
而切点就是用来捕捉这些连接点的结构,例如,下面的切点
pointcut setter(): target(Point) &&
                   (call(void setX(int)) ||
                    call(void setY(int)));
捕捉对于 Point 实例上 setX ( int )或 setY ( int )的每一个方法调用。看看另外一个例子
pointcut ioHandler(): within(MyClass) && handler(IOException);
这个切点捕捉类 MyClass 内异常处理代码执行时的每个连接点。
切点定义包括由冒号分割的两部分。左边包括切点的名称和切点的参数(例如事件发生时的数据)。右边则包括切点本身。
 
一些切点的例子
下面的切点是特定方法执行时起作用
       execution(void Point.setX(int))
进行方法调用时则使用
       call(void Point.setX(int))
异常处理执行时的切点定义如下
       handler(ArrayOutOfBoundsException)
当前正在使用 SomeType 类型的对象
this(SomeType)
SomeType 类型对象为目标对象时
target(SomeType)
如果连接点处在 Test 的无参数 main 函数调用流程中
cflow(call(void Test.main())
切点还可以使用或 (“||”) 以及与 (“and”) 和非 (“!”) 组合。
·可以使用通配符。因此
1. Execution(* *(..))
2. Call(* set(..))
代表( 1 )不考虑参数和返回值的任何方法执行( 2 )对于任何参数和返回值的方法名为 set 方法的调用。
·可以基于类型选择元素,例如
1. Execution(int *())
2. Call(* setY(long))
3. Call(* Point.setY(int))
4. Call(*.new(int,int))
代表( 1 )返回值是 int 型的任何无参数方法执行;( 2 )任何返回类型且参数为 long 类型的名为 setY 方法的调用;( 3 )任意 Point 对象的有一个 int 类型 setY 方法的调用,忽略返回类型;( 4 )对于任何类的构造子的调用,只要该构造子有两个 int 类型的参数。
·如何组合切点,例如
1. Target(Point) && call(int *())
2. Call(* *(..)) && (within(Line) || within(Point))
3. Within(*) && execution(*.new(int))
4. !this(Point) && call(int *(..))
代表( 1 ) Point 实例上返回类型为 int 的任意无参数方法调用;( 2 ) Line 或 Point 定义及其实例产生的任意方法调用;( 3 )任意带一个 int 类型参数的构造子调用;( 4 )任意返回类型为 int 类型的方法调用只要当前执行类实例不是 Point 类型。
·选择有特定修饰的方法或构造子
1. Call(public * *(..))
2. Execution(!static * *(..))
3. Execution(public !static * *(..))
代表( 1 )任意公共方法的调用;( 2 )任意非静态方法的执行;( 3 )任意公共非静态方法的执行。
·切点还能够处理接口。例如给定接口
       interface MyInterface { … }
       切点 call(* MyInterface.*(..)) 捕捉 MyInterface 定义的方法以及超类型定义的方法调用。
 
切点合成
切点可以使用操作符与( && )、或( || )和非(!)。这样可以使用简单的原始切点来创建强大功能的切点。当使用原始切点 cflow 和 cflowbelow 进行组合时,可能会有些迷糊。例如, cflow ( p )捕捉 p 流程内(包括 P 在内)的所有连接点,可以使用图形表示
 P ---------------------
    /
     / cflow of P
      /
那么 cflow(P) && cflow(Q) 捕捉的是什么呢?它捕捉同时处于 P 和 Q 流程中的连接点。
             P ---------------------
             /
               / cflow of P
                 /
                  /
                  /
 Q -------------/-------
    /                 /
     / cflow of Q  /  cflow(P) && cflow(Q)
      /                    /
注意 P 和 Q 可能没有任何公共的连接点,但是它们的程序流程中可能有公共的连接点。
Cflow(P && Q) 又是什么意思呢?它的意思是被 P 和 Q 共同捕捉的连接点的流程。
   P && Q -------------------
          /
           /  cflow of (P && Q)
            /
如果 P 和 Q 没有捕捉到公共的连接点,那么在( P&&Q )的程序流程中不可能有任何连接点。下面代码表明上述意思
public class Test {
    public static void main(String[] args) {
        foo();
    }
    static void foo() {
        goo();
    }
    static void goo() {
        System.out.println("hi");
    }
}
 
aspect A {
    pointcut fooPC(): execution(void Test.foo());
    pointcut gooPC(): execution(void Test.goo());
    pointcut printPC(): call(void java.io.PrintStream.println(String));
 
    before(): cflow(fooPC()) && cflow(gooPC()) && printPC() {
        System.out.println("should occur");
    }
 
    before(): cflow(fooPC() && gooPC()) && printPC() {
        System.out.println("should not occur");
    }
}
切点参数
考虑下述代码
 pointcut setter():
target(Point) && (call(void setX(int)) || call(void setY(int)));
这个切点捕捉以 Point 实例为目标的每个 setX(int) 或 setY(int) 方法的调用。切点名为 setters 并且在左边没有任何参数。空的参数列意味着切点不会暴露任何连接点的上下文信息。但是考虑另一版本的切点定义
 pointcut setter(Point p):
target(p) && (call(void setX(int)) || call(void setY(int)));
它的功能与前述的切点相同,但是这里的切点包括一个参数类型为 Point 。这就意味着任何使用这个切点的通知都可以访问被切点捕捉连接点中的 Point 实例。
现在看看又一版本的 setters 切点
pointcut setter(Point p, int newval): target(p) && args(newval)
&& (call(void setX(int)) || call(void setY(int)));
这里切点暴露了一个 Point 对象和一个 int 值。在切点定义的右边,我们发现 Point 对象是目标对象,而且 int 值是被调用方法的参数。
       切点参数的使用有很大的伸缩性。最重要的规则是所有的切点参数必须被绑定在每个切点捕捉的连接点上。因此,下面的例子就会参数编译错误。
 pointcut badPointcut(Point p1, Point p2):
      (target(p1) && call(void setX(int))) ||
(target(p2) && call(void setY(int)));
因为 p1 仅当 setX 调用时被绑定,而        p2 只在 setY 调用时被绑定,但是切点却捕捉所有的这些连接点并且试图同时绑定 p1 和 p2
 
一个例子: HandleLiveness
这个例子包括两个对象类,一个异常类和一个方面。 Handle 对象仅仅是代理 Partner 对象的非静态公共方法。方面 HandleLiveness 确保在代理之前 Partner 存在并且可用,否则抛出一个异常。
 class Handle {
    Partner partner = new Partner();
     public void foo() { partner.foo(); }
    public void bar(int x) { partner.bar(x); }
     public static void main(String[] args) {
      Handle h1 = new Handle();
      h1.foo();
      h1.bar(2);
    }
 }
   class Partner {
    boolean isAlive() { return true; }
    void foo() { System.out.println("foo"); }
    void bar(int x) { System.out.println("bar " + x); }
 }
  aspect HandleLiveness {
    before(Handle handle): target(handle) && call(public * *(..)) {
      if ( handle.partner == null || !handle.partner.isAlive() ) {
        throw new DeadPartnerException();
      }
    }
 }
   class DeadPartnerException extends RuntimeException {}
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值