AspectJ——切入点语法之捕获方法上的连接点

Posted KLeonard

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AspectJ——切入点语法之捕获方法上的连接点相关的知识,希望对你有一定的参考价值。

捕获方法上的连接点

0.捕获方法调用

在调用具有特定签名的方法时,你想捕获它,可以使用call(Signature)切入点,它的语法是:

pointcut [切入点名字](参数列表): call(<可选的方法修饰符> [返回类型] [类名].[方法名]([参数类型]))

注意三点:

  • 1.call(Signature)是在方法调用上触发通知,其环境是调用类。
  • 2.Signature可以包含通配符,用于选择不同类和方法上的一系列连接点。
  • 3.方法修饰符是可选的。要么省略表示任何修饰符都可以,要么写具体的一个修饰符比如public,不能使用通配符。

0.0 通配符的使用

下表列出了把方法Signature提供给切入点声明时,使用通配符选项的一些示例。

具有通配符的方法签名描述
void MyClass.method(int, float)无论修饰符是什么,都会捕获方法上的连接点。
* MyClass.method(int, float)无论修饰符和返回类型是什么,都会捕获方法上的连接点
* *.method(int, float)无论修饰符、返回类型和类是什么,都会捕获方法上的连接点
* *.me*(int, float)无论修饰符是什么、返回类型和类是什么,只要方法名是以me开头,则都会捕获方法上的连接点
* *.*(int, float)无论修饰符、返回类型、类和方法名是什么,都会捕获方法上的连接点
* *.*(*, float)无论修饰符、返回类型、类和方法名是什么,并且方法包含两个参数,第一个参数可以是任意类型,第二个参数是浮点类型,都会捕获方法上的连接点
* *.*(*, ..)无论修饰符、返回类型、类和方法名是什么,并且参数包含一个单值、后接任意数量任意类型的参数,都会捕获方法上的连接点
* *.*(..)或者* *(..)无论修饰符、返回类型、类、方法名以及方法的参数个数与类型是什么,都会捕获方法上的连接点
* mypackage..*.*(..)捕获mypackage包和子包内的任何方法上的连接点
* MyClass+.*(..)捕获MyClass和任何子类中的任何方法上的连接点

根据AspectJ官方网站介绍,星号*和两个点..的用法是这样的:

  • 星号*代表任何数量的除了点号.之外的任何字符。
  • 两个点号..代表任何数量的任何字符,也包括任何数量的点号.
  • 但是在方法参数中,两个点号..来表示任何数量的任何类型的参数,而星号*用来表示一个单独的参数。

下述的写法是错误的:

  1. 假如你想捕获MyClass中的method(int, float)方法,无论其方法修饰符是什么、无论其方法返回类型是什么。你可能会按照如下写法:
* * MyClass.method(int, float)

这里第一个星号*用以匹配方法修饰符,不管其是public还是private都可以;第二个星号*用以匹配方法返回类型,不管是int或者是float或是其他的都行。这个写法看似没有问题,但是一运行程序就会报错,为什么呢?我们前面要注意的三点中说了,方法的修饰符要么省略表示任何修饰符都可以,要么写一个具体的,不能用通配符。也就是说正确的写法是* MyClass.method(int, float)

当然假如限定了方法修饰符是public,也可以写一个具体的:public * MyClass.method(int, float),这样是没问题的。

  1. 与上面的错误原因一样,这样也是错误的:
* void MyClass.method(int, float)

0.1 一个示例

在Test1包下,我们做一个简单的测试。

业务类名字为Service,里面有一个test方法,如下:

package Test1;

public class Service 
    public void test(int a, float b) 
        System.out.println("a + b = " + (a + b));
    

测试类名字为Main,并由主方法,如下:

package Test1;

public class Main 
    public static void main(String[] args) 
        Service service = new Service();
        service.test(2, 3.5f);
    

另外,我们新建一个切面,名字为CallAspect,定义一个切入点callPointCut,并且为其织入前置通知:

package Test1;

