轻松上手Spring AOP,掌握切面编程的核心技巧

Spring框架是我们使用比较多的一个框架,而AOP又是Spring的核心特性之一,本篇文章将介绍一下AOP的切点表达式、通知等特性及如何使用Spring AOP。

AOP 是什么

AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,旨在将横切关注点与核心业务逻辑相分离,以提高代码的模块化性、可维护性和复用性。

在传统的面向对象编程中,程序的功能被模块化为类和方法,但某些功能可能会跨越多个类和方法,如日志记录、事务管理、安全控制等,这些功能不属于核心业务逻辑,但又必须在多个地方重复使用,导致代码重复和耦合性增加。

AOP提供了一种机制,可以将这些横切关注点单独定义,并在需要的地方插入到应用程序中,而不必修改核心业务逻辑。

AspectJ

AspectJ 是一个面向切面的框架,它扩展了Java语言。AspectJ 定义了AOP(面向切面编程) 语法,并拥有一个专门的编译器,用于生成遵守Java字节编码规范的Class文件

AspectJ 可以单独使用,也可以整合到其他框架中。当单独使用AspectJ时,需要使用专门的编译器ajcAspectJ属于静态织入,通过修改代码来实现,包括编译期织入等多种织入时机。

Spring集成AspectJ,可以在Spring中方便的使用AOP。

Spring AOP

Spring AOP核心概念主要包括以下几个方面:

  1. 切面(Aspect):切面是模块化横切关注点的机制,由切入点和通知组成。在Spring AOP中,一个切面可以定义在什么时候、什么地方以及如何应用某种特定的行为到目标对象上。

  2. 连接点(Joinpoint) :连接点是程序执行过程中的一个点,例如方法的调用、字段的访问等。在Spring AOP中,一个连接点总是代表一个方法的执行。连接点是AOP框架可以在其上 "织入" 切面的点。

  3. 通知(Advice) :通知定义了在切入点执行时要执行的代码,它是增强应用到连接点上的行为。通知有多种类型,包括前置通知(Before Advice)后置通知(After Advice)环绕通知(Around Advice)异常通知(After Throwing Advice)返回通知(After Returning Advice) 。这些通知类型决定了增强在连接点上的执行顺序和方式。

  4. 切点(Pointcut):切点用于定义通知应该应用到哪些连接点上。它是一组连接点的集合,这些连接点共享相同的特性。切点表达式用于匹配连接点,从而确定哪些连接点应该接收通知。

  5. 目标对象(Target Object) :被一个或多个切面所通知的对象。也被称为被通知(advised)对象。由于Spring AOP是通过代理模式实现的,因此在运行时,目标对象总是被代理对象所包裹。

  6. 织入(Weaving):织入是将切面应用到目标对象并创建代理对象的过程。这是AOP框架在运行时或编译时完成的核心任务。

  7. AOP代理(AOP Proxy) :AOP框架创建的对象,用于实现切面编程。在Spring中,AOP代理可以是JDK动态代理CGLIB代理

  8. 引入(Introduction) :用于向现有的类添加新的接口和实现,而不需要修改原始类的代码。Introduction允许在不修改现有类结构的情况下,向类引入新的功能和行为。在 AspectJ 社区中,引入称为类型间声明(inter-type declaration)

这些核心概念共同构成了AOP的基础,使得我们能够模块化地处理横切关注点,从而提高代码的可维护性和可重用性。

切点表达式

