AspectJ 是为了解决面向切面的编程而诞生的,通过它可以在Java应用程序中织入横切关注点,比如日志、性能分析、安全检查等。AspectJ 定义了一套自己的语法,拥有自己的编译器。

AOP与AspectJ以及SpringAOP的关系

  • AOP: Aspect-Oriented Programming 即面向切面的编程,是一种编程模式,或者说是一种编程思想.
  • AspectJ:是一个实现了 AOP 的框架,它提供了一种基于 Java 语言的面向切面编程语言扩展。AspectJ 具有强大的 AOP 功能,可以在编译期、类加载期或运行时织入切面,从而使开发人员能够更加灵活地控制切面的应用时机和范围
  • Spring AOP 是 Spring 框架中的 AOP 模块,是对Spring 管理的 Bean 进行切面编程的一种实现,并且完全支持 AspectJ 的语法,除此之外,与 AspectJ 没有关系,它是通过动态代理(JDK Proxy 或者 CGLib)来实现的.

它们三者的关系如下图所示:

2023-03-30T12:27:27.png

AspectJ 代码织入的方式

AspectJ 代码织入的方式有三种:

  • 编译时织入(Compile-time weaving, CTW): 在编译目标程序时,将切面代码编译到目标代码中,这种方式需要使用特殊的编译器来支持,比如 Aspect 编译器:ajc.
  • 类装载时织入(Load-time weaving, LTW): 当目标程序加载到 JVM 时,通过特殊的代理机制动态织入切面代码。这种方式需要使用特殊的代理库来支持,例如 AspectJ Weaver(aspectjweaver.jar). 一般都是通过 javaagent 来实现。
  • 运行时织入(Runtime weaving, RTW): 在目标程序运行时,通过特殊的API动态织入切面代码,这种方式需要使用 AspectJ 提供的 API 来实现,例如 AspectJProxyFactory 类.
    其中,编译时织入和类装载时织入,需要在编译和运行时使用特殊的参数和配置来启用,而运行时织入可以通过代码动态实现.

一般来说,编译时织入和类装载时织入可以在编译时或打包时完成,从而可以提高目标程序的性能。而运行时织入则可以更灵活地动态地控制切面的织入和撤销,但可能会影响目标程序的性能。

AspectJ 语法

AspectJ 语法分为三种

  • 直接使用 AspectJ 语法编写 aspect 类
  • 基于 XML 配置文件的语法
  • 注解的语法

编写aspect类

这种方式的切面类,通常以 .aj 结尾,并且更加灵活,
例如,以下是一个使用 .aj 文件编写的切面类的示例:

public aspect MyAspect {
    pointcut myPointcut(): execution(* com.example.MyClass.myMethod(..));
    before(): myPointcut() {
        // advice code
    }
}

注意,在使用 .aj 文件编写切面时,需要使用特定的编译器(例如 ajc 命令)进行编译,而不能直接使用普通的 Java 编译器(例如 javac 命令)。另外,由于 .aj 文件使用的是 AspectJ 语法而不是标准的 Java 语法,因此需要在编译时指定 AspectJ 运行时库的类路径,例如:

ajc -cp /path/to/aspectjrt.jar MyAspect.aj

这个命令将编译 MyAspect.aj 文件,并将 /path/to/aspectjrt.jar 文件添加到类路径中,以便编译器可以查找 AspectJ 运行时库中的类和方法。

实战

  • 安装 ajc 编译器

我们首先需要下载 AspectJ 的编译器 ajc,
可以通过 eclipse 官网进行下载:

https://www.eclipse.org/aspectj/downloads.php

下载完成后,运行下面的命令进行安装:

java -jar aspectj-1.x.x.jar

安装完成后,还需添加环境变量,之后在命令行运行 ajc,check是否安装成功.

  • 编写要被织入的Java目标代码
public class UserService {

    public int addUser() {
        System.out.println("add user");
        return 1;
    }
}
public class AjcTest {

    public static void main(String[] args) {

        UserService userService = new UserService();

        userService.addUser();
    }
}
  • 编写切面类
public aspect AuthAspect {

    before():execution(* com.kklab.clouddemo.ajc.*.*(..)) {
        System.out.println("process auth check");
    }
}
  • 编译
ajc -cp /path/to/aspectjrt.jar -d *.java *.aj

-d 命令可以让class文件在当前文件夹下生成

生成的UserService.class文件如下, 可以看到在 addUser 方法中已经织入了一行代码,这行代码将会调用我们定义的切面方法.

