前言

AOP中有些概念比较容易混淆,借着分享的机会梳理一波。文章包括AOP的概念,AspectJ与Spring AOP的区别,以及Spring AOP的一些语法、场景,同时结合示例说明关于不同通知的执行顺序情况。


AOP概念

AOP

Aspect-oriented Programming)面向切面编程,是一种编程模式,作为面向对象编程的一种补充,即提供了一种将横切关注点从业务中分离出来的方式。广泛用于例如事务管理、安全检查、缓存等等,可以把这些能力叫做横切关注点,与核心业务逻辑无关的、被重复使用的功能。

AOP的思想就是将横切关注点从核心业务逻辑中抽离出来,形成可复用的模块,也称为切面(Aspect),切面定义了在何处(切入点)以及如何(通知)将横切关注点应用到目标代码中。

常见的 AOP 框架包括 AspectJSpring AOP 等。框架的关键在于自动创建AOP代理。
AOP 代理则可分为静态代理和动态代理两大类:
静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;
动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中生成 AOP 动态代理类,因此也被称为运行时增强。

AspectJ AOP

明确一点,Aspect并不是Spring的一部分,它是独立的面向切面的解决方案,是一个框架,同时支持静态代理和动态代理。

AspectJ AOP的一些概念

1、切点(Pointcut):切点用于定义/判断在目标代码中哪些位置可以应用切面的功能。
2、通知(Advice):通知定义了在切点处执行的具体操作,如在方法执行前、执行后、抛出异常时等执行特定的逻辑。
3、切面(Aspect):切面是一个模块化的单元,用于封装横切关注点和通知。
4、织入(Weaving):织入是将切面的功能应用到目标代码中的过程。AspectJ 提供了编译器织入(Compile-Time Weaving)、类加载器织入(Load-Time Weaving)和运行时织入(Runtime Weaving)等织入方式。

切面通过将切点和通知结合在一起,实现了横切关注点的管理和控制。

示例演示

1、引入pom

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

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


<build>
    <plugins>
        <!-- 编译期织入,用以编译.aj文件到.class,在编译期的时候进行织入,这样编译出来的 .class 文件已经织入了我们的代码 -->
        <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>
                        <goal>compile</goal>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2、创建业务类

@Getter
@Setter
public class Account {

    public int balance = 20;

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

}

3、AspectJ实现

iead 需要导入下AspectJ插件

import com.mainto.spring.aop.Account;

// 使用aspect声明
public aspect AccountAspect {

    pointcut callPay(int amount, Account account):
            call(boolean com.mainto.spring.aop.Account.pay(int)) && args(amount) && target(account);

    boolean around(int amount, Account account): callPay(amount, account) {
        if (account.balance < amount) {
            System.out.println("[AccountAspect]拒绝付款!");
            return false;
        } else {
            System.out.println("[AccountAspect]支付成功 " + amount);
        }
        return proceed(amount, account);
    }

}

文件命名为 .aj

4、测试

public class AspectTest {
    public static void main(String[] args) {
        testCompileTime();
    }

    public static void testCompileTime() {
        Account account = new Account();
        System.out.println("==================");
        account.pay(10);
        account.pay(50);
        System.out.println("==================");
    }
}

输出结果:
==================
[AccountAspect]支付成功 10
[AccountAspect]拒绝付款!
==================

5、看下编译后的class

public static void main(String[] args) {
    testCompileTime();
}

public static void testCompileTime() {
    Account account = new Account();
    System.out.println("==================");
    byte var1 = 10;
    pay_aroundBody1$advice(account, var1, AccountAspect.aspectOf(), var1, account, (AroundClosure)null);
    byte var3 = 50;
    pay_aroundBody3$advice(account, var3, AccountAspect.aspectOf(), var3, account, (AroundClosure)null);
    System.out.println("==================");
}

可以看出来,示例中通过maven插件,使得AspectJ在编译阶段便进行了织入。

AspectJ引入供学习了解,更轻量的实现可以参考Spring AOP

Spring AOP