Pointcut 表达式 是用来定义切入点的规则,它决定了哪些连接点(方法调用或方法执行)将会被通知所影响。在 Spring AOP 中,Pointcut 表达式通常由以下几种规则和通配符组成:

  1. execution(): 用于匹配方法执行的连接点,它是最常用的切点指示器。它基于方法签名进行匹配,可以指定方法的返回类型、包名、类名、方法名以及参数列表等。比如: @Pointcut("execution(* com.example.myapp.service.*.*(..))") 表示匹配com.example.myapp.service包下所有类的所有方法执行。

  2. within(): 匹配指定类型内的方法 执行连接点。它通常用于匹配特定包或类中的所有方法。示例:@Pointcut("within(com.example.myapp.service.*)") 表示表示匹配com.example.myapp.service包下所有类的所有方法的执行。

  3. this(): 匹配当前代理对象 为指定类型的连接点。这用于限制切点只匹配特定类型的代理对象。示例:@Pointcut("this(com.example.myapp.service.MyService)") 表示匹配当前代理对象 类型为com.example.myapp.service.MyService的所有方法的执行。

  4. target(): 匹配目标对象 为制定类型的连接点。与this()不同,target()是基于目标对象类型,而不是代理类型。示例:@Pointcut("target(com.example.myapp.service.MyServiceImpl)") 表示匹配目标对象 类型为com.example.myapp.service.MyServiceImpl的所有方法的执行。

  5. args(): 匹配方法执行时参数为特定类型 的连接点。示例:@Pointcut("args(java.io.Serializable)") 表示匹配方法执行时至少有一个参数是java.io.Serializable类型的连接点。

  6. @annotation(): 匹配执行的方法上 带有指定注解的连接点。示例:@Pointcut("@annotation(com.example.myapp.annotation.MyAnnotation)") 表示匹配执行的方法上带有com.example.myapp.annotation.MyAnnotation注解的连接点。

  7. @target :用于匹配所有带有特定注解的类或接口 。 这个指示器通常与execution表达式结合使用,以进一步细化匹配条件。示例:@Pointcut("@target(com.example.annotation.MyAnnotation)") 表示匹配目标对象 类型上带有com.example.myapp.annotation.MyAnnotation注解的方法执行。

  8. @within :匹配指定类型带有指定注解 的连接点。与within()类似,但它是基于注解而不是包或类。示例: @Pointcut("@within(com.example.myapp.annotation.MyAnnotation)") 表示匹配带有MyAnnotation注解的类的方法执行。

  9. bean() :匹配Spring容器中特定名称 的bean的方法的执行。示例: @Pointcut("bean(myServiceImpl)") 表示匹配Spring容器中名称为myServiceImplbean的方法的执行。

  10. @args() :用于限制匹配的方法的参数必须有指定的注解

带有 @ 符的切点表达式都是需要指定注解的连接点。

这些规则可以通过逻辑运算符(如 &&、||、! )进行组合,以实现更复杂的 Pointcut 匹配规则。我们可以根据自己的需求,灵活地使用这些规则来定义切入点表达式,实现对目标方法的精确匹配和监控。

execution()

execution() 表达式使用的比较多,最复杂的一个表达式,这里重点介绍一下。

语法结构

execution() 表达式的语法结构如下:

java 复制代码
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

其中,各部分的含义如下:

  • modifiers-pattern: 方法的访问修饰符,如 publicprotected 等,可以省略。
  • ret-type-pattern: 方法的返回类型,如 voidint 等。
  • declaring-type-pattern: 方法所属的类的类型模式,可以使用通配符 * 匹配任意字符。
  • name-pattern: 方法的名称模式,可以使用通配符 * 匹配任意字符。
  • param-pattern: 方法的参数模式,包括参数类型和个数。
  • throws-pattern: 方法抛出的异常类型。

示例

  • 所有公共方法的执行
java 复制代码
execution(public * *(..))
  • 名称以 set 开头的所有方法的执行
java 复制代码
execution(* set*(..))
  • AccountService 接口定义的任何方法的执行
java 复制代码
execution(* com.xyz.service.AccountService.*(..))
  • service 包中定义的任何方法的执行
java 复制代码
execution(* com.xyz.service.*.*(..))
  • service 包或其子包之一中定义的任何方法的执行
java 复制代码
execution(* com.xyz.service..*.*(..))
  • 执行指定类型参数的方法
java 复制代码
execution(* com.example.service.MyService.myMethod(String, int))

注意事项

  • execution() 表达式中,通配符 * 可以用来匹配任意字符或任意个数的字符。
  • 使用 execution() 表达式时,需要注意合理地组织表达式,以确保精准地匹配目标方法。
  • 可以通过组合多个条件来更加灵活地定义切点,例如同时匹配方法的访问修饰符、返回类型、类名、方法名等。

