Spring 如何理解AOP编程思想?

78 阅读4分钟

昨天学习了Spring AOP面向切面编程,对于之前的理解又再次的加深了。

分为几个板块:
1、理解什么是AOP?
2、AOP解决了什么问题?
3、为什么叫面向切面编程?

板块一:理解什么是AOP?

首先AOP是OOP的延续,OOP又为封装、继承、多态。OOP思想是一种垂直纵向的继承体系。虽然OOP编程思想可以解决大多数代码重复的问题,但有一些情况是处理不了的。比如在顶级父类中的多个方法中相同位置出现了重复的代码,OOP就解决不了,这种情况AOP解决。

AOP的主要目的是将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,提高代码的模块化程度。

主要术语

  1. 切面(Aspect)
  • 横切关注点的模块化,例如日志记录、事务管理、权限控制等
  1. 连接点(JoinPoint)
  • 程序执行过程中的特定点,通常是方法的调用或执行
  1. 通知(Advice)
  • 切面在特定连接点采取的动作,包括:
  • @Before:前置通知,在方法执行前执行
  • @After:后置通知,在方法执行后执行
  • @AfterReturning:返回后通知,方法成功返回后执行
  • @AfterThrowing:异常通知,方法抛出异常后执行
  • @Around:环绕通知,包围方法执行
  1. 切入点(Pointcut)
  • 匹配连接点的表达式,定义在哪些方法上应用通知
  1. 目标对象(Target Object)
  • 被一个或多个切面所通知的对象

简单示例

`

常见应用场景

  1. 日志记录 - 统一记录方法调用日志
  1. 事务管理 - @Transactional 注解就是基于AOP实现
  1. 权限控制 - 检查用户权限
  1. 性能监控 - 统计方法执行时间
  1. 异常处理 - 统一异常捕获和处理
  1. 缓存 - 方法结果缓存

板块二:AOP解决了什么问题?

1. 代码重复问题(DRY原则)

问题场景: 没有AOP时,需要在每个方法中重复写相同的代码 使用AOP后: 代码简洁,关注业务逻辑

2. 关注点分离

横切关注点混杂在业务代码中,导致代码难以维护:

  • 🔴 业务逻辑:用户创建、订单处理
  • 🔵 技术关注点:日志、事务、安全、缓存

AOP将这些关注点分离,让每个模块专注自己的职责。

3. 可维护性问题

没有AOP: 修改横切逻辑需要改动数百个文件

// 需求:所有方法执行前都要打印IP地址
// 没有AOP:需要修改100个文件,添加1000行代码

使用AOP: 只需修改一个切面类

@Aspect
@Component
public class IpLoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logIp(JoinPoint joinPoint) {
        String ip = RequestUtils.getClientIp();
        logger.info("请求IP: " + ip);
    }
}
// 一处修改,全局生效

4. 业务代码污染问题

问题: 技术性代码污染业务逻辑,降低可读性 使用AOP: 业务逻辑清晰明了

@Transactional
@PreAuthorize("hasRole('USER')")
public Order createOrder(@Valid Order order) {
    // 纯粹的业务逻辑
    return orderDao.save(order);
}

5. 动态功能增强问题

不修改原有代码的情况下,动态添加新功能(开闭原则)

// 需求:为所有接口添加性能监控
// 不需要修改任何业务代码,只需添加一个切面
@Aspect
@Component
public class PerformanceMonitorAspect {
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long time = System.currentTimeMillis() - start;
        
        // 上报监控系统
        metricsService.record(pjp.getSignature().getName(), time);
        return result;
    }
}

板块三:为什么叫面向切面编程?

让我用图解的方式来解释:

"切面"的形象理解

传统OOP的视角 - 纵向结构

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ UserService │  │ OrderService│  │ ProductSvc  │
├─────────────┤  ├─────────────┤  ├─────────────┤
│ 日志记录    │  │ 日志记录    │  │ 日志记录    │
│ 权限检查    │  │ 权限检查    │  │ 权限检查    │
│ 事务管理    │  │ 事务管理    │  │ 事务管理    │
│ 性能监控    │  │ 性能监控    │  │ 性能监控    │
│ 业务逻辑    │  │ 业务逻辑    │  │ 业务逻辑    │
│ 异常处理    │  │ 异常处理    │  │ 异常处理    │
└─────────────┘  └─────────────┘  └─────────────┘

每个类都是一个纵向的柱子,重复的功能散落在各个类中。

AOP的视角 - 横向切入

                 日志记录(Logging Aspect)
═══════════════════════════════════════════════
                 权限检查(Security Aspect)
═══════════════════════════════════════════════
                 事务管理(Transaction Aspect)
═══════════════════════════════════════════════

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ UserService │  │ OrderService│  │ ProductSvc  │
├─────────────┤  ├─────────────┤  ├─────────────┤
│ 业务逻辑    │  │ 业务逻辑    │  │ 业务逻辑    │
│   createUser│  │  createOrder│  │ addProduct  │
│   updateUser│  │  payOrder   │  │ updatePrice │
└─────────────┘  └─────────────┘  └─────────────┘

为什么叫"切面"?

1. "切"

就像用刀横向切开一个蛋糕: 从侧面看蛋糕:

    ━━━━━━━━━━━  ← 奶油层(日志)
    ▓▓▓▓▓▓▓▓▓▓  ← 蛋糕体(业务)
    ━━━━━━━━━━━  ← 果酱层(事务)
    ▓▓▓▓▓▓▓▓▓▓  ← 蛋糕体(业务)
    ━━━━━━━━━━━  ← 巧克力层(权限)
    
    横切关注点就像这些"层"
    横向穿过多个业务模块  

2. "面"

这些横向的功能形成了一个横截面:
想象一个三维空间:

       Y轴(业务功能)
        ↑
        │  ProductService
        │  OrderService  
        │  UserService
        │
        └────────────────→ X轴(时间 / 调用顺序)
       ╱
      ╱  Z轴(横切关注点)
     ↙
        - 日志
        - 事务
        - 权限
        - 缓存

"切面" = 在业务功能的"柱子"上横向切一刀,切出的那个平面。

代码层面的体现

// 业务代码(纵向)
class UserService {
    void createUser() { }
    void deleteUser() { }
}

class OrderService {
    void createOrder() { }
    void cancelOrder() { }
}

// 切面代码(横向切入)
@Aspect
class LoggingAspect {
    // 这个切面"横向切入"所有Service的所有方法
    @Around("execution(* *..Service.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) {
        // 横向织入日志逻辑
        log("开始");
        Object result = pjp.proceed();
        log("结束");
        return result;
    }
}