Spring AOP连接点实战解析

🎯 AOP连接点(JoinPoint)详解

连接点是AOP中程序执行过程中的一个特定点,它是切面可以插入增强逻辑的位置。


📍 连接点是什么?

生动的比喻

复制代码
程序执行就像一部电影 🎬
连接点 → 电影的关键帧(可以插入特效的地方)
切面 → 特效团队
增强 → 实际添加的特效(慢动作、滤镜等)

更技术化的理解

连接点是程序运行中可以被拦截的点,例如:

  • 方法调用
  • 方法执行
  • 异常抛出
  • 字段访问
  • 对象初始化

在Spring AOP中,连接点特指:方法的执行


🎪 连接点的具体形式

1. 方法执行(Method Execution)Spring AOP唯一支持的

java 复制代码
public class UserService {
    public void saveUser(User user) {  // ← 这是一个连接点
        // 方法体
    }
}

2. 方法调用(Method Call)Spring AOP不支持

java 复制代码
public class Controller {
    public void process() {
        userService.saveUser(user);  // ← 这是一个调用连接点
    }
}

3. 其他连接点(AspectJ支持,但Spring AOP不支持)

java 复制代码
// 构造器执行
new UserService();  // ← 连接点

// 字段访问
user.name;          // ← 连接点

// 异常处理
throw new Exception();  // ← 连接点

🔧 JoinPoint对象详解

在通知方法中,可以通过JoinPoint参数获取连接点信息:

JoinPoint核心方法

java 复制代码
public interface JoinPoint {
    // 获取方法签名
    Signature getSignature();
    
    // 获取目标对象
    Object getTarget();
    
    // 获取代理对象
    Object getThis();
    
    // 获取方法参数
    Object[] getArgs();
    
    // 获取连接点类型(Spring AOP中总是:method-execution)
    String getKind();
    
    // 获取静态部分信息
    JoinPoint.StaticPart getStaticPart();
    
    // 获取源位置信息(文件名、行号等)
    SourceLocation getSourceLocation();
}

Signature对象

java 复制代码
public interface Signature {
    String getName();           // 方法名:saveUser
    int getModifiers();         // 修饰符:public
    Class getDeclaringType();   // 声明类:UserService
    String getDeclaringTypeName();  // 类名:com.example.UserService
    
    // 更多信息
    String toShortString();     // 简短字符串
    String toLongString();     // 完整字符串
}

💻 实际使用示例

示例1:获取方法信息

java 复制代码
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    // 1. 获取方法签名
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("=== 连接点信息 ===");
    System.out.println("方法名: " + signature.getName());                    // saveUser
    System.out.println("方法全名: " + signature.toLongString());            // public void com.example.UserService.saveUser(User)
    System.out.println("声明类: " + signature.getDeclaringTypeName());      // com.example.UserService
    System.out.println("返回类型: " + signature.getReturnType());            // void
    
    // 2. 获取参数
    Object[] args = joinPoint.getArgs();
    for (int i = 0; i < args.length; i++) {
        System.out.println("参数" + i + ": " + args[i] + " (类型: " + 
                          signature.getParameterTypes()[i] + ")");
    }
    
    // 3. 目标对象和代理对象
    System.out.println("目标对象: " + joinPoint.getTarget().getClass());     // UserService$$EnhancerBySpringCGLIB
    System.out.println("代理对象: " + joinPoint.getThis().getClass());       // UserService$$EnhancerBySpringCGLIB
}

示例2:环绕通知中使用ProceedingJoinPoint

java 复制代码
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // ProceedingJoinPoint是JoinPoint的子接口,多了proceed()方法
    
    System.out.println("【环绕通知开始】方法: " + joinPoint.getSignature().getName());
    
    // 1. 可以修改参数
    Object[] args = joinPoint.getArgs();
    if (args.length > 0 && args[0] instanceof String) {
        args[0] = ((String) args[0]).toUpperCase();  // 修改参数
    }
    
    // 2. 执行目标方法
    long start = System.currentTimeMillis();
    Object result = joinPoint.proceed(args);  // 可以传入修改后的参数
    long end = System.currentTimeMillis();
    
    System.out.println("【环绕通知结束】耗时: " + (end - start) + "ms");
    
    // 3. 可以修改返回值
    if (result instanceof String) {
        result = "处理后的结果: " + result;
    }
    
    return result;
}