public aspect CallAspect 
    pointcut callPointCut(): call(* Service.test(int, float));

    before():callPointCut()
        System.out.println("BeforeAdvice");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());
    

在前置通知中,我们打印了当前连接点的方法签名,并且打印了当前连接点在源代码中的位置,thisJoinPoint是AspectJ提供的内置对象,其代表当前连接点。运行结果如下:

运行结果的前面三行是我们织入的前置通知所打印的内容,最后一行输出了调用Service.test所得到的打印结果。从运行结果中可以看出连接点所表示的方法调用发生在Main.java文件中的第6行。

1.捕获方法调用上传递的参数值

假设我们想在切面中使用捕获到的方法调用所传递的参数值,那么该怎么做?AspectJ提供了原生切入点args来帮助实现这个功能。我们使用call(Signature) && args(标识符)切入点来捕获对方法的调用,然后把需要的标识符绑定到方法的参数值上。当然,参数是在声明切入点的时候传入的。

注意,AspectJ也支持切入点表达式进行逻辑运算,比如进行与或非,分别对应&&||!

我们在上例的基础上做修改,假设CallAspect切面也需要打印出捕获到的方法调用的参数,即捕获调用Service.test(int, float)方法时传递的参数,那么修改如下:

package Test1;

public aspect CallAspect 
    pointcut callPointCut(int a, float b): call(* Service.test(int, float)) && args(a, b);

    before(int a, float b):callPointCut(a, b)
        System.out.println("BeforeAdvice");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("第一个参数是 a = " + a);
        System.out.println("第二个参数是 b = " + b);
    

可以看到,我们给callPointCut切入点加入了要捕获的参数,并且在切入点表达式中使用了逻辑与运算,连接了args(a, b),该原生切入点将方法调用的参数值分别绑定到ab上。

前置通知是我们真正使用参数的地方,所以这里也加入了参数,要特别注意前置通知第一行的写法,before(int a, float b):callPointCut(a, b)。运行结果如下:

注意,要在切入点正确传入要捕获的参数类型,该例中是callPointCut(int a, float b),一个参数是int型,一个是float型。如果类型不匹配,语法上虽然没有错,但是该切入点不会捕获我们想要让他捕获的连接点。

2.捕获方法调用的目标对象

假如你想在切面中捕获调用方法的目标对象,也就是你想知道是哪个对象正在调用这个方法,AspectJ也提供了原生切入点target来实现这个功能。我们使用call(Signatrue) && target(标识符)切入点来捕获对方法的调用。

比如,同样的,我们在0.1的例子上做修改,此时为了标识Service对象,我们给其增加一个name属性,并添加构造方法和toString()方法。

package Test1;

public class Service 
    private String name;

    public Service(String name) 
        this.name = name;
    

    public void test(int a, float b) 
        System.out.println("a + b = " + (a + b));
    

    @Override
    public String toString() 
        return "Service" +
                "name='" + name + '\\'' +
                '';
    

所以主方法也要修改,假设我们实例化的Service的名字是Gavin

package Test1;

public class Main 
    public static void main(String[] args) 
        Service service = new Service("Gavin");
        service.test(2, 3.5f);
    

那么在切面CallAspect中,如果我们想知道调用test方法的Service对象的名字,必然需要获取到当前调用这个方法的Service对象,我们使用target原生切入点,如下:

package Test1;

public aspect CallAspect 
    pointcut callPointCut(Service service): call(* Service.test(int, float)) && target(service);

    before(Service service):callPointCut(service)
        System.out.println("BeforeAdvice");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("service = " + service);
    

运行结果如下:

要注意的是,你想要捕获什么类型的对象,必须在切入点正确传入对应类型的参数,比如该例中是callPointCut(Service service),如果你写成callPointCut(String str),肯定就不会匹配上,语法上没有错,但是该切入点不会匹配任何连接点。

3.捕获方法的执行

前面的讲解以及例子都是通过call捕获方法的调用,其捕获的环境是调用类。我们也可以使用execution来捕获方法的执行,其捕获的环境是目标类方法中。execution(Signature),它的语法是:

pointcut [切入点名字](参数列表): execution(<可选的方法修饰符> [返回类型] [类名].[方法名]([参数类型]))

从语法上看,除了关键字不同,executioncall没有其他的区别。

要注意的点是:

  • execution捕获连接点的环境是目标类方法中。
  • executionSignature也可以使用通配符,通配符的使用规则也是一样的。

所以executioncall最关键的区别就是call捕获连接点的环境是调用类中,而execution捕获连接点的环境是目标类方法中。

为了说明这个区别,我们来举一个例子。在Test2包下,我们有业务类Service和测试类Main,与上面例子中是一样的。除此之外,我们创建CallAndExecutionAspect切面。

业务类Service如下:

package Test2;

public class Service 
    private String name;

    public Service(String name) 
        this.name = name;
    

    public void test(int a, float b) 
        System.out.println("a + b = " + (a + b));
    

    @Override
    public String toString() 
        return "Service" +
                "name='" + name + '\\'' +
                '';
    

测试类Main如下:

package Test2;

public class Main 
    public static void main(String[] args) 
        Service service = new Service("Gavin");
        service.test(2, 3.5f);
    

切面CallAndExecutionAspect如下:

package Test2;

public aspect CallAndExecutionAspect 
    pointcut callPointcut(): call(* Service.test(int, float));

    pointcut executionPointcut(): execution(* Service.test(int, float));

    before(): callPointcut()
        System.out.println("============");
        System.out.println("CallPointCut Before Advice");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());
    

    before(): executionPointcut()
        System.out.println("============");
        System.out.println("ExecutionPointCut Before Advice");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());
    

在切面中,我们创建了两个切入点,分别是callPointcutexecutionPointcut,两者分别捕获方法的调用和方法的执行,其他都是一样的。并且,分别为这两个切入点织入了前置通知,在通知中,打印出了方法签名,与连接点在源代码中的位置。

执行测试类Main的结果如下:

从执行结果中,可以看出,两个连接点的位置是不一样的。方法调用发生在Main中,所以运行结果指示出了其位置是Main.java的第6行。方法执行是真正执行Service类中定义的test方法,所以运行结果指示出了其位置是Service类中的第10行。

通过这个示例,我们可以充分体会到方法调用切入点与方法执行切入点的不同之处,要记住的重点就是:在什么地方调用通知,以及它的环境是什么。

4.在执行方法时捕获this引用的值

假如在捕获方法时,你想显示Java的this引用所指向的对象,使之可以被通知使用,那么该怎么做呢?

AspectJ为此也提供了一个原生切入点this可以实现这一功能。可以使用execution(Signature) && this(标识符)来捕获方法执行时的Java的this对象。也可以使用call(Signature) && this(标识符)来捕获方法调用时的Java的this对象。

注意,this对象具体指向哪一个对象取决于连接点的位置,我们知道callexecution所捕获的连接点位置是不一样的,所以其捕获的this对象必然不同。在前面我们也知道了target原生切入点,其捕获的是方法调用的目标对象。对于execution来说,方法调用的目标对象与连接点(也就是方法执行时)的this引用的对象是一样的,故其使用this原生切入点和target原生切入点捕获的对象是一样的。而对于call来说,方法调用的目标对象与当前连接点的this引用的对象是不一样的(大多数情况),因为当前连接点的位置所在的类与方法调用目标对象所属的类是不一样的,故其使用thistarget原生切入点捕获的对象是不一样的。

举例如下,假设我们在Test3包下测试,业务类Service与上面的例子中一样,新建Test类,该类有runTest方法来调用Service类中的方法,主函数依然在Main类中,主函数调用runTest方法。另外,我们创建切面executionAspect

Service类与上面的例子一样,这里不再赘述。Test类如下:

package Test3;

public class Test 
    public void runTest() 
        Service service = new Service("Gavin");
        service.test(2, 3.5f);
    

主函数类Main如下:

package Test3;

public class Main 
    public static void main(String[] args) 
        Test test = new Test();
        test.runTest();
    

切面executionAspect如下:

package Test3;

public aspect executionAspect 
    pointcut executionAndThisPointcut(Service service):execution(void Test3.Service.test(..)) && this(service);

    pointcut executionAndTargetPointcut(Service service):execution(void Test3.Service.test(..)) && target(service);

    before(Service service): executionAndThisPointcut(service)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("this Object:" + service);
    

    before(Service service): executionAndTargetPointcut(service)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("target Object:" + service);
    

在该切面中,我们创建了两个切入点,两个切入点分别使用this原生切入点和target原生切入点捕获了方法执行时Java关键字this所指向的对象和方法调用的目标对象。这两个对象都是Service类型的。并且为这两个切入点分别织入了前置通知,在前置通知中我们打印了捕获到的对象。

执行结果如下:

从程序运行结果中,我们可以看出对于execution来说,this原生切入点和target原生切入点捕获到的对象确实是一样的。

那么对于call来说是怎么样的结果呢?我们删除executionAspect,并且新建callAspect,如下:

package Test3;

public aspect callAspect 
    pointcut callAndThisPointcut(Service service):call(void Test3.Service.test(..)) && this(service);

    pointcut callAndTargetPointcut(Service service):call(void Test3.Service.test(..)) && target(service);

    before(Service service): callAndThisPointcut(service)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("this Object:" + service);
    

    before(Service service): callAndTargetPointcut(service)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("target Object:" + service);
    

切面callAspect中定义的两个切入点和executionAspect中的定义的两个切入点除了使用call而不是execution之外,其他都一样。同样的,为这两个切入点织入前置通知,前置通知里面的代码也都是一样的。

执行结果如下:

从执行结果可以看出,callAndThisPointcut切入点并没有捕获到任何连接点。这是为什么呢?根据前面的分析,call(void Test3.Service.test(..)) && this(service),这个切入点使用this原生切入点捕获该连接点处的Java中的this所引用的对象,因为该连接点是位于Test类中的(从执行结果也可以看出),所以其捕获的对象一定是一个Test类的对象,而不是Service类的对象。而我们在定义切入点的时候,我们定义的是pointcut callAndThisPointcut(Service service),我们传入的是Service对象参数,所以可想而知,虽然语法上没有错误,但是该切入点并不能捕获到任何连接点(当然,除非该方法调用发生在Servcie类中,但我们的例子中并不是)。

此时,如果我们将callAndThisPointcut(Service service)更改为callAndThisPointcut(Test test),那么该切入点是可以捕获到这个方法调用连接点的。更改之后的callAspect代码如下:

package Test3;

public aspect callAspect 
    pointcut callAndThisPointcut(Test test):call(void Test3.Service.test(..)) && this(test);

    pointcut callAndTargetPointcut(Service service):call(void Test3.Service.test(..)) && target(service);

    before(Test test): callAndThisPointcut(test)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("this Object:" + test);
    

    before(Service service): callAndTargetPointcut(service)
        System.out.println("===========");
        System.out.println("Signature: " + thisJoinPoint.getSignature());
        System.out.println("Source Line: " + thisJoinPoint.getSourceLocation());

        System.out.println("target Object:" + service);
    

此时运行结果如下:

可见更改后的切入点确实捕获到了方法调用连接点。从这个例子中也可以充分体会到,callexecution分别在与thistarget结合使用时的区别。

以上是关于AspectJ——切入点语法之捕获方法上的连接点的主要内容,如果未能解决你的问题,请参考以下文章

AspectJ——切入点语法之捕获属性上的连接点

AspectJ——切入点语法之捕获类和对象构造上的连接点

AspectJ——切入点语法之捕获类和对象构造上的连接点

AspectJ——切入点语法之捕获异常处理上的连接点

AspectJ——切入点语法之thistargetargsif以及逻辑运算

AspectJ——切入点语法之thistargetargsif以及逻辑运算