Skip to content

AOP 面向切面编程

一、为什么需要 AOP

传统 OOP 的问题

java
@Service
public class UserService {
    
    public void save(User user) {
        // ========== 记录日志 ==========
        log.info("save user: {}", user);
        
        // ========== 权限校验 ==========
        if (!hasPermission()) {
            throw new SecurityException("No permission!");
        }
        
        // ========== 事务开启 ==========
        TransactionManager.begin();
        
        try {
            // ========== 核心业务逻辑 ==========
            userDao.insert(user);
            
            // ========== 事务提交 ==========
            TransactionManager.commit();
        } catch (Exception e) {
            // ========== 事务回滚 ==========
            TransactionManager.rollback();
            throw e;
        }
    }
}

问题

  • 代码重复(每个方法都要加日志、事务)
  • 业务逻辑与横切关注点耦合
  • 修改日志逻辑需要改所有类

AOP 解决方案

java
@Service
@Loggable          // 自定义注解标记
@Transactional     // 声明式事务
public class UserService {
    
    @PreAuthorize("hasRole('USER')")
    public void save(User user) {
        // 只有核心业务逻辑
        userDao.insert(user);
    }
}
┌──────────────────────────────────────────────────────────────┐
│                      业务方法 save()                          │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│   │  日志通知  │  │ 权限通知  │  │ 事务通知  │  │ 性能监控  │    │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘    │
└────────┼─────────────┼─────────────┼─────────────┼──────────┘
         │             │             │             │
         ▼             ▼             ▼             ▼
       Before       Before         Before         Before
                   (JoinPoint)    (JoinPoint)    (JoinPoint)
                        │             │             │
                        └─────────────┴─────────────┘

                              Target Method

                        ┌───────────┴───────────┐
                       AfterReturning        AfterThrowing
                              │                    │
                         (Result)              (Exception)
                              │                    │
                        └───────────┬───────────┘

                                 After (finally)

二、AOP 核心概念

概念说明类比
Aspect(切面)通知 + 切点的结合完整的面
Join Point(连接点)可以织入通知的时机可织入的点
Pointcut(切点)匹配连接点的表达式筛选条件
Advice(通知)织入的具体逻辑要做什么
Weaving(织入)将通知应用到目标的过程织进去

一张图理解

                    ┌─────────────────┐
                    │     Aspect      │  ← 切面 = 通知 + 切点
                    └────────┬────────┘

          ┌──────────────────┼──────────────────┐
          │                  │                  │
    ┌─────▼─────┐      ┌─────▼─────┐      ┌─────▼─────┐
    │  Pointcut │      │  Advice   │      │   切面    │
    │ (where)   │      │ (what)    │      │ (通知+切点)│
    └───────────┘      └───────────┘      └───────────┘

    ┌─────────────────────────────────────────────────────┐
    │                  目标对象 Target                      │
    │   ┌─────────────────────────────────────────────┐   │
    │   │  public void save(User user) { ... }        │   │
    │   │                                             │   │
    │   │  ← Before → ← AfterReturning → ← After →   │   │
    │   └─────────────────────────────────────────────┘   │
    └─────────────────────────────────────────────────────┘

三、五种通知类型

1. @Before(前置通知)

java
@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        // 获取方法签名
        String methodName = joinPoint.getSignature().getName();
        // 获取参数
        Object[] args = joinPoint.getArgs();
        System.out.println("[Before] " + methodName + " 开始执行,参数: " + Arrays.toString(args));
    }
}

2. @AfterReturning(返回通知)

java
@Aspect
@Component
public class PerformanceAspect {
    
    @AfterReturning(value = "execution(* com.example.service.*.*(..))", 
                    returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[AfterReturning] " + methodName + " 返回: " + result);
    }
}

3. @AfterThrowing(异常通知)

java
@Aspect
@Component
public class ExceptionAspect {
    
    @AfterThrowing(value = "execution(* com.example.service.*.*(..))",
                   throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[AfterThrowing] " + methodName + " 异常: " + exception.getMessage());
    }
}

4. @After(后置通知)

java
@Aspect
@Component
public class FinallyAspect {
    
    @After("execution(* com.example.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[After] " + methodName + " 执行结束");
    }
}

5. @Around(环绕通知)- 最强大

java
@Aspect
@Component
public class AroundAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        long start = System.currentTimeMillis();
        
        try {
            // 执行目标方法
            Object result = joinPoint.proceed();
            long cost = System.currentTimeMillis() - start;
            System.out.println("[Around] " + methodName + " 执行耗时: " + cost + "ms");
            return result;
        } catch (Throwable e) {
            long cost = System.currentTimeMillis() - start;
            System.out.println("[Around] " + methodName + " 执行异常,耗时: " + cost + "ms");
            throw e;
        }
    }
}

执行顺序

