🎯AOP连接点(JoinPoint)详解
连接点是AOP中程序执行过程中的一个特定点,它是切面可以插入增强逻辑的位置。
📍连接点是什么?
生动的比喻
程序执行就像一部电影 🎬 连接点 → 电影的关键帧(可以插入特效的地方) 切面 → 特效团队 增强 → 实际添加的特效(慢动作、滤镜等)更技术化的理解
连接点是程序运行中可以被拦截的点,例如:
- 方法调用
- 方法执行
- 异常抛出
- 字段访问
- 对象初始化
在Spring AOP中,连接点特指:方法的执行。
🎪连接点的具体形式
1. 方法执行(Method Execution)✅Spring AOP唯一支持的
publicclassUserService{publicvoidsaveUser(Useruser){// ← 这是一个连接点// 方法体}}2. 方法调用(Method Call)❌Spring AOP不支持
publicclassController{publicvoidprocess(){userService.saveUser(user);// ← 这是一个调用连接点}}3. 其他连接点(AspectJ支持,但Spring AOP不支持)
// 构造器执行newUserService();// ← 连接点// 字段访问user.name;// ← 连接点// 异常处理thrownewException();// ← 连接点🔧JoinPoint对象详解
在通知方法中,可以通过JoinPoint参数获取连接点信息:
JoinPoint核心方法
publicinterfaceJoinPoint{// 获取方法签名SignaturegetSignature();// 获取目标对象ObjectgetTarget();// 获取代理对象ObjectgetThis();// 获取方法参数Object[]getArgs();// 获取连接点类型(Spring AOP中总是:method-execution)StringgetKind();// 获取静态部分信息JoinPoint.StaticPartgetStaticPart();// 获取源位置信息(文件名、行号等)SourceLocationgetSourceLocation();}Signature对象
publicinterfaceSignature{StringgetName();// 方法名:saveUserintgetModifiers();// 修饰符:publicClassgetDeclaringType();// 声明类:UserServiceStringgetDeclaringTypeName();// 类名:com.example.UserService// 更多信息StringtoShortString();// 简短字符串StringtoLongString();// 完整字符串}💻实际使用示例
示例1:获取方法信息
@Before("execution(* com.example.service.*.*(..))")publicvoidbeforeAdvice(JoinPointjoinPoint){// 1. 获取方法签名MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();System.out.println("=== 连接点信息 ===");System.out.println("方法名: "+signature.getName());// saveUserSystem.out.println("方法全名: "+signature.toLongString());// public void com.example.UserService.saveUser(User)System.out.println("声明类: "+signature.getDeclaringTypeName());// com.example.UserServiceSystem.out.println("返回类型: "+signature.getReturnType());// void// 2. 获取参数Object[]args=joinPoint.getArgs();for(inti=0;i<args.length;i++){System.out.println("参数"+i+": "+args[i]+" (类型: "+signature.getParameterTypes()[i]+")");}// 3. 目标对象和代理对象System.out.println("目标对象: "+joinPoint.getTarget().getClass());// UserService$$EnhancerBySpringCGLIBSystem.out.println("代理对象: "+joinPoint.getThis().getClass());// UserService$$EnhancerBySpringCGLIB}示例2:环绕通知中使用ProceedingJoinPoint
@Around("execution(* com.example.service.*.*(..))")publicObjectaroundAdvice(ProceedingJoinPointjoinPoint)throwsThrowable{// ProceedingJoinPoint是JoinPoint的子接口,多了proceed()方法System.out.println("【环绕通知开始】方法: "+joinPoint.getSignature().getName());// 1. 可以修改参数Object[]args=joinPoint.getArgs();if(args.length>0&&args[0]instanceofString){args[0]=((String)args[0]).toUpperCase();// 修改参数}// 2. 执行目标方法longstart=System.currentTimeMillis();Objectresult=joinPoint.proceed(args);// 可以传入修改后的参数longend=System.currentTimeMillis();System.out.println("【环绕通知结束】耗时: "+(end-start)+"ms");// 3. 可以修改返回值if(resultinstanceofString){result="处理后的结果: "+result;}returnresult;}📊连接点 vs 切入点 vs 通知
| 概念 | 定义 | 比喻 |
|---|---|---|
| 连接点 | 程序执行中的具体点(如方法执行) | 电影中的具体帧 |
| 切入点 | 匹配连接点的表达式(筛选哪些连接点) | 选中的帧的范围(如所有打斗场景) |
| 通知 | 在连接点执行的增强逻辑 | 在选中的帧上添加的特效 |
三者关系
@Aspect@ComponentpublicclassLogAspect{// 切入点:匹配哪些连接点@Pointcut("execution(* com.example.service.*.*(..))")publicvoidserviceMethods(){}// ← 切入点表达式// 通知:在连接点上执行的逻辑@Before("serviceMethods()")// ← 应用到切入点匹配的连接点publicvoidlogBefore(JoinPointjoinPoint){// ← JoinPoint代表具体的连接点// 这里是通知逻辑System.out.println("执行方法: "+joinPoint.getSignature().getName());}}🎯获取连接点信息的实用工具类
@ComponentpublicclassJoinPointUtils{/** * 获取方法参数Map */publicstaticMap<String,Object>getArgsMap(JoinPointjoinPoint){Map<String,Object>argsMap=newHashMap<>();if(joinPoint.getSignature()instanceofMethodSignature){MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();String[]parameterNames=signature.getParameterNames();Object[]args=joinPoint.getArgs();for(inti=0;i<parameterNames.length;i++){argsMap.put(parameterNames[i],args[i]);}}returnargsMap;}/** * 获取方法注解 */publicstatic<TextendsAnnotation>TgetMethodAnnotation(JoinPointjoinPoint,Class<T>annotationClass){MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();Methodmethod=signature.getMethod();returnmethod.getAnnotation(annotationClass);}/** * 获取类注解 */publicstatic<TextendsAnnotation>TgetClassAnnotation(JoinPointjoinPoint,Class<T>annotationClass){Class<?>targetClass=joinPoint.getTarget().getClass();returntargetClass.getAnnotation(annotationClass);}/** * 获取完整方法路径 */publicstaticStringgetFullMethodName(JoinPointjoinPoint){StringclassName=joinPoint.getSignature().getDeclaringTypeName();StringmethodName=joinPoint.getSignature().getName();returnclassName+"."+methodName;}/** * 获取IP地址(Web环境下) */publicstaticStringgetIpAddress(JoinPointjoinPoint){try{ServletRequestAttributesattributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes!=null){HttpServletRequestrequest=attributes.getRequest();returnrequest.getRemoteAddr();}}catch(Exceptione){// 忽略}return"unknown";}}使用示例
@Before("execution(* com.example.service.*.*(..))")publicvoidbeforeAdvice(JoinPointjoinPoint){// 使用工具类Map<String,Object>args=JoinPointUtils.getArgsMap(joinPoint);StringfullMethodName=JoinPointUtils.getFullMethodName(joinPoint);System.out.println("方法: "+fullMethodName);System.out.println("参数: "+args);// 获取特定注解LoglogAnnotation=JoinPointUtils.getMethodAnnotation(joinPoint,Log.class);if(logAnnotation!=null){System.out.println("日志级别: "+logAnnotation.level());}}🚀实战场景
场景1:操作日志记录
@Aspect@ComponentpublicclassOperationLogAspect{@AfterReturning(pointcut="@annotation(log)",returning="result")publicvoidlogOperation(JoinPointjoinPoint,@annotation(log)OperationLoglog,Objectresult){// 从连接点获取信息StringmethodName=joinPoint.getSignature().getName();StringclassName=joinPoint.getSignature().getDeclaringTypeName();Object[]args=joinPoint.getArgs();// 构建日志LogRecordrecord=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:参数验证
@Aspect@ComponentpublicclassValidationAspect{@Before("execution(* com.example.controller.*.*(..))")publicvoidvalidateParams(JoinPointjoinPoint){// 获取所有参数Object[]args=joinPoint.getArgs();for(Objectarg:args){if(arginstanceofBaseDTO){// 执行DTO验证ValidationResultresult=validator.validate(arg);if(!result.isValid()){thrownewValidationException(result.getErrors());}}}}}场景3:缓存切面
@Aspect@ComponentpublicclassCacheAspect{@Around("@annotation(cacheable)")publicObjectcacheResult(ProceedingJoinPointjoinPoint,Cacheablecacheable)throwsThrowable{// 生成缓存key:类名+方法名+参数Stringkey=generateCacheKey(joinPoint);// 先从缓存获取Objectcached=cache.get(key);if(cached!=null){returncached;}// 执行方法Objectresult=joinPoint.proceed();// 存入缓存cache.put(key,result,cacheable.ttl(),TimeUnit.SECONDS);returnresult;}privateStringgenerateCacheKey(ProceedingJoinPointjoinPoint){StringBuilderkey=newStringBuilder();// 类名key.append(joinPoint.getSignature().getDeclaringTypeName()).append(".");// 方法名key.append(joinPoint.getSignature().getName()).append(":");// 参数(简单处理)for(Objectarg:joinPoint.getArgs()){key.append(arg!=null?arg.toString():"null").append(",");}returnkey.toString();}}⚠️重要限制
Spring AOP只支持方法执行连接点
// ✅ 支持的@Before("execution(* *.*(..))")// ❌ 不支持的(需要AspectJ)@Before("call(* *.*(..))")// 方法调用@Before("initialization(*.new(..))")// 构造器@Before("get(* *)")// 字段读取@Before("set(* *)")// 字段设置代理机制的影响
- JDK动态代理:只能代理接口方法,
this和target可能不同 - CGLIB代理:可以代理类,
this和target通常相同 - 内部方法调用不会被拦截(因为不走代理)
🧪调试技巧
打印连接点信息
@Before("execution(* *.*(..))")publicvoiddebugJoinPoint(JoinPointjoinPoint){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("==========================");}判断连接点类型
@Before("execution(* *.*(..))")publicvoidcheckJoinPoint(JoinPointjoinPoint){if(joinPointinstanceofProceedingJoinPoint){System.out.println("这是ProceedingJoinPoint(环绕通知可用)");}else{System.out.println("这是普通JoinPoint");}}💎总结
连接点的核心要点
- 定义:程序执行中可以插入增强的点
- Spring AOP:只支持方法执行这一种连接点
- JoinPoint对象:包含方法签名、参数、目标对象等信息
- ProceedingJoinPoint:用于环绕通知,可以控制方法执行
一句话记忆
连接点就像程序的"穴位",切入点就是"针灸的位置图",通知就是"扎针的治疗手法"。
实用口诀
连接点,执行点,方法执行最常见 JoinPoint,信息全,签名参数都在里边 ProceedingJoinPoint更强,可以控制方法执行权 记住Spring有局限,只支持方法执行这一个连接点