您现在的位置是:首页 >技术教程 >Spring AOP 实践指南网站首页技术教程

Spring AOP 实践指南

訾博ZiBo 2024-06-17 10:26:13
简介Spring AOP 实践指南

Spring AOP 实践指南

一、概述

1、简介

Spring AOP(面向切面编程)是Spring框架的一个关键特性之一。它提供了一种在应用程序中实现横切关注点的方法,这些关注点通常会散布在应用程序的多个模块中,并且与核心业务逻辑存在交叉。

AOP通过将关注点从它们所影响的对象中分离出来,使得开发人员能够更好地关注业务逻辑的实现,而不必担心与之交织在一起的横切关注点。在Spring中,这些横切关注点可以包括日志记录、安全性、事务管理、性能监控等等。

Spring AOP的核心概念是切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)和引入(Introduction)。

  • 切面(Aspect):切面是一个模块化单元,它是横切关注点的实现。它由切点和通知组成。
  • 连接点(Join Point):连接点是在应用程序执行期间可以插入切面的点。例如,方法调用、方法执行、异常处理等都可以是连接点。
  • 通知(Advice):通知是在连接点上执行的代码。它定义了在何时、何地和如何应用切面。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(After Returning)和异常通知(After Throwing)。
  • 切点(Pointcut):切点定义了在何处应用通知。通过使用切点表达式,可以指定连接点的匹配规则。
  • 引入(Introduction):引入允许我们向现有的类添加新的接口和实现,以便可以将新功能引入到这些类中。

Spring AOP使用代理模式来实现横切关注点的管理。在运行时,Spring会动态地创建代理对象,将通知织入到目标对象的方法调用中。

通过使用Spring AOP,开发人员可以更好地实现关注点的模块化和重用,从而提高代码的可维护性和可扩展性。

2、官方资料

Spring 官网:https://spring.io/

Spring 文档:https://docs.spring.io/spring-framework/reference/

3、本文档说明

本文档基于 Spring Boot 以及注解使用 Spring AOP 功能。

二、基本使用

1、引入依赖

<!-- aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、定义切面

在 Spring 管理的 Bean 类上使用 @Aspect 注解就可以定义一个切面。

package com.example.demo.aspects;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {
}

3、定义切点

在切面类的方法使用 @Pointcut 注解来定义切点,然后在通知注解中使用方法签名来指定切点。

切点表达式用来匹配切入的目标类和方法。目标类只能是 Spring 容器管理的类,切面只能切入 Bean 中的方法。

package com.example.demo.aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 环绕通知
     *
     * @param pjp 切点
     * @return Object
     * @throws Throwable 异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("before");
        Object result = pjp.proceed();
        System.out.println("after");
        return result;
    }
}

4、创建 HelloController

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zibo
 * @date 2023/5/15 12:55
 * @slogan 真正的大师永远怀着一颗学徒的心。——易大师
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping("/world")
    public String helloWorld() {
        System.out.println("hello world");
        return "Hello World!";
    }

}

5、启动项目,访问测试

访问地址:http://localhost:8080/hello/world

# 响应结果
Hello World!

# 控制台
before
Hello World!
after

三、通知

1、概述

五种通知

AOP 中的通知是基于连接点(Join point)业务逻辑的一种增强,Spring AOP 提供了下面五种通知类型:

  • Before advice(前置通知):连接点前面执行,不能终止后续流程,除非抛异常
  • After returning advice(后置通知):连接点正常返回时执行,有异常不执行
  • Around advice(环绕通知):围绕连接点前后执行,也能捕获异常处理
  • After advice(最终通知):连接点退出时执行,无论是正常退出还是异常退出
  • After throwing advice(异常通知):连接点方法抛出异常时执行

AOP 的连接点一般是指目标类的方法,五种通知类型执行的节点如下:

通知的顺序

Spring AOP 中一个目标类可以被多个切面切入,多个切面也可以切入一个目标类。

