如何理解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 分钟前
Docker 离线部署
java·spring cloud·docker
techdashen3 分钟前
Rust 社区在 4 月做了什么:项目管理月报解读
开发语言·rust·mfc
十五年专注C++开发4 分钟前
QFluentKit: 一个基于 Qt Widgets 的 Fluent Design 风格 UI 组件库
开发语言·c++·qt·ui·qfluentkit
lly2024065 分钟前
PHP JSON 使用指南
开发语言
沐知全栈开发10 分钟前
jQuery 尺寸
开发语言
Byte Wizard12 分钟前
C语言指针深入浅出5
c语言·开发语言
LucaJu15 分钟前
一次 OOM 线上排查实录
java·jvm·oom·内存溢出
csbysj202015 分钟前
Vue.js 监听属性
开发语言
Hesionberger23 分钟前
LeetCode 101:对称二叉树(多语言解法)
开发语言·python
小陈的进阶之路23 分钟前
Python系列课(11)——PySpark
开发语言·python·ajax