try {
    // @Before
    result = joinPoint.proceed();  // 目标方法
    // @AfterReturning + @After
} catch (Exception e) {
    // @AfterThrowing + @After
} finally {
    // @After (总是执行)
}

四、切点表达式

语法

execution([修饰符] 返回类型 [类名.]方法名(参数) [异常])

常用示例

java
// 1. 匹配指定类的方法
@Before("execution(public User com.example.UserService.getById(Long))")

// 2. 匹配所有 public 方法
@Before("execution(public * *(..))")

// 3. 匹配 service 包下所有方法
@Before("execution(* com.example.service.*.*(..))")

// 4. 匹配 service 及其子包下所有方法
@Before("execution(* com.example.service..*.*(..))")

// 5. 匹配以 get 开头的方法
@Before("execution(* get*(..))")

// 6. 匹配 UserService 中所有方法
@Before("execution(* com.example.UserService.*(..))")

// 7. 匹配第一个参数为 User 的方法
@Before("execution(* *(com.example.User, ..))")

// 8. 匹配抛出特定异常的方法
@Before("execution(* *(..) throws java.io.IOException)")

切点组合

java
@Aspect
@Component
public class CombinedAspect {
    
    // && 组合
    @Before("execution(* com.example.service.*.*(..)) && args(..)")
    public void beforeService(JoinPoint jp) { }
    
    // || 组合
    @Before("execution(* com.example.service.*.*(..)) || execution(* com.example.dao.*.*(..))")
    public void beforeAll(JoinPoint jp) { }
    
    // ! 取反
    @Before("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.internal.*.*(..))")
    public void beforePublic(JoinPoint jp) { }
    
    // @annotation 匹配带特定注解的方法
    @Before("@annotation(com.example.MyLoggable)")
    public void beforeAnnotated(JoinPoint jp) { }
}

@Pointcut 复用切点

java
@Aspect
@Component
public class LogAspect {
    
    // 定义切点
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }
    
    @Pointcut("execution(* com.example.dao.*.*(..))")
    public void daoLayer() { }
    
    @Pointcut("serviceLayer() || daoLayer()")
    public void appLayer() { }
    
    // 复用切点
    @Before("serviceLayer()")
    public void beforeService(JoinPoint jp) {
        System.out.println("Before service");
    }
    
    @Before("daoLayer()")
    public void beforeDao(JoinPoint jp) {
        System.out.println("Before dao");
    }
}

五、基于注解的 AOP

完整示例

java
// 1. 启用 AOP
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AppConfig { }

// 2. 定义切面
@Aspect
@Component
public class TransactionAspect {
    
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() { }
    
    @Around("servicePointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        
        System.out.println("[" + methodName + "] 开启事务");
        try {
            Object result = pjp.proceed();
            System.out.println("[" + methodName + "] 提交事务");
            return result;
        } catch (Exception e) {
            System.out.println("[" + methodName + "] 回滚事务");
            throw e;
        }
    }
}

// 3. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
    int maxAttempts() default 3;
    long delay() default 1000;
}

// 4. 使用注解定义切面
@Aspect
@Component
public class RetryAspect {
    
    @Around("@annotation(retryable)")
    public Object around(ProceedingJoinPoint pjp, Retryable retryable) throws Throwable {
        int attempts = 0;
        while (attempts < retryable.maxAttempts()) {
            try {
                return pjp.proceed();
            } catch (Exception e) {
                attempts++;
                if (attempts >= retryable.maxAttempts()) throw e;
                Thread.sleep(retryable.delay());
            }
        }
        throw new RuntimeException("Max attempts reached");
    }
}

// 5. 使用
@Service
public class UserService {
    
    @Retryable(maxAttempts = 3, delay = 500)
    public void callRemote() {
        // 可能失败的远程调用
    }
}

注解版本顺序控制

java
@Aspect
@Component
@Order(1)  // 数字越小越先执行
public class TransactionAspect { }

@Aspect
@Component
@Order(2)  // 后执行
public class LoggingAspect { }

六、基于 XML 的 AOP(传统方式)

xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop">
    
    <!-- 1. 启用 AspectJ 自动代理 -->
    <aop:aspectj-autoproxy />
    
    <!-- 2. 定义切面 Bean -->
    <bean id="loggingAspect" class="com.example.LoggingAspect" />
    <bean id="transactionAspect" class="com.example.TransactionAspect" />
    
    <!-- 3. 配置切点 + 通知 -->
    <aop:config>
        <!-- 切面 -->
        <aop:aspect id="logAspect" ref="loggingAspect">
            <!-- 切点 -->
            <aop:pointcut id="servicePointcut" 
                expression="execution(* com.example.service.*.*(..))" />
            
            <!-- 前置通知 -->
            <aop:before method="before" pointcut-ref="servicePointcut" />
            
            <!-- 返回通知 -->
            <aop:after-returning method="afterReturning" 
                pointcut-ref="servicePointcut" returning="result" />
            
            <!-- 环绕通知 -->
            <aop:around method="around" pointcut-ref="servicePointcut" />
        </aop:aspect>
    </aop:config>