使用 @Order 注解来指定切面的优先级,来控制切面的执行顺序。

在注册切面 Bean 的时候指定 @Order,如下:

@Order(1)
@Aspect
@Component
public class FirstAspect {
    // ......
}

2、通知方法接受的参数

  1. JoinPoint:JoinPoint是Spring AOP中表示连接点的对象。它提供了访问连接点的信息,如目标对象、方法签名、方法参数等。通过JoinPoint参数,可以获取有关当前正在执行的连接点的信息。
  2. ProceedingJoinPoint:ProceedingJoinPoint是JoinPoint的一个子接口,它只在使用环绕通知时才会使用。它提供了proceed()方法,用于执行连接点方法。ProceedingJoinPoint参数可以用于在环绕通知中控制连接点方法的执行。
  3. org.aspectj.lang.JoinPoint.StaticPart:JoinPoint.StaticPart表示连接点的静态部分。它提供了与JoinPoint相同的信息,但不提供对连接点方法的执行控制。
  4. org.aspectj.lang.Signature:Signature表示连接点方法的签名。它提供了对方法名称、修饰符、返回类型、参数类型等的访问。
  5. org.aspectj.lang.ProceedingJoinPoint.StaticPart:ProceedingJoinPoint.StaticPart是ProceedingJoinPoint的静态部分。它提供了与ProceedingJoinPoint相同的信息,但不提供对连接点方法的执行控制。
  6. org.aspectj.lang.JoinPoint.EnclosingStaticPart:JoinPoint.EnclosingStaticPart表示连接点所在的静态部分。它提供了与JoinPoint相同的信息,但是可以用于获取连接点所在的类或切面的信息。

这些参数可以根据需要选择性地在通知方法中使用,以获取关于连接点和方法的相关信息。

3、前置通知

代码实现

package com.example.demo.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 前置通知
     *
     * @param jp 切点
     */
    @Before("pointcut()")
    public void beforeAdvice(JoinPoint jp) {
        System.out.println("前置通知");
        // 获取方法名称
        String methodName = jp.getSignature().getName();
        System.out.println("方法名称:" + methodName);
    }
}

控制台打印内容

前置通知
方法名称:helloWorld
hello world # 此句的方法内部打印内容

4、后置通知

代码实现

package com.example.demo.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 后置通知
     *
     * @param jp 切点
     * @param result 返回值
     */
    @AfterReturning(pointcut = "pointcut()", returning = "result")
    public void afterReturningAdvice(JoinPoint jp, Object result) {
        System.out.println("后置通知");
        // 获取方法名称
        String methodName = jp.getSignature().getName();
        System.out.println("方法名称:" + methodName);
        // 获取返回值
        System.out.println("返回值:" + result);
    }

}

控制台打印内容

hello world # 此句的方法内部打印内容
后置通知
方法名称:helloWorld
返回值:Hello World!

5、环绕通知

代码实现

package com.example.demo.aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 环绕通知
     *
     * @param proceedingJoinPoint 连接点
     * @return 连接点方法的返回值
     * @throws Throwable 可能抛出的异常
     */
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("环绕通知 - 前置逻辑");
        // 获取方法名称
        String methodName = proceedingJoinPoint.getSignature().getName();
        System.out.println("方法名称:" + methodName);

        // 执行连接点方法
        Object result = proceedingJoinPoint.proceed();

        System.out.println("环绕通知 - 后置逻辑");
        // 可以对返回值进行处理或修改
        System.out.println("返回值:" + result);

        return result;
    }

}

控制台打印内容

环绕通知 - 前置逻辑
方法名称:helloWorld
hello world # 此句的方法内部打印内容
环绕通知 - 后置逻辑
返回值:Hello World!

6、最终通知

代码实现

package com.example.demo.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 最终通知
     *
     * @param jp 切点
     */
    @After("pointcut()")
    public void afterAdvice(JoinPoint jp) {
        System.out.println("最终通知");
        // 获取方法名称
        String methodName = jp.getSignature().getName();
        System.out.println("方法名称:" + methodName);
    }

}

