1. 简介

本教程将快速且实用地介绍 AspectJ

首先,我们演示如何启用面向切面编程(AOP),然后重点讲解编译时、后编译时和加载时织入的区别。

让我们从面向切面编程(AOP)和 AspectJ 基础的简要介绍开始。

2. 概述

AOP 是一种编程范式,旨在通过分离横切关注点来提高模块化。它通过在不修改现有代码的情况下添加额外行为来实现这一点。相反,我们单独声明要修改哪些代码。

AspectJ 通过扩展 Java 编程语言,实现了关注点和横切关注点的织入。

3. Maven 依赖

AspectJ 根据不同用途提供不同库。我们可以在 Maven 中央仓库的 org.aspectj 组下找到相关依赖。

本文将重点介绍创建切面和使用编译时、后编译时、加载时织入器所需的依赖。

3.1. AspectJ 运行时

运行 AspectJ 程序时,类路径需包含类、切面以及 AspectJ 运行时库 aspectjrt.jar

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.20.1</version>
</dependency>

此依赖可在 Maven Central 获取。

3.2. AspectJWeaver

除了运行时依赖,我们还需要引入 aspectjweaver.jar 以在加载时向 Java 类添加通知:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.20.1</version>
</dependency>

此依赖同样可在 Maven Central 获取。

4. 创建切面

AspectJ 提供了 AOP 的实现,包含三个核心概念

  • 连接点(Join Point)
  • 切入点(Pointcut)
  • 通知(Advice)

我们将通过创建一个验证用户账户余额的简单程序来演示这些概念。

首先,创建一个带初始余额的 Account 类,并定义取款方法:

public class Account {
    int balance = 20;

    public boolean withdraw(int amount) {
        if (balance < amount) {
            return false;
        } 
        balance = balance - amount;
        return true;
    }
}

创建 AccountAspect.aj 文件记录账户信息并验证余额(注意:AspectJ 文件以 "*.aj" 为扩展名):

public aspect AccountAspect {
    final int MIN_BALANCE = 10;

    pointcut callWithDraw(int amount, Account acc) : 
     call(boolean Account.withdraw(int)) && args(amount) && target(acc);

    before(int amount, Account acc) : callWithDraw(amount, acc) {
    }

    boolean around(int amount, Account acc) : 
      callWithDraw(amount, acc) {
        if (acc.balance < amount) {
            return false;
        }
        return proceed(amount, acc);
    }

    after(int amount, Account balance) : callWithDraw(amount, balance) {
    }
}

如上所示,我们为 withdraw 方法添加了 pointcut,并创建了三个引用该 pointcutadvice

为便于理解,先明确以下定义:

  • 切面(Aspect):横切多个对象的关注点的模块化。每个切面专注于特定的横切功能。
  • 连接点(Join point):程序执行过程中的点,如方法执行或属性访问。
  • 通知(Advice):切面在特定连接点执行的操作。
  • 切入点(Pointcut):匹配连接点的正则表达式。通知与切入点表达式关联,并在所有匹配的连接点上运行。

关于这些概念及其语义的更多细节,可参考官方文档

接下来需要将切面织入代码。以下章节将介绍 AspectJ 的三种织入方式:编译时织入、后编译时织入和加载时织入。

5. 编译时织入

最简单的织入方式是编译时织入。当同时拥有切面源代码和使用切面的代码时,AspectJ 编译器会从源码编译并生成织入后的类文件。执行代码时,织入后的类文件会像普通 Java 类一样加载到 JVM。

可下载 AspectJ 开发工具,它包含捆绑的 AspectJ 编译器。AJDT 最重要的特性之一是横切关注点可视化工具,这对调试切入点定义非常有用。我们甚至可以在代码部署前预览织入效果。

使用 Mojo 的 AspectJ Maven 插件,通过 AspectJ 编译器将切面织入类:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.14.0</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <showWeaveInfo>true</showWeaveInfo>
        <verbose>true</verbose>
        <Xlint>ignore</Xlint>
        <encoding>UTF-8 </encoding>
    </configuration>
    <executions>
        <execution>
            <goals>
                <!-- 织入所有主类 -->
                <goal>compile</goal>
                <!-- 织入所有测试类 -->
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

关于 AspectJ 编译器选项的更多细节,可参考选项参考文档

Account 类添加测试用例:

public class AccountUnitTest {
    private Account account;

    @Before
    public void before() {
        account = new Account();
    }

    @Test
    public void givenBalance20AndMinBalance10_whenWithdraw5_thenSuccess() {
        assertTrue(account.withdraw(5));
    }

    @Test
    public void givenBalance20AndMinBalance10_whenWithdraw100_thenFail() {
        assertFalse(account.withdraw(100));
    }
}

运行测试时,控制台显示以下文本表示织入成功:

[INFO] Join point 'method-call
(boolean com.baeldung.aspectj.Account.withdraw(int))' in Type
'com.baeldung.aspectj.test.AccountTest' (AccountTest.java:20)
advised by around advice from 'com.baeldung.aspectj.AccountAspect'
(AccountAspect.class:18(from AccountAspect.aj))

[INFO] Join point 'method-call
(boolean com.baeldung.aspectj.Account.withdraw(int))' in Type 
'com.baeldung.aspectj.test.AccountTest' (AccountTest.java:20) 
advised by before advice from 'com.baeldung.aspectj.AccountAspect' 
(AccountAspect.class:13(from AccountAspect.aj))

[INFO] Join point 'method-call
(boolean com.baeldung.aspectj.Account.withdraw(int))' in Type 
'com.baeldung.aspectj.test.AccountTest' (AccountTest.java:20) 
advised by after advice from 'com.baeldung.aspectj.AccountAspect'
(AccountAspect.class:26(from AccountAspect.aj))

