AspectJ——基于注解的开发方式

Posted KLeonard

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AspectJ——基于注解的开发方式相关的知识,希望对你有一定的参考价值。

基于注解的开发方式

AspectJ5版本支持了基于注解的开发方式,当然其仍然需要AspectJ自己的编译器。

要使用基于注解的开发方式,需要为项目引入aspectjweaver.jar包,该Jar包也在AspectJ安装目录下的lib目录中。aspectjweaver.jar中包含了aspectjrt.jar包中的内容,所以只需要引入aspectjweaver.jar包即可。

0.一个示例

此时我们在Test16包下做一个测试。首先创建业务类Service和测试类Main,如下:

package Test16;

public class Service 
    public int add(int a, int b) 
        return a + b;
    

    public double square(double a) 
        return a * a;
    

    public String upper(String string) 
        return string.toUpperCase();
    
package Test16;

public class Main 
    public static void main(String[] args) 
        Service service = new Service();
        System.out.println("service.add(1, 2) = " + service.add(1, 2));
        System.out.println("service.square(6) = " + service.square(6));
        System.out.println("service.upper(\\"Gavin\\") = " + service.upper("Gavin"));
    

此时如果运行程序,其运行结果是:

接下来,我们使用注解的方式来开发一个切面AnnotationAspect,这时候不使用aspect关键字了,还是使用class类,只不过该类使用注解@Aspect来标注。

package Test16;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class AnnotationAspect 
    @Pointcut("execution(* Test16.Service.add(..))")
    public void addPointcut() 

    

    @Pointcut("call(* Test16.Service.square(double)) && args(value)")
    public void squarePointcut(double value) 

    

    @Pointcut("call(* Test16.Service.upper(String)) && args(string)")
    public void upperPointcut(String string) 

    

    @Before("addPointcut()")
    public void addAdvice(JoinPoint joinPoint) 
        System.out.println();
        System.out.println("joinPoint: " + joinPoint);
        System.out.println("Source Line: " + joinPoint.getSourceLocation());
    

    @Around("squarePointcut(value)")
    public Object squareAround(double value, ProceedingJoinPoint joinPoint)
        System.out.println();
        System.out.println("接收到的参数是:" + value);
        Object result = null;
        try 
            result = joinPoint.proceed(new Object[]value * 2);
         catch (Throwable throwable) 

        
        System.out.println("执行结果是:" + result);
        return result;
    

    @AfterReturning("upperPointcut(value)")
    public void upperAfterReturning(String value) 
        System.out.println();
        System.out.println("接收到的参数是:" + value);
    

我们使用@Aspect来表示该类是一个切面;使用@Pointcut来表示这个方法是一个切入点,方法名就是切入点的名字;使用@Before@After(或者@AfterReturning@AfterThrowing)和@Around来声明这个方法是一个前置通知、后置通知和环绕通知,方法体中写通知的代码。

此时执行结果如下:

1.语法细节

这里的语法细节译自AspectJ的官方文档

1.0.简介

支持注解开发切面的注解集合被称为“@AspectJ”注解。在AspectJ5中,我们不仅可以使用基于注解的方式来开发切面,也可以使用基于代码的方式来开发切面,也可以混用它们。

1.1.声明切面

切面可以使用org.aspectj.lang.annotation.Aspect注解声明。下述声明:

@Aspect
public class Foo

等效于:

public aspect Foo