控制台打印内容

hello world # 此句的方法内部打印内容
最终通知
方法名称:helloWorld

7、异常通知

代码实现

package com.example.demo.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
     */
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 异常通知
     *
     * @param jp        切点
     * @param exception 异常对象
     */
    @AfterThrowing(pointcut = "pointcut()", throwing = "exception")
    public void afterThrowingAdvice(JoinPoint jp, Exception exception) {
        System.out.println("异常通知");
        // 获取方法名称
        String methodName = jp.getSignature().getName();
        System.out.println("方法名称:" + methodName);
        // 获取异常信息
        System.out.println("异常信息:" + exception.getMessage());
    }

}

修改 HelloController 制造异常

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zibo
 * @date 2023/5/15 12:55
 * @slogan 真正的大师永远怀着一颗学徒的心。——易大师
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping("/world")
    public String helloWorld() {
        System.out.println("hello world");
        int num = 10 / 0;
        return "Hello World!";
    }

}

控制台打印内容

hello world # 此句的方法内部打印内容
异常通知
方法名称:helloWorld
异常信息:/ by zero

四、切点表达式

1、概述

概述

切入点指示符用来指示切入点表达式目的,在 Spring AOP 中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符,切入点表达式可以使用 &&、||、!来组合切入点表达式,还可以使用类型匹配的通配符来进行匹配。

通配符

类型匹配通配符说明
*表示匹配任何数量字符。示例:java.*.String,表示匹配 java 包下的任何"一级子包"下的 String 类型; 如匹配 java.lang.String,但不匹配java.lang.ss.String
表示任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。示例:java…* ,表示匹配java包及任何子包下的任何类型; 如匹配java.lang.String、java.lang.annotation.Annotation
+仅能作为后缀放在类型模式后边,匹配指定类型的子类型;

2、execution

简介

execution 切点表达式用于定义切点的匹配规则,根据方法的修饰符、返回类型、方法名、参数类型和异常类型等来进行匹配。

语法

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

解释

  • modifiers-pattern:方法的修饰符,如 publicprivate 等。可选。
  • ret-type-pattern:方法的返回类型。使用 * 表示任意返回类型,使用完全限定的类名表示具体的返回类型。必选。
  • declaring-type-pattern:方法所在的类或接口。使用完全限定的类名表示具体的类或接口。可选。
  • name-pattern:方法名,使用 * 表示任意方法名。必选。
  • param-pattern:方法的参数类型。使用 * 表示任意参数类型,使用完全限定的类名表示具体的参数类型。可选。
  • throws-pattern:方法抛出的异常类型。使用完全限定的类名表示具体的异常类型。可选。

示例

匹配 public 方法:

execution(public * *(..))

匹配名称以 set 开头的方法:

execution(* set*(..))

匹配指定类的所有方法:

execution(* com.example.demo.service.UserService.*(..))

匹配指定包及其子包下的类或接口的所有方法:

execution(* com.example.demo.service..*(..))

匹配带有特定注解的方法:

execution(@com.example.demo.annotation.Loggable * *(..))

匹配返回类型为指定类型的方法:

execution(java.util.List<com.example.demo.model.User> com.example.demo.service.UserService.*(..))

这些示例展示了不同的 execution 切点表达式的用法,你可以根据具体的需求和要匹配的方法特征来定义切点表达式。

3、within

简介

within 切点表达式用于定义切点的作用范围,根据类型(类或接口)来匹配其中的方法执行。

语法

within(type-pattern)

示例

匹配指定类中的所有方法:

within(com.example.demo.service.UserService)

匹配指定包及其子包下的所有类或接口的方法:

within(com.example.demo.service..*)

匹配指定包中的所有类或接口的方法:

within(com.example.demo.service.*)

匹配指定包及其子包下的所有类的所有方法:

within(com.example.demo..*)