2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
-  Balance before withdrawal: 20
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
-  Withdraw ammout: 5
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
- Balance after withdrawal : 15
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
-  Balance before withdrawal: 20
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
-  Withdraw ammout: 100
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
- Withdrawal Rejected!
2016-11-15 22:53:51 [main] INFO  com.baeldung.aspectj.AccountAspect 
- Balance after withdrawal : 20

6. 后编译时织入

后编译时织入(也称二进制织入)用于织入现有类文件和 JAR 文件。与编译时织入类似,用于织入的切面可以是源码或二进制形式,且自身可被其他切面织入。

使用 Mojo 的 AspectJ Maven 插件实现时,需在插件配置中指定要织入的所有 JAR 文件:

<configuration>
    <weaveDependencies>
        <weaveDependency>  
            <groupId>org.agroup</groupId>
            <artifactId>to-weave</artifactId>
        </weaveDependency>
        <weaveDependency>
            <groupId>org.anothergroup</groupId>
            <artifactId>gen</artifactId>
        </weaveDependency>
    </weaveDependencies>
</configuration>

包含待织入类的 JAR 文件必须在 Maven 项目中声明为 <dependencies/>,并在 AspectJ Maven 插件的 <configuration> 中列为 <weaveDependencies/>

7. 加载时织入

加载时织入本质上是将二进制织入推迟到类加载器加载类文件并将其定义到 JVM 的时刻。

为此,需要一个或多个“织入类加载器”。这些类加载器由运行时环境显式提供,或通过“织入代理”启用。

7.1. 启用加载时织入

AspectJ 加载时织入可通过 AspectJ 代理启用,该代理可参与类加载过程,并在类定义到 VM 前织入所有类型。通过向 JVM 指定 -javaagent:pathto/aspectjweaver.jar 选项,或使用 Maven 插件配置 javaagent

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
        <argLine>
            -javaagent:"${settings.localRepository}"/org/aspectj/
            aspectjweaver/${aspectj.version}/
            aspectjweaver-${aspectj.version}.jar
        </argLine>
        <useSystemClassLoader>true</useSystemClassLoader>
        <forkMode>always</forkMode>
    </configuration>
</plugin>

7.2. 配置织入器

AspectJ 的加载时织入代理通过 aop.xml 文件配置。代理会在类路径的 META-INF 目录中查找一个或多个 aop.xml 文件,聚合内容以确定织入器配置。

aop.xml 文件包含两个关键部分:

  • Aspects:向织入器定义一个或多个切面,控制织入过程中使用的切面。aspects 元素可选择性包含 includeexclude 元素(默认使用所有定义的切面)。
  • Weaver:向织入器定义选项,指定应织入的类型集合。若未指定 include 元素,则织入器会织入所有可见类型。

配置一个切面到织入器:

<aspectj>
    <aspects>
        <aspect name="com.baeldung.aspectj.AccountAspect"/>
        <weaver options="-verbose -showWeaveInfo">
            <include within="com.baeldung.aspectj.*"/>
        </weaver>
    </aspects>
</aspectj>

如上所示,我们配置了指向 AccountAspect 的切面,且仅 com.baeldung.aspectj 包中的源码会被 AspectJ 织入。

8. 注解式切面

除了熟悉的 AspectJ 代码风格切面声明,AspectJ 5 还支持基于注解的切面声明。我们通常将支持此开发风格的注解集称为“*@AspectJ*”注解。

创建一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Secured {
    public boolean isLocked() default false; 
}

使用 @Secured 注解启用或禁用方法:

public class SecuredMethod {

    @Secured(isLocked = true)
    public void lockedMethod() {
    }

    @Secured(isLocked = false)
    public void unlockedMethod() {
    }
}

接下来,使用 AspectJ 注解风格添加切面,并根据 @Secured 注解属性检查权限:

@Aspect
public class SecuredMethodAspect {
    @Pointcut("@annotation(secured)")
    public void callAt(Secured secured) {
    }

    @Around("callAt(secured)")
    public Object around(ProceedingJoinPoint pjp, 
      Secured secured) throws Throwable {
        return secured.isLocked() ? null : pjp.proceed();
    }
}

关于 AspectJ 注解风格的更多细节,可参考官方文档

使用加载时织入器织入类和切面,并将 aop.xml 放在 META-INF 文件夹下:

<aspectj>
    <aspects>
        <aspect name="com.baeldung.aspectj.SecuredMethodAspect"/>
        <weaver options="-verbose -showWeaveInfo">
            <include within="com.baeldung.aspectj.*"/>
        </weaver>
    </aspects>
</aspectj>

最后,添加单元测试并验证结果:

@Test
public void testMethod() throws Exception {
    SecuredMethod service = new SecuredMethod();
    service.unlockedMethod();
    service.lockedMethod();
}

运行测试时,可通过控制台输出验证切面和类是否成功织入:

[INFO] Join point 'method-call
(void com.baeldung.aspectj.SecuredMethod.unlockedMethod())'
in Type 'com.baeldung.aspectj.test.SecuredMethodTest'
(SecuredMethodTest.java:11)
advised by around advice from 'com.baeldung.aspectj.SecuredMethodAspect'
(SecuredMethodAspect.class(from SecuredMethodAspect.java))

2016-11-15 22:53:51 [main] INFO com.baeldung.aspectj.SecuredMethod 
- unlockedMethod
2016-11-15 22:53:51 [main] INFO c.b.aspectj.SecuredMethodAspect - 
public void com.baeldung.aspectj.SecuredMethod.lockedMethod() is locked

9. 总结

本文介绍了 AspectJ 的基础概念。更多细节可查阅 AspectJ 官网

本文的源代码可在 GitHub 获取。