代码的隐形守护者:Spring AOP 是如何做到的?

如何使用AOP

用 "打针" 场景一次性记住 AOP 核心概念

想象你去医院打针的过程:

目标对象(Target)

就是 "你"(被打针的人),核心业务是 "生病看病"。

连接点(Join Point)

你身体上所有能打针的地方(胳膊、屁股等所有可能的位置)。

用打针场景重新理解「切点(Pointcut)」

还是以打针为例,但更贴近@Pointcut注解和表达式的作用:

你去医院打针时,护士不会随便找个地方扎,而是要按「规则」精准定位 ------ 这个「规则」就是切点

比如医院规定: 「只给儿科发烧 患者,在胳膊三角肌退烧针 」 这里的「儿科患者 + 发烧 + 胳膊三角肌 + 退烧针」就是一套「切点规则」,对应 AOP 中@Pointcut的表达式逻辑。

对应到代码中的切点定义

@Pointcut注解就像医院的「打针规则手册」,而execution 表达式就是手册里的具体条款:

java 复制代码
// 例:拦截所有Controller中public方法的切点
@Pointcut("execution(public * cn.soboys.controller.*.*(..))")
public void controllerPointcut() {}

这个表达式的含义,对应到打针场景就是:

  • execution:「在打针动作执行时生效」(方法执行时触发);

  • public :「不管是哪种针(肌肉针 / 静脉针),只要是公开给患者的操作」(任意返回值的 public 方法);

  • cn.soboys.controller.*:「只针对儿科诊室里的所有病床」(指定包下的所有类);

  • *(..):「不管患者年龄、体重如何,只要是打针操作都算」(任意方法名,任意参数)。

切点的核心作用:精准筛选,可重用

  • 精准筛选 :就像医院用「儿科 + 发烧 + 退烧针」筛出需要特殊处理的患者,execution表达式通过「返回值 + 类名 + 方法名 + 参数」精准定位要拦截的方法(比如只拦截UserController里带@PostMapping的方法)。
  • 可重用 :定义一次@Pointcut("xxx"),后续所有通知(前置 / 后置等)都能直接引用这个切点,就像医院把「儿科退烧针规则」写在手册里,所有护士都能按同一套规则执行,不用每次重新定义。

通知(Advice)

打针的具体动作:

  • 前置通知:打针前 "用酒精棉消毒";
  • 后置通知:打完后 "贴创可贴";
  • 环绕通知:"按住皮肤→扎针→推药→拔针"(全程包围核心动作);
  • 异常通知:如果 "打针时出血了",医生会额外 "止血"。

切面(Aspect)

医生的 "打针流程手册",里面写清了 "在胳膊三角肌(切点)打针,步骤是消毒→扎针→贴创可贴(通知)"------切点 + 通知 = 切面

  • 织入(Weaving):医生按照手册给你打针的过程(把流程 "套" 到你身上)。

一句话总结

  • 连接点:所有可能被 "切" 的地方(能打针的所有位置);
  • 切点:实际被 "切" 的地方(选好的打针位置);
  • 通知:"切" 的时候做什么(消毒、扎针等动作);
  • 切面:规定 "在哪里切 + 切了做什么"(完整的打针方案)。

AOP 最适合的场景

记住:所有 "重复出现在多个业务里,但又不是核心业务" 的功能,都该用 AOP。 比如:

  • 日志:每个接口都要记录请求,但日志不是业务本身;
  • 权限:每个敏感操作都要验权限,验权限不是业务目的;
  • 事务:订单、支付等操作都要事务控制,控制事务不是业务核心;
  • 监控:统计每个方法的执行时间,统计不是业务逻辑。

就像打针时,"消毒、贴创可贴" 不是看病的核心(核心是给药),但每个打针流程都需要 ------ 这就是 AOP 要干的事。

案例说明

本案例的主要目标是通过两个不同的业务类(CalculatorUserService),展示 AOP 在实际项目中的应用。具体来说,使用 AOP 实现以下功能:

  1. Calculator 类的所有方法进行日志记录,包括方法调用前、调用后、正常返回和抛出异常时的日志。
  2. UserService 类的方法进行日志记录、数据脱敏和权限检查。

导入依赖

java 复制代码
<!--aop 切面-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

定义业务逻辑类

UserService

java 复制代码
import lombok.Data;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public User getUserById(int id) {
        // 模拟返回用户信息
        return new User(id, "Alice", "alice@example.com");
    }

    public void createUser(User user) {
        // 模拟创建用户
        System.out.println("创建用户: " + user);
    }

    public void updateUser(User user) {
        // 模拟更新用户
        System.out.println("更新用户: " + user);
    }

    public void deleteUser(int id) {
        // 模拟删除用户
        System.out.println("删除用户,ID: " + id);
    }
}