这些示例展示了使用 within 切点表达式的一些常见用法,你可以根据具体的需求和要匹配的类型来定义切点表达式。

注意

需要注意的是,within 切点表达式只能匹配到类型级别,无法直接匹配到具体的方法。

4、this

简介

this 切点表达式用于匹配当前代理对象所实现的接口类型,并选择这些接口中定义的方法作为切点。

语法

this(type)

示例

这个示例表示匹配当前代理对象所实现的 com.example.demo.service.UserService 接口中定义的所有方法。

this(com.example.demo.service.UserService)

注意

需要注意的是,this 切点表达式只能匹配到当前代理对象实现的接口方法,并不包括其实现类或其他接口的方法。

5、target

简介

target 切点表达式用于匹配目标对象的类型,并选择这些类型中定义的方法作为切点。

语法

target(type)

示例

这个示例表示匹配目标对象的类型为 com.example.demo.service.UserService,即选择目标对象为该类型的所有方法作为切点。

target(com.example.demo.service.UserService)

注意

需要注意的是,target 切点表达式匹配的是目标对象的类型,而不是当前代理对象的类型。这意味着它会选择目标对象的方法,而不考虑当前代理对象的实现类或其他接口的方法。

6、args

简介

args 切点表达式用于匹配方法的参数类型,并选择具有匹配参数类型的方法作为切点。

语法

args(type-pattern)

示例

匹配带有一个整型参数的方法:

args(int)

匹配带有任意参数类型的方法:

args(*)

匹配带有一个字符串参数的方法:

args(java.lang.String)

匹配带有两个整型参数的方法:

args(int, int)

注意

需要注意的是,args 切点表达式仅匹配参数类型,而不考虑参数名称。它只选择具有匹配参数类型的方法,而不限制参数的个数或顺序。

7、bean

简介

bean 切点表达式用于匹配 Spring 容器中的 Bean 名称,并选择具有匹配名称的 Bean 的方法作为切点。

语法

bean(beanNamePattern)

示例

匹配名称为 “userService” 的 Bean:

bean(userService)

匹配名称以 “service” 结尾的 Bean:

bean(*Service)

匹配名称以 “service” 开头并且包含 “impl” 的 Bean:

bean(service*impl)

注意

需要注意的是,bean 切点表达式匹配的是 Spring 容器中的 Bean 名称,而不是具体的类名或接口名。

8、@within

简介

@within 切点表达式用于匹配被特定注解标注的类及其子类中定义的方法作为切点。

语法

@within(annotation-type)

示例

@within(org.springframework.stereotype.Service)

这个示例表示匹配被 @Service 注解标注的类及其子类中定义的所有方法作为切点。

具体代码示例

package com.example.demo.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * 切面
 */
@Aspect
@Component
public class DemoAspect {

    /**
     * 异常通知
     *
     * @param jp 切点
     */
    @Before("@within(service)")
    public void beforeAdvice(JoinPoint jp, Service service) {
        System.out.println("前置通知");
        // 获取方法名称
        String methodName = jp.getSignature().getName();
        System.out.println("方法名称:" + methodName);
        // 获取注解的值
        String value = service.value();
        System.out.println("注解的值:" + value);
    }

}

9、@target

简介

@target 切点表达式用于匹配目标对象所属的类上标注的注解类型,并选择这些类中定义的方法作为切点。

语法

@target(annotation-type)

示例

这个示例表示匹配目标对象所属的类上标注了 @Service 注解的所有方法作为切点。

@target(org.springframework.stereotype.Service)

注意

需要注意的是,@target 切点表达式匹配的是目标对象所属的类上的注解,而不是当前代理对象所属的类上的注解。它会选择目标对象所属的类中定义的方法,而不考虑当前代理对象的实现类或其他接口的方法。

10、@annotation

简介

@annotation 切点表达式用于匹配被特定注解标注的方法,并选择这些方法作为切点。

语法

@annotation(annotation-type)

示例