1.2.切入点(Pointcuts

切入点可以通过在一个方法上使用注解org.aspectj.lang.annotation.Pointcut来指定,这个方法必须返回void,方法的参数与切入点的参数一致,方法的修饰符与切入点的修饰符一致。

一般情况下,使用@Pointcut注解的方法的方法体必须是空的,并且没有任何throws语句。如果切入点绑定了形式参数(使用args()target()this()@args()@target()@this()@annotation()),那么它们必须也是方法的形式参数。

if()切入点比较特殊,我们在之后介绍。

这里是一些切入点的例子,它们同时使用代码方式和@AspectJ方式。

@Pointcut("call(* *.*(..))")
void anyCall()

等效于:

pointcut anyCall(): call(* *.*(..));

当要绑定参数的时候,只需要将参数作为被注解的方法的参数即可:

@Pointcut("call(* *.*(int)) && args(i) && target(callee)")
void anyCall(int i, Fool callee)

它等效于:

pointcut anyCall(int i, Foo callee): call(* *.*(int)) && args(i) && target(callee);

1.2.0.if()切入点表达式

在基于代码的方式中,我们可以使用if(...)切入点来定义一个条件切入点表达式,它会在运行的时候对每一个候选的连接点进行评估。if(...)表达式的条件可以是任何有效的Java逻辑表达式,并且可以使用任何暴露出来的变量,比如连接点变量thisJoinPointthisJoinPointStaticPartthisJoinPointEnclosingStaticPart

当使用注解的方式的时候,因为我们无法在注解值中写一个完整的Java表达式,所以这里的语法有些微不同,但是与之前具有相同的语义和运行时的行为。

if()切入点表达式可以在一个@Pointcut注解中声明,但是其条件必须是空的,被注解的方法必须是public static的,并且返回boolean类型,方法体包含被评估的条件。比如下例:

@Pointcut("call(* *.*(int)) && args(i) && if()")
public static boolean someCallWithIfTest(int i)
    return i > 0;

它等效于:

pointcut someCallWithIfTest(int i): call(* *.*(int)) && args(i) && if(i > 0);

下面也是一个有效的格式:

static int COUNT = 0;

@Pointcut("call(* *.*(int)) && args(i) && if()")
public static boolean someCallWithIfTest(int i, JoinPoint jp, JoinPoint.EnclosingStaticPart esjp) 
    // any legal Java expression...
    return i > 0
        && jp.getSignature().getName.startsWith("doo")
        && esjp.getSignature().getName().startsWith("test")
        && COUNT++ < 10;


@Before("someCallWithIfTest(anInt, jp, enc)") 
public void beforeAdviceWithRuntimeTest(int anInt, JoinPoint jp, JoinPoint.EnclosingStaticPart enc) 
//...


// 注意下面的写法是不对的
/*
@Before("call(* *.*(int)) && args(i) && if()")
public void advice(int i) 
// 所以你想在这里写一个通知还是一个if表达式

*/

1.3.通知

使用注解的方式,一个通知被写成一个普通的Java方法,并且使用BeforeAfterAfterReturningAfterThrowing或者Around注解。除了Around注解的环绕通知以外,所有的方法必须返回void。方法必须是public的。

下述例子使用两种不同的方式表示了一个简单的前置通知:

@Before("call(* org.aspectprogrammer..*(..)) && this(Foo)")
public void callFromFoo()
    System.out.println("Call From Foo")

它等效于:

before(): call(* org.aspectprogrammer..*(..)) && this(Foo)
    System.out.println("Call From Foo");

如果通知需要知道是哪一个具体的Foo对象做出的这个方法调用,只需要为通知声明加上一个参数即可:

before(Foo foo):  call(* org.aspectprogrammer..*(..)) && this(foo)
    System.out.println("Call From Foo" + foo);

这可以写成:

@Before("call(* org.aspectprogrammer..*(..)) && this(foo)")
public void callFromFoo(Foo foo) 
    System.out.println("Call from Foo: " + foo);

如果通知需要访问thisJoinPointthisJoinPointStaticPartthisEnclosingJoinPointStaticPart,它们需要被声明为一个额外的方法参数:

@Before("call(* org.aspectprogrammer..*(..)) && this(foo)")
public void callFromFoo(JoinPoint thisJoinPoint, Foo foo)
    System.out.println("Call from Foo: " + foo + " at " + thisJoinPoint);

这等效于:

 before(Foo foo) : call(* org.aspectprogrammer..*(..)) && this(foo) 
    System.out.println("Call from Foo: " + foo + " at " + thisJoinPoint);

同时需要三个参数的通知可以声明为:

@Before("call(* org.aspectprogrammer..*(..)) && this(Foo)")
public void callFromFoo(JoinPoint thisJoinPoint,
                        JoinPoint.StaticPart thisJoinPointStaticPart,
                        JoinPoint.EnclosingStaticPart thisEnclosingJoinPointStaticPart) 
    // ...

后置通知After()与前置通知Before()的声明方式是一样,并且成功的后置通知AfterReturning在不需要获取返回值、异常的后置通知AfterThrowing在不需要获取抛出的异常的时候,它们的声明方式也是和前置通知一样的。

如果要使用成功的后置通知获取返回值,只需要将返回值声明为方法的参数,并且在注解中将它绑定到returning属性上:

@AfterReturning("criticalOperation()")
public void phew()
    System.out.println("phew");


@AfterReturning(pointcut="call(Foo+.new(..))", returning="f")
public void itsAFoo(Foo f)
    System.out.println("It's a Foo: " + f);

这等效于:

after() returning: criticalOperation()
    System.out.println("phew");


after() returning(Foo f) : call(Foo+.new(..))
    System.out.println("It's a Foo: " + f);

(需要注意的是,需要在切入点表达式之前使用“pointcut=”前缀)

异常的后置通知AfterThrowing与此类似,当需要获取抛出的异常对象的时候,使用throwing属性来绑定参数即可。

对于环绕通知来说,我们需要来解决proceed()的问题。如果在方法体内直接调用proceed()方法:

@Around("call(* org.aspectprogrammer..*(..))")
public Object doNothing()
    return proceed();

我们会得到一个No Such method的编译错误。为此,AspectJ5定义了JoinPoint的一个子接口ProceedingJoinPoint

public interface ProceedingJoinPoint extends JoinPoint
    public Object proceed(Object[] args);

那么上面的环绕通知可以这样写:

@Around("call(* org.aspectprogrammer..*(..))")
public Object doNothing(ProceedingJoinPoint thisJoinPoint)
    return thisJoinPoint.proceed();

这是一个在proceed()调用中使用参数的例子:

@Aspect
public class ProceedAspect
    @Pointcut("call(* setAge(..)) && args(i)")
    void setAge(int i)

    @Around("setAge(i)")
    public Object twiceAsOld(ProceedingJoinPoint thisJoinPoint, int i)
        return thisJoinPoint.proceed(new Object[]i * 2);
    

它等效于:

public aspect ProceedAspect
    pointcut setAge(int i): call(* setAge(..)) && args(i);

    Object around(int i): setAge(i)
        return proceed(i * 2);
    

以上是关于AspectJ——基于注解的开发方式的主要内容,如果未能解决你的问题,请参考以下文章

两种方式开发AspectJ

Spring AOP基于@AspectJ注解的切面

Spring5学习笔记 — “AOP操作—AspectJ注解”

Spring5学习笔记 — “AOP操作—AspectJ注解”

AspectJ注解

AspectJ开发