@Data
class User {
    private int id;
    private String name;
    private String email;

    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

意义UserService 类模拟了一个用户服务,包含了获取用户信息、创建用户、更新用户和删除用户的方法。通过对这个类的方法进行 AOP 处理,可以展示 AOP 在更复杂业务场景下的应用,如数据脱敏和权限检查。

定义切面

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

    // 定义切点,拦截UserService类的所有方法
    @Pointcut("execution(* org.example.aopdemo.UserService.*(..))")
    public void allMethods() {}

    // 定义切点,拦截UserService类中createUser和updateUser方法
    @Pointcut("execution(* org.example.aopdemo.UserService.createUser(..)) || execution(* org.example.aopdemo.UserService.updateUser(..))")
    public void userOperations() {}

    // 定义切点,拦截UserService类中getUserById方法
    @Pointcut("execution(* org.example.aopdemo.UserService.getUserById(..))")
    public void getUserOperations() {}

    // 前置通知:在方法执行之前执行
    @Before("allMethods()")
    public void beforeAdvice(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("前置通知:方法 " + methodName + " 被调用,参数为 " + Arrays.toString(joinPoint.getArgs()));
    }

    // 后置通知:在方法执行之后执行,无论是否发生异常
    @After("allMethods()")
    public void afterAdvice(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("后置通知:方法 " + methodName + " 已执行完毕。");
    }

    // 返回通知:在方法正常返回后执行
    @AfterReturning(pointcut = "getUserOperations()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("返回通知:方法 " + methodName + " 返回结果为 " + result);
        // 数据脱敏处理
        if (result instanceof User) {
            User user = (User) result;
            System.out.println("数据脱敏处理:用户信息已脱敏,返回的用户ID为 " + user.getId());
        }
    }

    // 异常通知:在方法抛出异常后执行
    @AfterThrowing(pointcut = "allMethods()", throwing = "exception")
    public void afterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("异常通知:方法 " + methodName + " 抛出异常:" + exception.getMessage());
    }

    // 环绕通知:对createUser和updateUser方法进行权限检查
    @Around("userOperations()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("环绕通知:方法 " + methodName + " 开始执行,进行权限检查...");
        // 模拟权限检查
        if (!hasPermission()) {
            throw new SecurityException("没有权限执行此操作!");
        }
        Object result = joinPoint.proceed(); // 继续执行原方法
        System.out.println("环绕通知:方法 " + methodName + " 执行完成。");
        return result;
    }

    private boolean hasPermission() {
        // 模拟权限检查逻辑
        return true; // 假设当前用户有权限
    }
}

在 AOP(面向切面编程)中,JoinPoint 是一个非常重要的概念,JoinPoint joinPoint 中的 JoinPoint 是一个接口,它代表程序执行过程中的某个特定位置,例如方法调用、异常抛出等。下面详细解释 JoinPoint 的含义、作用以及在提供的代码中的使用方式。

JoinPoint 的含义

JoinPoint 是 AOP 中的一个抽象概念,它表示程序执行过程中可以插入切面代码的点。在 Spring AOP 里,JoinPoint 通常代表方法的执行,因为 Spring AOP 主要基于方法拦截来实现。

JoinPoint 的作用

JoinPoint 提供了一系列方法,用于获取与当前连接点相关的信息,比如:

  • 获取方法签名:可以获取被调用方法的名称、参数类型等信息。
  • 获取方法参数:可以获取传递给被调用方法的实际参数。
  • 获取目标对象:可以获取被调用方法所在的对象实例。

以这个为例子说明:

java 复制代码
  public void beforeAdvice(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getMethod().getName();
        System.out.println("前置通知:方法 " + methodName + " 被调用,参数为 " + Arrays.toString(joinPoint.getArgs()));
    }
  • joinPoint.getSignature()getSignature()JoinPoint 接口的一个方法,用于获取当前连接点的签名信息,返回的是一个 Signature 类型的对象。
  • (MethodSignature):由于我们处理的是方法调用,所以将 Signature 对象强制转换为 MethodSignature 类型,以便获取方法的具体信息。

测试执行

java 复制代码
@SpringBootApplication
public class AopDemoApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);

        UserService userService = context.getBean(UserService.class);

        System.out.println("\n测试getUserById方法:");
        User user = userService.getUserById(1);

        System.out.println("\n测试createUser方法:");
        userService.createUser(new User(2, "Bob", "bob@example.com"));

        System.out.println("\n测试updateUser方法:");
        userService.updateUser(new User(2, "Bob Updated", "bob-updated@example.com"));

        System.out.println("\n测试deleteUser方法:");
        userService.deleteUser(2);
    }
}

这里写的只不过是测试方法,在项目中应该是写到config文件中。直接配置,注入到springboot容器中。

相关推荐
2401_895521346 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare6 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL6 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本8 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole11 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉11 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠12 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet12 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明12 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
Binary-Jeff12 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法