joken-前端工程师

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: :: :: 管理 ::

Spring AOP 切面使用指南:从入门到实战

📖 前言

Spring AOP(面向切面编程)是Spring框架的核心特性之一,它允许我们在不修改业务代码的情况下,为应用程序添加横切关注点(如日志、缓存、事务管理等)。本文将通过实际项目中的缓存实现案例,详细讲解Spring AOP的使用方式。

🎯 什么是AOP?

AOP(Aspect-Oriented Programming)面向切面编程,是对面向对象编程(OOP)的补充。它的核心思想是:

  • 将横切关注点(Cross-cutting Concerns)从业务逻辑中分离出来
  • 通过"切面"的方式,在运行时将这些关注点"织入"到目标对象中

核心概念

概念 说明 举例
切面 (Aspect) 横切关注点的模块化 缓存切面、日志切面
连接点 (Join Point) 程序执行的特定点 方法调用、异常抛出
切点 (Pointcut) 连接点的集合 所有Service层的方法
通知 (Advice) 切面在特定连接点执行的动作 方法执行前、后、异常时
织入 (Weaving) 将切面应用到目标对象的过程 运行时动态代理

🛠️ 实战案例:自定义缓存切面

让我们通过一个实际的缓存实现来学习AOP的使用方式。

第一步:创建自定义注解

package com.peng.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)           // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME)   // 运行时保留注解信息
public @interface MyCache {
    int overTime() default 3600;      // 缓存过期时间,默认1小时
}

注解说明:

  • @Target(ElementType.METHOD): 限定注解只能用在方法上
  • @Retention(RetentionPolicy.RUNTIME): 确保运行时可以通过反射获取注解信息
  • overTime(): 自定义属性,支持设置缓存过期时间

第二步:创建切面类

package com.peng.aspect;

import com.peng.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect        // 声明这是一个切面类
@Component     // 注册为Spring Bean
public class MyCacheAspect {
    
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 生成缓存Key
     * 规则:类名.方法名-参数1-参数2...
     */
    private String createCacheKey(ProceedingJoinPoint jp) {
        Signature signature = jp.getSignature();
        String methodName = signature.getName();
        String className = signature.getDeclaringTypeName();
        
        StringBuffer sbKey = new StringBuffer();
        sbKey.append(className).append(".").append(methodName);
        
        Object[] args = jp.getArgs(); // 获取方法参数
        for (Object arg : args) {
            sbKey.append("-").append(arg);
        }
        return sbKey.toString();
    }

    /**
     * 环绕通知:缓存切面逻辑
     * 切点表达式:匹配service.Impl包下所有带@MyCache注解的公共方法
     */
    @Order(1)  // 切面执行顺序
    @Around("execution(public * com.peng.service.Impl..*(..)) && @annotation(myCache)")
    public Object around(ProceedingJoinPoint jp, MyCache myCache) {
        String key = createCacheKey(jp);
        
        try {
            // 1. 检查缓存
            if (redisUtil.hasKey(key)) {
                log.info("缓存命中: {}", key);
                return redisUtil.get(key);
            } 
            
            // 2. 缓存未命中,执行原方法
            log.info("缓存未命中,执行原方法: {}", key);
            Object result = jp.proceed(jp.getArgs());
            
            // 3. 将结果存入缓存
            redisUtil.set(key, result, myCache.overTime());
            return result;
            
        } catch (Throwable t) {
            log.error("缓存切面执行异常: {}", t.getMessage());
            return null;
        }
    }
}

第三步:在业务方法上使用注解

@Service
public class CacheServiceImpl implements ICacheService {
    
    @Autowired
    private IBlogService iBlogService;
    
    @Override
    @MyCache                    // 使用默认过期时间3600秒
    public PageInfo<Blog> getIndexPage(String title, Integer pageNum) {
        return iBlogService.getIndexPage(title, pageNum);
    }

    @Override
    @MyCache(overTime = 7200)   // 自定义过期时间7200秒
    public List<Type> getIndexTypes() {
        return iTypeService.getIndexTypes();
    }
}

🔍 切点表达式详解

切点表达式是AOP的核心,它决定了切面在哪些地方生效。

常用切点表达式

// 1. 执行表达式 - 最常用
@Around("execution(public * com.peng.service..*(..))")
// 匹配:com.peng.service包及子包下所有公共方法

// 2. 注解表达式
@Around("@annotation(com.peng.aspect.MyCache)")
// 匹配:标注了@MyCache注解的方法

// 3. 组合表达式
@Around("execution(public * com.peng.service.Impl..*(..)) && @annotation(myCache)")
// 匹配:service.Impl包下所有公共方法 且 标注了@MyCache注解

// 4. 类型表达式
@Around("within(com.peng.service.impl.*)")
// 匹配:指定包下所有类的所有方法