这个示例表示匹配被 @RequestMapping 注解标注的方法作为切点。

@annotation(org.springframework.web.bind.annotation.RequestMapping)

注意

需要注意的是,@annotation 切点表达式匹配的是方法上的注解,而不是类级别的注解。它会选择被注解标注的方法,而不包括其他方法或类级别的注解。

11、@args

简介

@args 切点表达式用于匹配方法参数上具有特定注解的方法,并选择这些方法作为切点。

语法

@args(annotation-type)

示例

这个示例表示匹配方法参数上具有 @PathVariable 注解的方法作为切点。

@args(org.springframework.web.bind.annotation.PathVariable)

注意

需要注意的是,@args 切点表达式匹配的是方法参数上的注解,而不是方法本身或类级别的注解。它会选择具有特定注解的方法,而不包括其他方法或类级别的注解。

五、切点表达式组合

1、概述

切点表达式的组合可以使用逻辑运算符 &&(与)、||(或)、!(非)来组合多个切点表达式。括号可以用于明确定义优先级和逻辑关系。

2、示例

使用逻辑运算符 &&(与):

execution(public * com.example.service.*Service.*(..)) && @within(org.springframework.stereotype.Service)

该示例表示匹配包名为 com.example.service 的类中,被 @Service 注解标注的方法。

使用逻辑运算符 ||(或):

execution(public * com.example.controller.*Controller.*(..)) || execution(public * com.example.service.*Service.*(..))

该示例表示匹配包名为 com.example.controller 的类中的方法或者包名为 com.example.service 的类中的方法。

使用逻辑运算符 !(非):

!execution(public void com.example.controller.AdminController.logout())

该示例表示匹配除了 com.example.controller.AdminController 类中的 logout 方法之外的所有方法。

使用括号进行优先级和逻辑关系的定义:

(execution(public * com.example.service.*Service.*(..)) && @within(org.springframework.stereotype.Service)) || execution(public * com.example.controller.*Controller.*(..))

该示例表示匹配被 @Service 注解标注的 com.example.service 包中的类的方法,以及匹配 com.example.controller 包中的类的方法。

六、注意事项

  1. AOP代理:
    • Spring AOP 使用代理来实现切面的织入。默认情况下,Spring AOP 使用基于代理的 AOP,其中代理对象包装了目标对象,并在方法调用时执行切面逻辑。代理可以通过 JDK 动态代理或 CGLIB 生成。
    • 注意选择正确的代理模式,例如,当目标对象实现接口时,使用 JDK 动态代理,否则使用 CGLIB 代理。
  2. 切面优先级:
    • 多个切面可以应用于同一个方法或类,因此需要了解切面的优先级。可以通过实现 Ordered 接口或使用 @Order 注解来指定切面的执行顺序。
  3. 切点匹配性能:
    • 切点表达式的复杂度会影响性能。过于复杂的切点表达式可能导致切面执行的性能下降。尽量使用简单且高效的切点表达式,避免不必要的复杂性。
  4. 环绕通知的注意事项:
    • 环绕通知是最强大的通知类型,可以完全控制目标方法的执行。但在使用环绕通知时,需要确保调用 ProceedingJoinPoint 对象的 proceed() 方法,以继续执行目标方法。否则,目标方法将被阻止执行。
  5. 异常处理:
    • 当目标方法抛出异常时,可以使用异常通知来捕获和处理异常。异常通知可以帮助你在方法发生异常时执行额外的逻辑。
  6. Spring AOP 的限制:
    • Spring AOP 只能应用于 Spring 容器管理的 bean 上。它无法拦截自调用的方法、静态方法或无法通过 Spring 容器管理的对象。
  7. Proxy vs. AspectJ:
    • Spring AOP 提供了一种简化的方式来实现切面编程,但其功能相对有限。如果需要更高级的切面功能,例如在构造器、私有方法上织入切面,或者更细粒度的控制,可以考虑使用 AspectJ,它提供了更丰富和灵活的切面编程功能。
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。