</beans>

七、JoinPoint 获取信息

java
@Aspect
@Component
public class InfoAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void info(JoinPoint joinPoint) {
        // 获取目标对象
        Object target = joinPoint.getTarget();
        System.out.println("Target: " + target.getClass().getName());
        
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        System.out.println("Method: " + signature.getName());
        System.out.println("ReturnType: " + signature.getReturnType());
        System.out.println("ParameterNames: " + Arrays.toString(signature.getParameterNames()));
        
        // 获取参数值
        Object[] args = joinPoint.getArgs();
        System.out.println("Args: " + Arrays.toString(args));
        
        // 获取 this(代理对象)
        Object proxy = joinPoint.getThis();
        System.out.println("Proxy: " + proxy.getClass().getName());
    }
}

八、Spring AOP vs AspectJ

对比Spring AOPAspectJ
织入时机运行时编译时/加载时
复杂度简单复杂
性能略低(有代理开销)更高(编译时织入)
功能方法级别字段/构造函数/静态块
配置简单需要 AspectJ 编译器
使用场景业务层(推荐)需要精细控制时
java
// Spring AOP 支持的 JoinPoint 位置
// 只有方法执行的连接点
public void method() { }           ✅ 可织入
private void method() { }          ❌ 不可织入(无法外部访问)
static void method() { }           ❌ 不可织入

九、实际应用场景

1. 日志记录

java
@Aspect
@Component
public class LoggingAspect {
    
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    @Around("execution(* com.example.controller.*.*(..))")
    public Object controllerLog(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest request = getRequest();
        log.info("{} {} - {}", request.getMethod(), request.getRequestURI(), getClientIP());
        return pjp.proceed();
    }
}

2. 性能监控

java
@Aspect
@Component
public class PerformanceAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            long cost = (System.nanoTime() - start) / 1_000_000;
            if (cost > 1000) {  // 超过1秒告警
                log.warn("Slow method: {}.{} took {}ms", 
                    pjp.getSignature().getDeclaringTypeName(),
                    pjp.getSignature().getName(), cost);
            }
        }
    }
}

3. 权限校验

java
@Aspect
@Component
public class SecurityAspect {
    
    @Before("execution(* com.example.service.admin.*.*(..))")
    public void checkAdmin(JoinPoint jp) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!auth.getAuthorities().contains("ROLE_ADMIN")) {
            throw new SecurityException("Admin access required");
        }
    }
}

4. 缓存

java
@Aspect
@Component
public class CacheAspect {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    @Around("@annotation(Cacheable)")
    public Object cache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
        String key = cacheable.value() + ":" + Arrays.toString(pjp.getArgs());
        
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        
        Object result = pjp.proceed();
        cache.put(key, result);
        return result;
    }
}

十、面试高频问题

Q1: AOP 核心概念有哪些?

  • Aspect(切面):通知 + 切点
  • Join Point(连接点):可织入点
  • Pointcut(切点):匹配表达式
  • Advice(通知):要织入的逻辑
  • Weaving(织入):应用通知的过程

Q2: Spring AOP 实现原理?

  1. Spring 在启动时创建代理对象
  2. 默认使用 JDK 动态代理(接口实现类)
  3. 如果目标对象没有接口,使用 CGLIB 字节码增强
  4. 方法调用通过代理对象,通知在调用前后执行
java
// 代理对象调用流程
proxy.save(user);

// 相当于执行:
try {
    beforeAdvice();      // 前置通知
    target.save(user);   // 目标方法
    afterReturning();    // 返回通知
} catch (Exception e) {
    afterThrowing();     // 异常通知
} finally {
    after();             // 后置通知
}

Q3: JDK 动态代理 vs CGLIB?

对比JDK 动态代理CGLIB
原理反射字节码生成
要求目标类实现接口不需要接口
性能略慢更快
生成物$Proxy0 继承自 Proxy子类继承目标类
java
// 强制使用 CGLIB
@EnableAspectJAutoProxy(proxyTargetClass = true)

Q4: @Before 和 @Around 哪个先执行?

@Around 开始

@Before

目标方法

@AfterReturning / @AfterThrowing

@After

@Around 结束

Q5: 什么是织入(Weaving)?

将切面的通知应用到目标对象的过程。时机:

  • 编译时织入:AspectJ 编译器
  • 类加载时织入:AspectJ LTW
  • 运行时织入:Spring AOP(创建代理)

十一、下一章预告

下一章我们将学习 Bean 的作用域与生命周期

  • Bean 的 6 种作用域详解
  • Bean 的完整生命周期(创建 → 初始化 → 销毁)
  • Spring 后置处理器(BeanPostProcessor)
  • Spring 初始化和销毁回调

基于 MIT 许可发布