代码的隐形守护者: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容器中。

相关推荐
小马爱打代码27 分钟前
Spring Boot:将应用部署到Kubernetes的完整指南
spring boot·后端·kubernetes
卜锦元41 分钟前
Go中使用wire进行统一依赖注入管理
开发语言·后端·golang
SoniaChen332 小时前
Rust基础-part3-函数
开发语言·后端·rust
全干engineer2 小时前
Flask 入门教程:用 Python 快速搭建你的第一个 Web 应用
后端·python·flask·web
William一直在路上3 小时前
SpringBoot 拦截器和过滤器的区别
hive·spring boot·后端
小马爱打代码3 小时前
Spring Boot 3.4 :@Fallback 注解 - 让微服务容错更简单
spring boot·后端·微服务
曾曜4 小时前
PostgreSQL逻辑复制的原理和实践
后端
豌豆花下猫4 小时前
Python 潮流周刊#110:JIT 编译器两年回顾,AI 智能体工具大爆发(摘要)
后端·python·ai
轻语呢喃4 小时前
JavaScript :事件循环机制的深度解析
javascript·后端
ezl1fe4 小时前
RAG 每日一技(四):让AI读懂你的话,初探RAG的“灵魂”——Embedding
后端