Spring AOP是基于代理的轻量级AOP框架,运行时的动态代理实现,Spring AOP并不是完全的Spring的生态,是基于AspectJ和IoC容器的整合。Spring AOP实现了AspectJ的Annotation,使用了@Aspect来定义切面,使用
@Pointcut来定义切入点,使用Advice的注解来定义增强处理。虽然使用了AspectJ的Annotation,但是并没有使用它的编译器和织入器。

Spring AOP一些概念

1、切点(Pointcut):切点用于定义/判断在目标代码中哪些位置可以应用切面的功能。
2、通知(Advice):通知定义了在切点处执行的具体操作,如在方法执行前、执行后、抛出异常时等执行特定的逻辑。
3、切面(Aspect):切面是一个模块化的单元,用于封装横切关注点和通知。在 Spring AOP 中,切面是一个普通的 Java 类。
4、连接点(Join Point):连接点是在应用程序执行过程中与切点匹配的点。在 Spring AOP 中,方法调用是最常见的连接点。
5、代理(Proxy):Spring AOP 使用代理模式来实现切面的织入。它可以通过 JDK 动态代理或 CGLIB 字节码增强技术创建代理对象。

示例演示

1、引入pom

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.3.19</version>
</dependency>

2、定义aop

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;


@Aspect
public class AspectConfiguration {

    @Pointcut("execution(public * *(..))") // 匹配 Join Point
    private void anyPublicMethod() {} // 方法名即 Pointcut 名


    @Around("anyPublicMethod()")         // Join Point 拦截动作
    public Object aroundAnyPublicMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("@Around any public method.");
        return pjp.proceed();
    }

}

3、定义测试类

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
// 激活 Aspect 注解自动代理
@EnableAspectJAutoProxy
public class AspectJAnnotatedPointcutDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AspectJAnnotatedPointcutDemo.class,
                         AspectConfiguration.class);
        // 刷新
        context.refresh();

        AspectJAnnotatedPointcutDemo aspectJAnnotationDemo = context.getBean(AspectJAnnotatedPointcutDemo.class);
        aspectJAnnotationDemo.execute();

        // 关闭
        context.close();
    }

    private void execute() {
        System.out.println("AspectJAnnotatedPointcutDemo execute()...");
    }

}

输出结果:
@Around any public method.
AspectJAnnotatedPointcutDemo execute()…

Spring AOP 与 AspectJ AOP的区别

1、AspectJ是AOP的完整实现,Spring AOP是部分实现。
2、Spring AOP比AspectJ使用起来更加的简单;AspectJ更强大,可以修改目标代码的字节码。
3、Spring AOP整合了AspectJ和Spring IoC容器。
4、Spring AOP仅支持基于代理模式的AOP,AspectJ可以支持3种织入方式(编译、类加载、运行)。
5、Spring AOP仅支持方法级别的PointCut,AspectJ可以在方法级别之外匹配更细粒度的连接点,包括方法执行、构造器执行、方法调用、构造器调用、异常处理、字段访问、静态初始化和对象初始化等。


Spring AOP详解

AOP通知(Advice)类型

上面已经介绍了一些观念,再看下AOP的通知,Spring中通知(Advice)包括5中:
前置通知(Before)
后置通知(After)
返回通知(AfterReturning)
异常通知(AfterThrowing)
环绕通知(Around)

它们的顺序是怎么样的?
如果有多个切面,它们的顺序又是怎么样的?

Spring AOP的使用方式

1、基于注解的方式:通过在切面类和目标类中使用注解来标识切面和切点。(推荐)
2、基于XML配置的方式:通过在 XML 配置文件中定义切面、切点和通知的关系来实现切面的织入。
3、Api编程方式:通过编写代码来手动创建切面、定义切点和通知,并将切面应用到目标对象中。

Advice演示

使用注解的方式

1、定义切面1

@Aspect
@Order(10)
public class AspectConfiguration {

    @Pointcut("execution(public * *())") // 匹配 Join Point,无参数
    private void anyPublicMethod() {} // 方法名即 Pointcut 名


