AspectJ 简介

引言

首先,明确以下几个概念:

  • 切面(Aspect):跨越多个对象的连接点的模块化(简单理解为监视切点的类)。
  • 连接点(Joint Point):程序执行过程中的一个点,例如方法的的执行或者属性的访问
  • 通知(Advice):在切面中特定的连接点采取的行为
  • 切点(Pointcut):通过相关表达式匹配的连接点

​ 一般来讲,实现 AOP 主要有以下两种手段:静态代理、动态代理。

​ 静态代理将要执行的织入点的操作和原有类封装成一个代理对象,在执行到相应的切点时执行代理的操作,这种方式较为笨拙,一般也不会采用这种方式来实现 AOP

​ 动态代理是一种比较好的解决方案,通过在程序运行时动态生成代理对象来完成相关的 Advice 操作。Spring 便是通过动态代理的方式来实现 AOP 的。使用动态代理的方式也有一定的局限性,操作更加复杂,同时相关的类之间也会变得耦合起来。

​ 相比较与使用代理的方式来实现 AOPAspectJ是目前作为 AOP (Aspect-Oriented Programming 面向切面编程) 实现的一种最有效的解决方案。

使用介绍

AspectJ 提供了三种方式来实现 AOP,通过在类加载的不同时间段来完成相关代码的织入以达到目的。

具体有以下三种方式:

  • 编译期(compiler-time)织入:在类进行编译的时候就将相应的代码织入到元类文件的 .class 文件中
  • 编译后(post-compiler)织入:在类编译后,再将相关的代码织入到 .class 文件中
  • 加载时(load-time) 织入:在 JVM 加载 .class 文件的时候将代码织入

需要的依赖项

使用 AspectJ 主要依赖于以下两个依赖:

<properties>
    <!-- AspectJ 依赖的版本 -->
    <aspectj.version>1.9.7</aspectj.version>
</properties>

<!-- 织入的依赖 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
</dependency>

<!-- 运行时需要的依赖 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj.version}</version>
</dependency>

定义具体实体类

定义一个 Account 类,以及相关的一些行为方法

public class Account {
    int balance = 20;
    
    // 提款操作
    public boolean withDraw(int amount) {
        if (balance < amount) return false;
        balance -= amount;

        return true;
    }
}

定义 Aspect

可以通过使用 AspectJ 的语法来定义切面,也可以通过 Java 来定义

  • 使用 AsjpectJ 定义切面

    public aspect AccountAspect { // Aspect,注意概念的对应关系
        final int MIN_BALANCE = 10;
        
        /*
        	定义切点,对应 Pointcut
        	这里的切点定义在调用 Account 对象在调用 witdraw 方法
        */
        pointcut callWithDraw(int amount, Account acc):
        call(boolean Account.withdraw(int)) && args(amount) && target(acc);
    
        /*
        	在上文定义的切点执行之前采取的行为,这就被称之为 Advice
        */
        before(int amount, Account acc): callWithDraw(amount, acc) {
            System.out.println("[AccountAspect] 付款前总额: " + acc.balance);
            System.out.println("[AccountAspect] 需要付款: " + amount);
        }
    
        /*
        	在对应的切点执行前后采取的行为
        */
        boolean around(int amount, Account acc):
        callWithDraw(amount, acc) {
            if (acc.balance < amount) {
                System.out.println("[AccountAspect] 拒绝付款!");
                return false;
            }
            return proceed(amount, acc);
        }
    
        /*
        	对应的切点执行后的采取的行为
        */
        after(int amount, Account balance): callWithDraw(amount, balance) {
            System.out.println("[AccountAspect] 付款后剩余:" + balance.balance);
        }
    }
    
  • 使用 Java 来定义切面

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    @Aspect
    public class ProfilingAspect {
        private final static Logger log = LoggerFactory.getLogger(ProfilingAspect.class);
    
        // 定义切点
        @Pointcut("execution(* org.xhliu.aop.entity.Account.*(..))")
        public void modelLayer() {}
        
        // 在切点执行前后采取的行为,这里是记录方法调用的时间
        @Around("modelLayer()")
        public Object logProfile(ProceedingJoinPoint joinPoint) throws Throwable {
            long startTime = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            log.info("[ProfilingAspect] 方法:【" + joinPoint.getSignature()
                     + "】结束,耗时:" + (System.currentTimeMillis() - startTime));
    
            return result;
        }
    }
    

编译时织入

由于 javac 无法编译 AspectJ,因此首先需要加入相关的 AspectJ 插件来完成编译时的织入:

<!-- 编译时织入的 maven 插件 -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.7</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>
                <!-- use this goal to weave all your main classes -->
                <goal>compile</goal>
                <!-- use this goal to weave all your test classes -->
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

现在,定义一个 Main 方法来执行 Account 的方法:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AspectApplication {
    private final static Logger log = LoggerFactory.getLogger(AspectApplication.class);

    public static void main(String[] args) {
        Account account = new Account();
        log.info("================ 分割线 ==================");
        account.withDraw(10);
        account.withDraw(100);
        log.info("================ 结束 ==================");
    }
}

