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有局限,只支持方法执行这一个连接点
相关推荐
武子康6 小时前
大数据-183 Elasticsearch - 并发冲突与乐观锁、分布式数据一致性剖析
大数据·后端·elasticsearch
while(1){yan}6 小时前
基于IO流的三个小程序
java·开发语言·青少年编程
CoderYanger6 小时前
贪心算法:7.最长连续递增序列
java·算法·leetcode·贪心算法·1024程序员节
期待のcode6 小时前
MyBatis-Plus的Wrapper核心体系
java·数据库·spring boot·后端·mybatis
曼巴UE56 小时前
UE C++ 字符串的操作
java·开发语言·c++
醇氧6 小时前
springAI学习 一
学习·spring·ai·ai编程
凛_Lin~~7 小时前
安卓/Java语言基础八股文
java·开发语言·安卓
老华带你飞7 小时前
出行旅游安排|基于springboot出行旅游安排系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring·旅游
舒一笑7 小时前
在低配云服务器上实现自动化部署:Drone CI + Gitee Webhook 的轻量级实践
前端·后端·程序员