如何理解AOP?带你写一个!

前言

大家好,这里是程序员阿亮!今天来给大家讲一下AOP!

不知道大家是否有过这样的经历,当我们要在项目中加上权限校验、日志处理等公用操作,那么很有可能就需要修改大量的文件,给每一个类、方法都加上许多的重复性的代码,这样很繁杂且没有效率,更难以维护...

这时候,AOP(Aspect-Oriented Programming)就是你的救星。

如果说 IOC 是为了解耦 (把对象创建交出去),那么 AOP 就是为了降噪(把重复的非业务代码抽离出来)。

一、AOP是什么?

AOP(Aspect Oriented Programming) 是一种编程范式,用于将横切关注点(cross-cutting concerns)从业务逻辑中分离出来。横切关注点是指那些跨越多个模块的功能,如日志记录、事务管理、安全控制等。

二、核心术语

这些核心概念实际上不用特意去记,我用一些例子来讲解一下。

1. Aspect(切面)

切面是横切关注点的模块化,包含通知和切入点的组合。

java 复制代码
// 使用@Aspect注解定义切面
@Aspect
@Component
public class LoggingAspect {
    
    // 定义切入点
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}
    
    // 定义通知
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("方法执行前: " + joinPoint.getSignature().getName());
    }
}

2. Join Point(连接点)

程序执行过程中的某个特定点,如方法调用、异常抛出等。在Spring AOP中,连接点总是方法的执行。

java 复制代码
// 连接点示例:UserService中的所有方法都是连接点
@Service
public class UserService {
    
    // 这是一个连接点
    public void createUser(User user) {
        // 业务逻辑
    }
    
    // 这也是一个连接点
    public User getUserById(Long id) {
        return userRepository.findById(id);
    }
}

3. Pointcut(切入点)

匹配连接点的谓词,用于指定在哪些连接点应用通知。

java 复制代码
@Aspect
@Component
public class PointcutExamples {
    
    // 匹配UserService类中的所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {}
    
    // 匹配所有以"create"开头的方法
    @Pointcut("execution(* com.example.service..create*(..))")
    public void createMethods() {}
    
    // 匹配所有返回User类型的方法
    @Pointcut("execution(com.example.model.User com.example.service..*(..))")
    public void userReturnMethods() {}
    
    // 组合切入点
    @Pointcut("userServiceMethods() && createMethods()")
    public void userServiceCreateMethods() {}
}

4. Advice(通知)

在特定连接点执行的动作。有5种类型的通知:

4.1 @Before(前置通知)

在连接点之前执行。

java 复制代码
@Aspect
@Component
public class BeforeAdviceExample {
    
    @Before("execution(* com.example.service.UserService.createUser(..))")
    public void beforeCreateUser(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        System.out.println("准备创建用户,参数: " + Arrays.toString(args));
        
        // 可以进行参数验证
        if (args.length > 0 && args[0] instanceof User) {
            User user = (User) args[0];
            if (user.getName() == null || user.getName().isEmpty()) {
                throw new IllegalArgumentException("用户名不能为空");
            }
        }
    }
}

4.2 @After(后置通知)

在连接点之后执行,无论是否异常。

java 复制代码
@Aspect
@Component
public class AfterAdviceExample {
    
    @After("execution(* com.example.service.UserService.*(..))")
    public void afterAnyUserServiceMethod(JoinPoint joinPoint) {
        System.out.println("方法 " + joinPoint.getSignature().getName() + " 执行完毕");
    }
}

4.3 @AfterReturning(返回通知)

在连接点正常返回后执行。

java 复制代码
@Aspect
@Component
public class AfterReturningAdviceExample {
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.UserService.getUserById(..))",
        returning = "result"
    )
    public void afterGetUserById(JoinPoint joinPoint, User result) {
        System.out.println("获取用户成功: " + result.getName());
        // 可以对返回结果进行处理
        if (result != null) {
            result.setLastAccessTime(new Date());
        }
    }
}

4.4 @AfterThrowing(异常通知)

在连接点抛出异常后执行。

java 复制代码
@Aspect
@Component
public class AfterThrowingAdviceExample {
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        throwing = "ex"
    )
    public void afterUserServiceException(JoinPoint joinPoint, Exception ex) {
        System.out.println("方法 " + joinPoint.getSignature().getName() + 
                          " 抛出异常: " + ex.getMessage());
        
        // 记录异常日志
        logger.error("业务方法异常", ex);
    }
}

4.5 @Around(环绕通知)

包围连接点,在方法执行前后都可执行自定义行为。

java 复制代码
@Aspect
@Component
public class AroundAdviceExample {
    
    @Around("execution(* com.example.service.UserService.*(..))")
    public Object aroundUserServiceMethods(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            System.out.println("方法开始: " + pjp.getSignature().getName());
            
            // 执行目标方法
            Object result = pjp.proceed();
            
            System.out.println("方法结束: " + pjp.getSignature().getName());
            return result;
        } catch (Exception ex) {
            System.out.println("方法异常: " + ex.getMessage());
            throw ex;
        } finally {
            long endTime = System.currentTimeMillis();
            System.out.println("方法执行时间: " + (endTime - startTime) + "ms");
        }
    }
}

5. Target Object(目标对象)

