Spring AOP面向切面编程详解

AOP是Spring的另一核心特性,通过面向切面编程实现横切关注点的统一处理。本文详解AOP核心概念、实现原理、常用场景和最佳实践,帮你掌握Spring AOP的精髓。

前言

IOC 解决了对象依赖问题,AOP 解决的是横切关注点问题。

什么意思?比如日志、事务、权限验证,这些功能散布在各个业务方法里,代码重复且难维护。

AOP 就是把这些"横切"的功能抽出来,统一处理。

什么是 AOP

核心概念

AOP(Aspect Oriented Programming)面向切面编程,核心思想是:

把横切关注点从业务逻辑中分离出来,统一处理。

传统方式:

┌─────────────────┐
│   UserService   │
├─────────────────┤
│ + 日志记录      │
│ + 权限检查      │
│ + 事务管理      │
│ + 业务逻辑      │  ← 业务代码被淹没
│ + 异常处理      │
└─────────────────┘

AOP方式:

      横切关注点
┌─────────────────┐
│      日志       │ ─┐
└─────────────────┘  │
┌─────────────────┐  │ 切入
│      权限       │ ─┤
└─────────────────┘  │
┌─────────────────┐  │
│   UserService   │ ←┘
│   (纯业务逻辑)   │
└─────────────────┘
​

AOP 术语

理解 AOP,先搞清楚几个关键概念:

AOP术语对照:

┌─────────────┬─────────────┬─────────────────┐
│   术语      │   英文      │      说明       │
├─────────────┼─────────────┼─────────────────┤
│   切面      │   Aspect    │  功能模块       │
│   切点      │  Pointcut   │  在哪里切入     │
│   通知      │   Advice    │  切入后做什么   │
│   连接点    │ Join Point  │  可以切入的位置  │
│   织入      │   Weaving   │  把切面应用进去  │
│   代理      │   Proxy     │  增强后的对象   │
└─────────────┴─────────────┴─────────────────┘

形象理解:
- 切面 = 一把刀(日志切面、事务切面)
- 切点 = 在哪切(哪些方法需要日志)
- 通知 = 切完干啥(记录日志、开启事务)
- 织入 = 动手术的过程
​

AOP 怎么工作

实际例子

// 原始业务类
@Service
public class UserService {
  
    public User createUser(User user) {
        // 纯业务逻辑
        return userDao.save(user);
    }
  
    public void deleteUser(String id) {
        // 纯业务逻辑
        userDao.delete(id);
    }
}

// 日志切面
@Aspect
@Component
public class LogAspect {
  
    // 切点:UserService的所有公共方法
    @Pointcut("execution(* com.example.UserService.*(..))")
    public void userServiceMethods() {}
  
    // 前置通知:方法执行前记录日志
    @Before("userServiceMethods()")
    public void logBefore(JoinPoint jp) {
        System.out.println("调用方法: " + jp.getSignature().getName());
    }
  
    // 后置通知:方法执行后记录日志
    @After("userServiceMethods()")
    public void logAfter(JoinPoint jp) {
        System.out.println("方法执行完成: " + jp.getSignature().getName());
    }
}
​

运行结果:

调用方法: createUser
方法执行完成: createUser
​

通知类型

Spring AOP 支持 5 种通知类型:

通知类型详解:

1. @Before 前置通知
┌─────────────────┐
│   方法执行前    │ ← 记录参数、权限检查
└─────────────────┘
          │
          ▼
┌─────────────────┐
│   目标方法      │
└─────────────────┘

2. @After 后置通知
┌─────────────────┐
│   目标方法      │
└─────────────────┘
          │
          ▼
┌─────────────────┐
│   方法执行后    │ ← 清理资源、记录结果
└─────────────────┘

3. @AfterReturning 返回后通知
┌─────────────────┐
│   目标方法      │
└─────────────────┘
          │ 正常返回
          ▼
┌─────────────────┐
│  处理返回值     │ ← 可以修改返回值
└─────────────────┘