    @Around("anyPublicMethod()")
    public Object aroundAnyPublicMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("@Around any public method. " + pjp.getSignature());
        return pjp.proceed();
    }

    @Before("anyPublicMethod()")
    public void beforeAnyPublicMethod() throws Throwable {
        System.out.println("@Before any public method.");
    }


    @After("anyPublicMethod()")
    public void finalizeAnyPublicMethod() {
        System.out.println("@After any public method.");
    }

    @AfterReturning("anyPublicMethod()")
    public void afterAnyPublicMethod() {
        System.out.println("@AfterReturning any public method.");
    }

}

2、定义切面2

@Aspect
@Order(20)
public class AspectConfiguration2 {

    @Pointcut("execution(public * *())") // 匹配 Join Point,无参数
    private void anyPublicMethod() {} // 方法名即 Pointcut 名

    @Around("anyPublicMethod()")        
    public Object aroundAnyPublicMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("@Around any public method.(2) " + pjp.getSignature());
        return pjp.proceed();
    }

    @Before("anyPublicMethod()")
    public void beforeAnyPublicMethod() throws Throwable {
        System.out.println("@Before any public method.(2)");
    }


    @After("anyPublicMethod()")
    public void finalizeAnyPublicMethod() {
        System.out.println("@After any public method.(2)");
    }

    @AfterReturning("anyPublicMethod()")
    public void afterAnyPublicMethod() {
        System.out.println("@AfterReturning any public method.(2)");
    }

}

3、测试

@Configuration
// 激活 Aspect 注解自动代理
@EnableAspectJAutoProxy
public class AspectJAnnotatedPointcutDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AspectJAnnotatedPointcutDemo.class,
                AspectConfiguration.class,
                AspectConfiguration2.class
        );
        // 刷新
        context.refresh();

        AspectJAnnotatedPointcutDemo aspectJAnnotationDemo = context.getBean(AspectJAnnotatedPointcutDemo.class);
        aspectJAnnotationDemo.execute();

        // 关闭
        context.close();
    }

    public void execute() {
        System.out.println("AspectJAnnotatedPointcutDemo execute()...");
    }

}

输出结果:
@Around any public method. void com.mainto.spring.aop.spring.AspectJAnnotatedPointcutDemo.execute()
@Before any public method.
@Around any public method.(2) void com.mainto.spring.aop.spring.AspectJAnnotatedPointcutDemo.execute()
@Before any public method.(2)

AspectJAnnotatedPointcutDemo execute()…

@AfterReturning any public method.(2)
@After any public method.(2)
@AfterReturning any public method.
@After any public method.

需注意:Spring 4 和 Spring 5关于after和afterReturning的顺序会有所不同,Spring 4 after先执行,Spring 5 after后执行。本文演示使用的Spring 5版本。

小结:
1、单个切面的通知:
around -> before -> target method -> afterReturing/afterThrowing -> after
image-1683556849702

2、多个切面,按优先级高低执行,如@Order值越小优先级越高,优先执行的后结束,即先进后出。
image-1683557160513

PointCut常用方式

1、execution表达式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

详见官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts-examples

示例:
1)、execution(public void com.example.MyService.doSomething()):匹配 com.example.MyService 类中名为 doSomething、无参数且返回类型为 void 的公共方法。
2)、execution(* com.example.MyService.*()):匹配 com.example.MyService 类中的所有无参数方法。

2、自定义注解:@annotation(方法)、@within(类)

Spring AOP 的常见应用场景

1、日志记录:通过 AOP 可以方便地在方法执行前后记录方法的输入参数、返回值等信息,实现全局的日志记录功能。
2、事务管理:AOP 可以在方法执行前后自动开启和提交事务,简化事务管理的代码。
3、缓存管理:通过 AOP 可以在方法执行前先查询缓存,减少对数据库等资源的访问。
4、安全性检查:AOP 可以在方法执行前进行安全性/权限检查,确保用户有足够的权限执行某个操作。
5、性能监控:AOP 可以对方法的执行时间进行统计,帮助开发者分析系统的性能瓶颈。

扩展

Spring aop相关api:
image


总结

Spring AOP 是一个强大而又常用的面向切面编程框架,常用在日常开发中。篇幅有限关于Api的一些介绍后续再扩展。