总的来说,execution() 方法提供了一种灵活且强大的方式来定义切点表达式,从而精确定位需要添加通知的目标方法。

通知(Advice)类型

在 Spring AOP 中,通知(Advice)是在切入点(Pointcut)上执行的代码。Spring 提供了几种类型的通知,每种类型都对应着在连接点执行前、执行后或抛出异常时执行的不同代码逻辑。这些通知对应着不同的注解,常用的通知注解包括:

  1. @Before: 在方法执行之前执行的通知。它有以下属性:

    • value:要绑定的切点或者切点表达式。
    • argNames: 用于指定连接点表达式中方法参数的名称,以便在通知方法中通过参数名来获取方法参数的值。这样可以在前置通知中访问和处理方法参数的具体数值。该属性即使不指定也能获取参数。
  2. @AfterReturning: 在方法执行成功返回结果后执行的通知。它比 @Before注解多了2个属性:

    • pointcut :作用和value 属性一样,当指定pointcut 时,会覆盖value属性的值。
    • returning:方法返回的结果将被绑定到此参数名,可以在通知中访问方法的返回值。
  3. @AfterThrowing: 在方法抛出异常后执行的通知。它的属性前3个和 @AfterReturning注解一样,多了1个属性:

    • throwing:指定方法抛出的异常将被绑定到此参数名,可以在通知中访问方法抛出的异常。
  4. @After: 在方法执行后(无论成功或失败)执行的通知。属性同 @Before 注解。

  5. @Around: 环绕通知,能够在方法执行前后都可以进行操作,具有最大的灵活性。属性同 @Before 注解。

通知的执行顺序为:@Around -> @Before -> @AfterReturning (不抛异常情况) 或者 @AfterThrowing (抛异常情况) -> @After

这些通知注解可以与 Pointcut 表达式结合使用,实现对目标方法的拦截和处理。通过选择合适的通知类型,开发者可以根据需求在不同的时间点插入自定义的逻辑,实现对方法调用的控制和增强。

如何使用

讲了那么多概念性的东西,下面来看怎么使用Spring AOP。

在Spring 中使用AOP也很简单,主要分3步:

  1. 定义切面
  2. 定义切点
  3. 在具体通知上使用切点

准备阶段

我这里使用的是Springboot 3.1.5jdk 17 ,如果是Springboot低版本的可能需要引入 spring-boot-starter-aop 依赖,高版本的AOP已经包含在spring-boot-starter-web依赖中了:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Spring官网中介绍,使用Spring AOP要在启动类或者配置类中加上 @EnableAspectJAutoProxy 注解开启 AspectJ 注解的支持,在我使用的这个版本中并不需要,如果你的项目中切面未生效可以尝试使用该注解。

定义一个接口,下面用于对这个接口及其实现类进行拦截:

java 复制代码
public interface AopService {

    /**
     * 两数除法
     * @param a
     * @param b
     * @return
     */
    BigDecimal divide(BigDecimal a, BigDecimal b);

    /**
     * 两数加法
     * @param a
     * @param b
     * @return
     */
    BigDecimal add(BigDecimal a, BigDecimal b);
}
java 复制代码
@Service
public class MyAopServiceImpl implements AopService{

    /**
     * 两数除法
     *
     * @param a
     * @param b
     * @return
     */
    @Override
    public BigDecimal divide(BigDecimal a, BigDecimal b) {
        return a.divide(b , RoundingMode.UP);
    }

    /**
     * 两数加法
     *
     * @param a
     * @param b
     * @return
     */
    @Override
    public BigDecimal add(BigDecimal a, BigDecimal b) {
        return a.add(b);
    }
}

定义切面

新建一个类,在类上加上@Aspect 注解,标记该类为切面。

java 复制代码
@Component
@Aspect
public class AspectComponent {
}

定义并使用切点

在切面中使用@Pointcut注解定义切点表达式,然后在通知注解中使用定义好的切点。在该示例中主要对AopService#divide()方法进行拦截。

java 复制代码
@Component
@Aspect
public class AspectComponent {

	/**
     * 匹配AopService接口的divide方法
     */
    @Pointcut("execution(* site.suncodernote.aop.AopService.divide(..))")
    void dividePointCut(){
    }