此时运行 Main 方法(可能会存在缓存,使用 mvn clean 来清理原来的生成文件),会看到类似于如下的输出:

AspectJ 简介

Main 方法所在的类进行反编译,得到类似如下图所示的结果:

AspectJ 简介

可以看到,在生成的 .class 文件中已经添加了 Aspect 的相关内容,因此在运行时会执行在 AccountAspect 中定义的内容。

再查看 Account 编译后的类:

AspectJ 简介

可以看到,Account.class 也已经被织入了一些 Aspect 的内容

以上操作都是在 shell 中完成,因为有的 IDE 将使用自己的一些特有的处理方式而不是使用插件。

# 使用 mvn 来启动相关的主类
mvn exec:java -D"exec.mainClass"="org.xhliu.aop.entity.AspectApplication"

编译后织入

一般来讲,使用编译后的织入方式已经足够了,但是试想一下这样的场景:现在已经得到了一个 SDK,需要在这些 SDK 的类上定义一些切点的行为,这个时候只能针对编译后的 .class 文件进行进一步的织入,或者在加载 .class 时再织入。

在另一个项目中定义一个 UserAccount 的主类

package com.example.aopshare.entity;

public class UserAccount {
    private int balance = 20;

    public UserAccount() {
    }

	public int getBalance(){return this.balance;}

	public void setBalance(int balance){
			this.balance = balance;
	}

    public boolean withDraw(int amount) {
        if (this.balance < amount) {
            return false;
        } else {
            this.balance -= amount;
            return true;
        }
    }
}

将这个项目打包到本地的 maven 仓库

mvn clean package

mvn install

现在就可以直接在当前的项目中引用这个 SDK 了,加入对应的 gav 即可:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>aop-share</artifactId>
    <version>1.0</version>
</dependency>

为这个第三方库的 UserAccount 创建一个 Aspect

import com.example.aopshare.entity.UserAccount;

public aspect UserAccountAspect {
    pointcut callWithDraw(int amount, UserAccount acc):
            call(boolean UserAccount.withDraw(int)) && args(amount) && target(acc);

    before(int amount, UserAccount acc): callWithDraw(amount, acc) {
        System.out.println("[UserAccountAspect] 付款前总额: " + acc.getBalance());
        System.out.println("[UserAccountAspect] 需要付款: " + amount);
    }

    boolean around(int amount, UserAccount acc):
            callWithDraw(amount, acc) {
        if (acc.getBalance() < amount) {
            System.out.println("[UserAccountAspect] 拒绝付款!");
            return false;
        }
        return proceed(amount, acc);
    }

    after(int amount, UserAccount balance): callWithDraw(amount, balance) {
        System.out.println("[UserAccountAspect] 付款后剩余:" + balance.getBalance());
    }
}

分割线——————————————————————————————

准备工作已经完成,现在正式开始实现编译后的织入,只需添加对应的插件即可完成:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.7</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <!-- 要处理的第三方 SDK,只有在这里定义的 SDK 才会执行对应后织入 -->
        <weaveDependencies>
            <weaveDependency>
                <groupId>com.example</groupId>
                <artifactId>aop-share</artifactId>
            </weaveDependency>
        </weaveDependencies>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

现在再运行 Main 类,得到与编译时织入类似的输出:

AspectJ 简介

加载时织入

JVM 加载 Class 对象的时候完成织入。Aspect 通过在启动时指定 Agent 来实现这个功能

加载时织入与编译后织入的使用场景十分相似,因此依旧以上文的例子来展示加载时织入的使用

首先,注释掉使用到的 aspect 编译插件,这回影响到这部分的测试

在项目的 resources 目录下的 META-INF目录(如果没有就创建一个)中,添加一个 aop.xml 文件,具体内容如下所示:

<!DOCTYPE aspectj
        PUBLIC "-//AspectJ//DTD//EN"
        "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <aspects>
        <!-- 使用加载时织入的方式只能通过定义具体的 Aspect 类来实现,因为 AspectJ 无法被 javac 编译 -->
        <aspect name="org.xhliu.aop.entity.UserAspect"/>

        <!-- 要监听的切点所在的包位置,即要执行对应切面方法的位置 -->
        <include within="org.xhliu.aop.entity..*"/>
    </aspects>
</aspectj>

查看监听的 main 方法所在的类,类似下图所示:

AspectJ 简介

与编译时织入和编译后织入不同,加载时织入不会修改原有的 .class 文件

在运行时需要添加相关的代理参数类来实现加载时的织入,具体的参数如下所所示:

# 注意将 -javaagent 对应的代理类修改为本地 maven 仓库对应的
java -javaagent:/home/lxh/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar -jar target/spring-aop-1.0-SNAPSHOT-jar-with-dependencies.jar

具体的输出如下所示:

AspectJ 简介

具体的项目地址:https://github.com/LiuXianghai-coder/Spring-Study

参考:https://javadoop.com/post/aspectj

上一篇:我自己的1024开发工具之1024都没软件


下一篇:SpringBoot中AspectJ的使用