public class UserService {
    public UserService() {
    }

    public int addUser() {
        AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$204a3380();
        System.out.println("add user");
        return 1;
    }
}

假如我们将 aspect 类中 execution 改为 call,再运行生成 class 文件会发现,并没有在 UserService.class 中织入代码,而是在调用 UserSerivce 方法的地方织入了代码:

public class AjcTest {
    public AjcTest() {
    }

    public static void main(String[] args) {
        UserService userService = new UserService();
        AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$1dafce7a();
        userService.addUser();
    }
}

这也充分说明了 execution 和 call 两者的区别:

execution 用在方法执行的位置织入,而 call 是在方法调用的位置织入.

在编写切面时,需要根据具体的需求选择合适的切入点类型。

注解语法

AspectJ 的注解语法是基于 Java 注解的,可以使用 @Aspect 注解声明一个切面类,使用其他注解声明切点和通知等元素。
以下是常用的 AspectJ 注解及其用法:

  • @Aspect:用于声明一个切面类。
  • @Pointcut:用于声明一个切点
  • @Before:用于声明一个前置通知。
  • @After:用于声明一个后置通知。
  • @Around:用于声明一个环绕通知。
  • @AfterReturning:用于声明一个返回通知。
  • @AfterThrowing:用于声明一个异常通知。

除了以上注解外,AspectJ 还提供了其他一些注解,例如 @DeclareParents 用于声明一个引入通知,以及 @DeclarePrecedence 用于声明切面类之间的优先级顺序等。

实战

我们创建一个普通的Java类文件,不过这次要在这个类上面加一些 aspect 的注解:

@Aspect
public class LogAspect {
    @Before("execution(* com.kklab.clouddemo.ajc.*.*(..))")
    public void logBefore() {
        System.out.println("Before executing add method.");
    }
}

然后再使用 ajc 编译一下,由于注解是在jdk 1.5 之后才出现的,所以这次我们需要指定一下 ajc 的 jdk 兼容模式:

ajc -cp /path/to/aspectjrt.jar -d *.java *.aj -1.8

编译之后,查看 class 文件,发现同样对 UserService.class 进行了织入:

public class UserService {
    public UserService() {
    }

    public int addUser() {
        AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$204a3380();
        LogAspect.aspectOf().logBeforeAdd();
        System.out.println("add user");
        return 1;
    }
}

AspectJ 表达式语法

AspectJ 表达式语法是用来声明切入点的,可以指定匹配的目标方法或者构造方法。
AspectJ 表达式语法非常的灵活,可以根据需要使用各种模式匹配规则来定位目标 JoinPoint,
下面是 AspectJ 表达式语法的一些常用模式:

方法调用模式

call([可见性模式] [返回类型模式] 包名模式.类名模式.方法名模式([参数模式]) [throws模式])

示例:

// 匹配可见性为 public,返回类型为 void,包名为 com.example,类名以 Service 结尾,方法名以 save 开头,且有一个参数的方法。
call(public void com.example.*Service.save(*))

// 匹配所有可见性,返回类型为 int,包名为 com.example,类名以 Dao 结尾,方法名为 get 开头,且不接受参数的方法。
call(int com.example.Dao.get())

// 匹配可见性为 private 或 protected,返回类型为任意类型,包名为 com.example,类名以 *Dao 结尾,方法名为 *,且任意参数的方法。
call(private|protected * com.example.*Dao.*(*))

方法执行模式

execution([可见性模式] [返回类型模式] 包名模式.类名模式.方法名模式([参数模式]) [throws模式])

示例:

// 匹配可见性为 public,返回类型为 void,包名为 com.example,类名以 Service 结尾,方法名以 save 开头,且有一个参数的方法。
execution(public void com.example.*Service.save(*))

// 匹配所有可见性,返回类型为 int,包名为 com.example,类名以 Dao 结尾,方法名为 get 开头,且不接受参数的方法。
execution(int com.example.Dao.get())

// 匹配可见性为 private 或 protected,返回类型为任意类型,包名为 com.example,类名以 *Dao 结尾,方法名为 *,且任意参数的方法。
execution(private|protected * com.example.*Dao.*(*))

Maven 自动化构建

codehaus 提供了一个 ajc 的编译插件 aspectj-maven-plugin,只要在 maven 配置文件中添加这个插件,
就可以使用 maven 来编译 AspectJ 了.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.10</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <complianceLevel>1.8</complianceLevel>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

参考文档:
https://toutiao.io/posts/os3asg/preview

文章目录