	/**
     * 匹配AopService接口的divide方法
     */
    @Pointcut("within(site.suncodernote.aop.AopService+)")
    void withinPointCut(){
    }

	/**
     * 匹配AopService接口的add方法 或者 divide方法
     */
    @Pointcut("execution(* site.suncodernote.aop.AopService.add(..)) || execution(* site.suncodernote.aop.AopService.divide(..))")
    void addOrDividePointCut(){
    }


	@Before("dividePointCut()")
    public void beforeDivide(JoinPoint joinPoint){
        System.out.println("---------------------@Before----------------");
        printJoinPoint(joinPoint);
    }

    @After("dividePointCut()")
    public void afterDivide(JoinPoint joinPoint){
        System.out.println("---------------------@After----------------");
        printJoinPoint(joinPoint);
    }

    @AfterReturning(pointcut = "dividePointCut()" , returning = "result")
    public void afterReturningDivide(JoinPoint joinPoint , BigDecimal result){
        System.out.println("---------------------@AfterReturning----------------");
        System.out.println("返回结果="+result);
        printJoinPoint(joinPoint);
    }

    @AfterThrowing(pointcut = "dividePointCut()" , throwing = "e")
    public void afterThrowingDivide(JoinPoint joinPoint ,Exception e){
        System.out.println("---------------------@AfterThrowing----------------");
        System.out.println("异常:"+e.getMessage());
        printJoinPoint(joinPoint);
    }

    @Around("dividePointCut()")
    public Object aroundDivide(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("---------------------@Around----------------");
        printJoinPoint(joinPoint);
        Object[] args = joinPoint.getArgs();
        Object result = null;
        try {
            //执行方法
            result = joinPoint.proceed(args);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("返回值:"+result);
        return result;
    }

    private void printJoinPoint(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        System.out.println("方法名:"+signature.getName());
        System.out.println("方法参数:"+ Arrays.toString(args));
        System.out.println();
    }
}

测试

写个简单的单元测试,调用AopService#divide()方法,然后看一下输出结果。

java 复制代码
@SpringBootTest
public class AOPTest {

    @Resource
    private AopService aopService;

    @Test
    public void testAOP() {
        BigDecimal a = new BigDecimal(1);
        BigDecimal b = new BigDecimal(2);

//        aopService.add(a, b);
        aopService.divide(a, b);
    }
}

测试结果:

java 复制代码
---------------------@Around----------------
方法名:divide
方法参数:[1, 2]

---------------------@Before----------------
方法名:divide
方法参数:[1, 2]

---------------------@AfterReturning----------------
返回结果=1
方法名:divide
方法参数:[1, 2]

---------------------@After----------------
方法名:divide
方法参数:[1, 2]

返回值:1

从测试结果中通知执行的顺序是按照我们上面所说的执行顺序执行的。

总结

本文介绍了Spring AOP的常用的切点表达式、通知注解等,我们可以利用AOP对业务逻辑的各个部分进行隔离,使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。

相关推荐
老张聊数据集成几秒前
数据建模怎么做?一文讲清数据建模全流程
后端
一只爱撸猫的程序猿2 分钟前
创建一个关于智能博物馆导览案例
spring boot·aigc·ai编程
S妖O风F6 分钟前
IDEA报JDK版本问题
java·ide·intellij-idea
Mr. Cao code9 分钟前
使用Tomcat Clustering和Redis Session Manager实现Session共享
java·linux·运维·redis·缓存·tomcat
纪莫11 分钟前
DDD领域驱动设计的理解
java·ddd领域驱动设计
颜如玉20 分钟前
Kernel bypass技术遥望
后端·性能优化·操作系统
一块plus35 分钟前
创造 Solidity、提出 Web3 的他回来了!Gavin Wood 这次将带领波卡走向何处?
javascript·后端·面试
山中月侣42 分钟前
Java多线程编程——基础篇
java·开发语言·经验分享·笔记·学习方法
用户2986985301444 分钟前
C#代码:Word文档加密与解密(Spire.Doc)
后端
海海思思1 小时前
Go结构体字段提升与内存布局完全指南:从报错解析到高效实践
后端