4. @AfterThrowing 异常通知
┌─────────────────┐
│   目标方法      │
└─────────────────┘
          │ 抛出异常
          ▼
┌─────────────────┐
│   异常处理      │ ← 记录异常、发送告警
└─────────────────┘

5. @Around 环绕通知
┌─────────────────┐
│   前置处理      │
├─────────────────┤
│   目标方法      │ ← 完全控制方法执行
├─────────────────┤
│   后置处理      │
└─────────────────┘
​

切点表达式

切点表达式决定在哪些方法上应用通知:

// 常用切点表达式

// 1. 匹配方法执行
@Pointcut("execution(* com.example.service.*.*(..))")
//           ↑     ↑                    ↑  ↑
//         返回值  包名               类名 方法名(参数)

// 2. 匹配注解
@Pointcut("@annotation(com.example.Log)")
public void logMethods() {}  // 有@Log注解的方法

// 3. 匹配类型
@Pointcut("within(com.example.service..*)")
public void serviceLayer() {}  // service包下的所有类

// 4. 组合表达式
@Pointcut("serviceLayer() && logMethods()")
public void serviceWithLog() {}  // service包且有@Log注解
​

表达式语法:

execution表达式详解:

execution(修饰符 返回值 包名.类名.方法名(参数))

通配符:
*  : 匹配任意字符(一层)
.. : 匹配任意字符(多层)或任意参数
+  : 匹配指定类型及其子类

示例:
execution(* com.example..*.*(..))  
          ↑                ↑     ↑
      任意返回值        任意方法  任意参数
     
execution(public * com.example.service.*Service.get*(String))
          ↑      ↑                            ↑       ↑
       public   任意返回值                 get开头  String参数
​

AOP 实现原理

动态代理机制

Spring AOP 底层使用动态代理:

Spring AOP代理选择:

目标对象实现接口?
        │
   ┌────┴────┐
   │         │
  是        否
   │         │
   ▼         ▼
JDK代理    CGLIB代理
(接口代理)  (类代理)

JDK动态代理:
┌─────────────┐    实现    ┌─────────────┐
│ UserService │ ────────▶ │ UserService │
│ (接口)      │          │   Impl      │
└─────────────┘          └─────────────┘
      ↑                        ↑
      │                        │
   代理对象                  目标对象

CGLIB代理:
┌─────────────┐
│ UserService │ ← 目标类
└─────────────┘
      ↑
      │ 继承
┌─────────────┐
│ UserService │ ← 代理子类
│   $Proxy    │
└─────────────┘
​

代理对象创建过程

AOP代理创建流程:

1. Bean初始化
┌─────────────────────────────────┐
│ Spring创建UserService实例        │
└─────────────────────────────────┘
                │
                ▼
2. AOP处理器介入
┌─────────────────────────────────┐
│ BeanPostProcessor检查是否需要代理 │
└─────────────────────────────────┘
                │
                ▼
3. 查找匹配的切面
┌─────────────────────────────────┐
│ 扫描所有@Aspect,找到匹配的切点  │
└─────────────────────────────────┘
                │
                ▼
4. 创建代理对象
┌─────────────────────────────────┐
│ 使用JDK或CGLIB创建代理对象       │
└─────────────────────────────────┘
                │
                ▼
5. 返回代理对象
┌─────────────────────────────────┐
│ 容器中保存的是代理对象,不是原对象│
└─────────────────────────────────┘
​

常用场景

1. 日志记录

@Aspect
@Component
public class LoggingAspect {
  
    @Around("@annotation(com.example.Loggable)")
    public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
      
        Object result = pjp.proceed();  // 执行目标方法
      
        long end = System.currentTimeMillis();
        System.out.println(pjp.getSignature() + " 耗时: " + (end - start) + "ms");
      
        return result;
    }
}

// 使用
@Service
public class UserService {
  
    @Loggable  // 自动记录执行时间
    public User createUser(User user) {
        return userDao.save(user);
    }
}
​

2. 权限控制

@Aspect
@Component
public class SecurityAspect {
  