// 5. 参数表达式
@Around("execution(* com.peng.service..*(String, ..)) && @annotation(myCache)")
// 匹配:第一个参数为String类型的方法

表达式语法规则

execution(修饰符 返回类型 包名.类名.方法名(参数类型))
通配符 含义 示例
* 匹配任意字符 *Service 匹配所有以Service结尾的类
.. 匹配任意包层级或参数 com.peng..* 匹配com.peng下所有子包
+ 匹配子类型 BaseService+ 匹配BaseService及其子类

📋 通知类型详解

Spring AOP提供5种通知类型:

1. 前置通知 (@Before)

@Before("execution(* com.peng.service..*(..))")
public void before(JoinPoint jp) {
    log.info("方法执行前: {}", jp.getSignature().getName());
}

2. 后置通知 (@After)

@After("execution(* com.peng.service..*(..))")
public void after(JoinPoint jp) {
    log.info("方法执行后: {}", jp.getSignature().getName());
}

3. 返回通知 (@AfterReturning)

@AfterReturning(pointcut = "execution(* com.peng.service..*(..))", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
    log.info("方法正常返回: {}, 返回值: {}", jp.getSignature().getName(), result);
}

4. 异常通知 (@AfterThrowing)

@AfterThrowing(pointcut = "execution(* com.peng.service..*(..))", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
    log.error("方法执行异常: {}, 异常: {}", jp.getSignature().getName(), ex.getMessage());
}

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

@Around("execution(* com.peng.service..*(..))")
public Object around(ProceedingJoinPoint jp) throws Throwable {
    long startTime = System.currentTimeMillis();
    
    try {
        // 前置逻辑
        log.info("方法开始执行: {}", jp.getSignature().getName());
        
        // 执行原方法
        Object result = jp.proceed();
        
        // 后置逻辑
        log.info("方法执行成功,耗时: {}ms", System.currentTimeMillis() - startTime);
        return result;
        
    } catch (Exception e) {
        // 异常逻辑
        log.error("方法执行异常: {}", e.getMessage());
        throw e;
    }
}

🚀 高级特性

1. 切面执行顺序

@Aspect
@Order(1)  // 数字越小,优先级越高
@Component
public class CacheAspect {
    // 缓存切面逻辑
}

@Aspect
@Order(2)
@Component
public class LogAspect {
    // 日志切面逻辑
}

2. 获取方法参数和注解信息

@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
    // 获取方法信息
    String methodName = jp.getSignature().getName();
    String className = jp.getTarget().getClass().getName();
    Object[] args = jp.getArgs();
    
    // 获取注解属性
    int overTime = myCache.overTime();
    
    // 执行切面逻辑
    return result;
}

3. 条件切面

@Around("@annotation(myCache) && @annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
    // 只对同时标注了@MyCache和@Transactional的方法生效
}

💡 最佳实践

1. 切面设计原则

  • 单一职责: 每个切面只处理一种横切关注点
  • 低耦合: 切面之间应该相互独立
  • 高内聚: 相关的切面逻辑应该放在同一个类中

2. 性能优化建议

@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
    // ❌ 避免在切面中执行耗时操作
    // Thread.sleep(1000);
    
    // ✅ 使用异步处理耗时操作
    CompletableFuture.runAsync(() -> {
        // 异步日志记录
    });
    
    return jp.proceed();
}

3. 异常处理

@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
    try {
        // 切面逻辑
    } catch (Exception e) {
        log.error("切面执行失败,降级执行原方法", e);
        // 发生异常时,确保原方法能正常执行
        return jp.proceed();
    }
}

🎯 实际运行效果

当调用标注了@MyCache的方法时:

第一次调用:
2024-01-01 10:00:00 INFO  - 缓存未命中,执行原方法: com.peng.service.Impl.CacheServiceImpl.getIndexTypes
2024-01-01 10:00:01 INFO  - 缓存已存储,Key: com.peng.service.Impl.CacheServiceImpl.getIndexTypes

第二次调用:
2024-01-01 10:00:02 INFO  - 缓存命中: com.peng.service.Impl.CacheServiceImpl.getIndexTypes

📚 总结

Spring AOP切面编程通过以下步骤实现:

  1. 定义切面: 使用@Aspect@Component注解
  2. 编写切点: 使用切点表达式指定拦截规则
  3. 实现通知: 选择合适的通知类型编写切面逻辑
  4. 应用切面: 在目标方法上使用注解或匹配切点表达式

核心优势:

  • 无侵入性: 不需要修改业务代码
  • 可重用性: 切面逻辑可以应用到多个方法
  • 易维护性: 横切关注点集中管理
  • 灵活配置: 支持复杂的切点表达式

通过合理使用Spring AOP,我们可以轻松实现缓存、日志、权限控制、事务管理等功能,让代码更加清晰和易于维护。


参考资源:

posted on 2025-09-30 17:30  joken1310  阅读(13)  评论(0)    收藏  举报