📊 连接点 vs 切入点 vs 通知

概念 定义 比喻
连接点 程序执行中的具体点(如方法执行) 电影中的具体帧
切入点 匹配连接点的表达式(筛选哪些连接点) 选中的帧的范围(如所有打斗场景)
通知 在连接点执行的增强逻辑 在选中的帧上添加的特效

三者关系

java 复制代码
@Aspect
@Component
public class LogAspect {
    
    // 切入点:匹配哪些连接点
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}  // ← 切入点表达式
    
    // 通知:在连接点上执行的逻辑
    @Before("serviceMethods()")  // ← 应用到切入点匹配的连接点
    public void logBefore(JoinPoint joinPoint) {  // ← JoinPoint代表具体的连接点
        // 这里是通知逻辑
        System.out.println("执行方法: " + joinPoint.getSignature().getName());
    }
}

🎯 获取连接点信息的实用工具类

java 复制代码
@Component
public class JoinPointUtils {
    
    /**
     * 获取方法参数Map
     */
    public static Map<String, Object> getArgsMap(JoinPoint joinPoint) {
        Map<String, Object> argsMap = new HashMap<>();
        
        if (joinPoint.getSignature() instanceof MethodSignature) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            String[] parameterNames = signature.getParameterNames();
            Object[] args = joinPoint.getArgs();
            
            for (int i = 0; i < parameterNames.length; i++) {
                argsMap.put(parameterNames[i], args[i]);
            }
        }
        
        return argsMap;
    }
    
    /**
     * 获取方法注解
     */
    public static <T extends Annotation> T getMethodAnnotation(
            JoinPoint joinPoint, Class<T> annotationClass) {
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        
        return method.getAnnotation(annotationClass);
    }
    
    /**
     * 获取类注解
     */
    public static <T extends Annotation> T getClassAnnotation(
            JoinPoint joinPoint, Class<T> annotationClass) {
        
        Class<?> targetClass = joinPoint.getTarget().getClass();
        return targetClass.getAnnotation(annotationClass);
    }
    
    /**
     * 获取完整方法路径
     */
    public static String getFullMethodName(JoinPoint joinPoint) {
        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        return className + "." + methodName;
    }
    
    /**
     * 获取IP地址(Web环境下)
     */
    public static String getIpAddress(JoinPoint joinPoint) {
        try {
            ServletRequestAttributes attributes = 
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                return request.getRemoteAddr();
            }
        } catch (Exception e) {
            // 忽略
        }
        return "unknown";
    }
}

使用示例

java 复制代码
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    // 使用工具类
    Map<String, Object> args = JoinPointUtils.getArgsMap(joinPoint);
    String fullMethodName = JoinPointUtils.getFullMethodName(joinPoint);
    
    System.out.println("方法: " + fullMethodName);
    System.out.println("参数: " + args);
    
    // 获取特定注解
    Log logAnnotation = JoinPointUtils.getMethodAnnotation(joinPoint, Log.class);
    if (logAnnotation != null) {
        System.out.println("日志级别: " + logAnnotation.level());
    }
}

🚀 实战场景

场景1:操作日志记录

java 复制代码
@Aspect
@Component
public class OperationLogAspect {
    
    @AfterReturning(pointcut = "@annotation(log)", returning = "result")
    public void logOperation(JoinPoint joinPoint, 
                            @annotation(log) OperationLog log,
                            Object result) {
        
        // 从连接点获取信息
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getSignature().getDeclaringTypeName();
        Object[] args = joinPoint.getArgs();
        
        // 构建日志
        LogRecord record = LogRecord.builder()
            .module(log.module())
            .operation(log.operation())
            .method(className + "." + methodName)
            .params(JSON.toJSONString(args))
            .result(JSON.toJSONString(result))
            .ip(JoinPointUtils.getIpAddress(joinPoint))
            .build();
        
        // 保存日志
        logService.save(record);
    }
}

场景2:参数验证

java 复制代码
@Aspect
@Component
public class ValidationAspect {
    