    @Before("@annotation(requiresRole)")
    public void checkPermission(JoinPoint jp, RequiresRole requiresRole) {
        String currentUserRole = getCurrentUserRole();
        String requiredRole = requiresRole.value();
      
        if (!currentUserRole.equals(requiredRole)) {
            throw new SecurityException("权限不足,需要: " + requiredRole);
        }
    }
}

// 使用
@Service
public class AdminService {
  
    @RequiresRole("ADMIN")  // 需要ADMIN权限
    public void deleteUser(String userId) {
        userDao.delete(userId);
    }
}
​

3. 事务管理

// Spring提供的事务切面
@Service
@Transactional  // 类级别事务
public class UserService {
  
    public User createUser(User user) {
        // 自动开启事务
        User saved = userDao.save(user);
      
        // 发送邮件
        emailService.sendWelcome(user.getEmail());
      
        return saved;
        // 自动提交事务(或回滚)
    }
  
    @Transactional(readOnly = true)  // 只读事务
    public User getUser(String id) {
        return userDao.findById(id);
    }
}
​

4. 缓存处理

@Aspect
@Component
public class CacheAspect {
  
    private Map<String, Object> cache = new ConcurrentHashMap<>();
  
    @Around("@annotation(cacheable)")
    public Object handleCache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
        String key = generateKey(pjp);
      
        // 先查缓存
        Object cached = cache.get(key);
        if (cached != null) {
            System.out.println("缓存命中: " + key);
            return cached;
        }
      
        // 执行方法
        Object result = pjp.proceed();
      
        // 结果入缓存
        cache.put(key, result);
        System.out.println("缓存更新: " + key);
      
        return result;
    }
}
​

最佳实践

设计原则

// 1. 切面职责单一
@Aspect
@Component
public class LoggingAspect {  // 只负责日志
    // 日志相关逻辑
}

@Aspect
@Component
public class SecurityAspect {  // 只负责权限
    // 权限相关逻辑
}

// 2. 切点表达式精确
// ❌ 不好:范围太广
@Pointcut("execution(* com.example..*.*(..))")

// ✅ 好:范围精确
@Pointcut("execution(* com.example.service.*Service.*(..))")

// 3. 通知类型选择合适
// 简单前置检查 → @Before
// 需要控制执行 → @Around
// 异常处理 → @AfterThrowing
​

性能考虑

// 1. 避免过度使用AOP
// ❌ 不必要的切面
@Before("execution(* *.get*(..))")
public void logAllGetters() {
    // getter方法通常不需要日志
}

// 2. 切点表达式优化
// ❌ 慢:每次都要匹配
@Pointcut("execution(* com.example..*.*(..)) && @annotation(Log)")

// ✅ 快:先匹配注解
@Pointcut("@annotation(Log)")

// 3. 合理使用@Around
@Around("serviceLayer()")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
    // 前置处理要轻量
    long start = System.currentTimeMillis();
  
    Object result = pjp.proceed();  // 必须调用
  
    // 后置处理也要轻量
    long time = System.currentTimeMillis() - start;
    if (time > 1000) {  // 只记录慢查询
        log.warn("慢方法: {} 耗时: {}ms", pjp.getSignature(), time);
    }
  
    return result;
}
​

小结

AOP 是 Spring 的另一个核心特性,专门解决横切关注点问题。

核心概念:

  • 切面(Aspect):功能模块
  • 切点(Pointcut):在哪里切入
  • 通知(Advice):切入后做什么
  • 织入(Weaving):把切面应用到目标对象

实现原理:

  • JDK 动态代理(有接口)
  • CGLIB 代理(无接口)
  • 在 Bean 初始化时创建代理对象

常用场景:

  • 日志记录、权限控制
  • 事务管理、缓存处理
  • 性能监控、异常处理

最佳实践:

  • 切面职责单一
  • 切点表达式精确
  • 合理选择通知类型
  • 注意性能影响

配合 IOC,AOP 让 Spring 成为了一个完整的企业级框架。IOC 管对象,AOP 管功能,两者结合威力无穷。

更多推荐

章节目录