被一个或多个切面通知的对象,也称为被代理对象。

java 复制代码
// 目标对象
@Service
public class UserService {
    
    public void createUser(User user) {
        System.out.println("创建用户: " + user.getName());
    }
    
    public User getUserById(Long id) {
        System.out.println("获取用户ID: " + id);
        return new User(id, "张三");
    }
}

6. Weaving(织入)

将切面应用到目标对象并创建代理对象的过程。织入可以在编译时、类加载时或运行时进行。

java 复制代码
// Spring配置类,启用AOP自动代理
@Configuration
@EnableAspectJAutoProxy // 启用AspectJ自动代理(织入)
public class AppConfig {
    
    @Bean
    public UserService userService() {
        return new UserService();
    }
    
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }
}

三、AOP如何实现?

Spring AOP 的魔法核心只有四个字:动态代理

当你从 Spring 容器(IOC容器)中获取一个 Bean 时,你拿到的根本不是你写的那个类,而是一个被 Spring 偷梁换柱后的"代理对象"

Spring 提供了两种实现动态代理的方式,这是面试的重灾区:

1. JDK 动态代理 (JDK Dynamic Proxy)

  • 原理: 基于 Java反射包 java.lang.reflect.Proxy 实现。

  • 要求: 目标类必须实现接口

  • 工作方式: 代理对象和目标对象实现了同一个接口。

2. CGLIB 动态代理 (Code Generation Library)

  • 原理: 基于 ASM 字节码生成框架。

  • 要求: 目标类不需要实现接口

  • 工作方式: 代理对象是目标对象的子类(继承)。它通过重写父类方法来增强功能。

  • 注意:因为是继承,所以无法代理 final 修饰的类或方法。

Spring 如何选择?

  • 如果目标对象实现了接口,Spring 默认使用 JDK 动态代理

  • 如果目标对象没有实现接口,Spring 强制使用 CGLIB

  • (注:在 Spring Boot 2.x 后,默认配置趋向于强制使用 CGLIB,因为它的性能已经优化得很好,且更稳定)。

四、手写一个简易AOP

假设有一个 UserService:

java 复制代码
public interface UserService {
    void login();
}

public class UserServiceImpl implements UserService {
    public void login() {
        System.out.println("--- 正在执行登录业务逻辑 ---");
    }
}

模拟 JDK 动态代理实现 AOP

java 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class MyAopDemo {

    public static void main(String[] args) {
        // 1. 创建目标对象(房东)
        UserService target = new UserServiceImpl();

        // 2. 创建代理对象(中介)
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // AOP逻辑:前置通知
                        System.out.println("[Before] 开启事务/记录日志...");

                        // 执行真正的业务逻辑
                        Object result = method.invoke(target, args);

                        // AOP逻辑:后置通知
                        System.out.println("[After] 提交事务/清理资源...");
                        return result;
                    }
                }
        );

        // 3. 调用代理对象的方法
        // 注意:这里调用的是 proxy,而不是 target
        proxy.login();
    }
}

实际上就是生成代理对象然后去增强。

实际上Spring也差不多是这样实现的,结合IOC的话就是:

  1. IOC 创建对象: Spring 扫描类,利用反射实例化 Bean。

  2. 后置处理器介入 (BeanPostProcessor):

    Spring 有一个关键的接口叫 AnnotationAwareAspectJAutoProxyCreator(它是一个 BeanPostProcessor)。

    在 Bean 初始化之后,这个处理器会检查:

    • "这个 Bean 匹配上了哪个切点(Pointcut)吗?"

    • "如果有匹配,我就不返回原始对象了,我要创建一个Proxy(代理对象)返回回去。"

  3. 放入容器: 最终放入 Map 单例池的是这个 Proxy 对象


总结

AOP 并不神秘,它就是一个解耦利器

  1. 解决什么问题? 消除重复代码(日志、事务、权限),让业务逻辑更纯粹。

  2. 核心原理? 动态代理

    • 有接口 -> JDK 代理。

    • 没接口 -> CGLIB 代理(子类继承)。

  3. 关键组件? 切面(Aspect)、切点(Pointcut)、通知(Advice)。

一句话总结:
IOC 让对象从"手动 new"变成了"自动注入";
AOP 让代码从"纵向重复"变成了"横向切入"。

相关推荐
大尚来也1 小时前
Python 中使用 ezdxf:轻松读写 DXF 文件的完整指南
开发语言·python
礼拜天没时间.1 小时前
Docker Registry私有仓库搭建与使用
java·运维·docker·云原生·容器·centos
追随者永远是胜利者1 小时前
(LeetCode-Hot100)70. 爬楼梯
java·算法·leetcode·职场和发展·go
瓦特what?2 小时前
希 尔 排 序
开发语言·c++
前路不黑暗@2 小时前
Java项目:Java脚手架项目的阿里云短信服务集成(十六)
android·java·spring boot·学习·spring cloud·阿里云·maven
寒秋花开曾相惜2 小时前
(学习笔记)2.2 整数表示(2.2.3 补码编码)
c语言·开发语言·笔记·学习
海南java第二人2 小时前
Flink运行时组件深度解析:Java工程师的架构设计与实战指南
java·大数据·flink
沐知全栈开发2 小时前
CSS 下拉菜单
开发语言