    @Before("execution(* com.example.controller.*.*(..))")
    public void validateParams(JoinPoint joinPoint) {
        // 获取所有参数
        Object[] args = joinPoint.getArgs();
        
        for (Object arg : args) {
            if (arg instanceof BaseDTO) {
                // 执行DTO验证
                ValidationResult result = validator.validate(arg);
                if (!result.isValid()) {
                    throw new ValidationException(result.getErrors());
                }
            }
        }
    }
}

场景3:缓存切面

java 复制代码
@Aspect
@Component
public class CacheAspect {
    
    @Around("@annotation(cacheable)")
    public Object cacheResult(ProceedingJoinPoint joinPoint, 
                              Cacheable cacheable) throws Throwable {
        
        // 生成缓存key:类名+方法名+参数
        String key = generateCacheKey(joinPoint);
        
        // 先从缓存获取
        Object cached = cache.get(key);
        if (cached != null) {
            return cached;
        }
        
        // 执行方法
        Object result = joinPoint.proceed();
        
        // 存入缓存
        cache.put(key, result, cacheable.ttl(), TimeUnit.SECONDS);
        
        return result;
    }
    
    private String generateCacheKey(ProceedingJoinPoint joinPoint) {
        StringBuilder key = new StringBuilder();
        
        // 类名
        key.append(joinPoint.getSignature().getDeclaringTypeName())
           .append(".");
        
        // 方法名
        key.append(joinPoint.getSignature().getName())
           .append(":");
        
        // 参数(简单处理)
        for (Object arg : joinPoint.getArgs()) {
            key.append(arg != null ? arg.toString() : "null")
               .append(",");
        }
        
        return key.toString();
    }
}

⚠️ 重要限制

Spring AOP只支持方法执行连接点

java 复制代码
// ✅ 支持的
@Before("execution(* *.*(..))")

// ❌ 不支持的(需要AspectJ)
@Before("call(* *.*(..))")       // 方法调用
@Before("initialization(*.new(..))")  // 构造器
@Before("get(* *)")              // 字段读取
@Before("set(* *)")              // 字段设置

代理机制的影响

  • JDK动态代理 :只能代理接口方法,thistarget可能不同
  • CGLIB代理 :可以代理类,thistarget通常相同
  • 内部方法调用不会被拦截(因为不走代理)

🧪 调试技巧

打印连接点信息

java 复制代码
@Before("execution(* *.*(..))")
public void debugJoinPoint(JoinPoint joinPoint) {
    System.out.println("====== JoinPoint信息 ======");
    System.out.println("Kind: " + joinPoint.getKind());
    System.out.println("Signature: " + joinPoint.getSignature());
    System.out.println("SourceLocation: " + joinPoint.getSourceLocation());
    System.out.println("StaticPart: " + joinPoint.getStaticPart());
    System.out.println("==========================");
}

判断连接点类型

java 复制代码
@Before("execution(* *.*(..))")
public void checkJoinPoint(JoinPoint joinPoint) {
    if (joinPoint instanceof ProceedingJoinPoint) {
        System.out.println("这是ProceedingJoinPoint(环绕通知可用)");
    } else {
        System.out.println("这是普通JoinPoint");
    }
}

💎 总结

连接点的核心要点

  1. 定义:程序执行中可以插入增强的点
  2. Spring AOP :只支持方法执行这一种连接点
  3. JoinPoint对象:包含方法签名、参数、目标对象等信息
  4. ProceedingJoinPoint:用于环绕通知,可以控制方法执行

一句话记忆

连接点就像程序的"穴位",切入点就是"针灸的位置图",通知就是"扎针的治疗手法"。

实用口诀

复制代码
连接点,执行点,方法执行最常见
JoinPoint,信息全,签名参数都在里边
ProceedingJoinPoint更强,可以控制方法执行权
记住Spring有局限,只支持方法执行这一个连接点
相关推荐
蚂蚁背大象34 分钟前
Rust 所有权系统是为了解决什么问题
后端·rust
子玖2 小时前
go实现通过ip解析城市
后端·go
Java不加班2 小时前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬3 小时前
RAG 进阶检索学习笔记
后端
Moment3 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_3 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术3 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark
古时的风筝3 小时前
花10 分钟时间,把终端改造成“生产力武器”:Ghostty + Yazi + Lazygit 配置全流程
前端·后端·程序员
Cache技术分享3 小时前
340. Java Stream API - 理解并行流的额外开销
前端·后端