AOP专题
含义和作用含义指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式;作用提供组件和切面的复用程度,提高程序灵活性有利于切面和组件解耦 。Java AOP 应用场景AOP面向切面编程核心作用在不修改业务代码的前提下统一处理横切逻辑日志、事务、权限等解耦、复用、易维护。一、AOP 核心思想面试必背横切关注点日志、事务、权限、缓存、监控等通用功能切面Aspect封装通用逻辑的类通知Advice什么时候执行前置/后置/环绕/异常/最终切点Pointcut在哪里执行匹配哪些方法二、高频应用场景面试重点1. 统一日志记录最常用用途自动记录方法入参、出参、执行时间、调用链路不用每个方法手写日志。实现环绕通知/前置后置通知// 示例记录接口入参、出参、耗时Around(execution(* com.xxx.service.*.*(..)))publicObjectlogAround(ProceedingJoinPointjoinPoint)throwsThrowable{// 入参日志// 执行目标方法// 出参耗时日志}2. 声明式事务管理用途方法执行前开启事务成功提交异常回滚。实现SpringTransactional底层就是 AOP核心自动管理事务生命周期无需手动commit/rollback3. 权限控制 / 安全校验用途接口调用前校验用户是否登录、是否拥有权限。实现前置通知Before(execution(* com.xxx.controller.*.*(..)))publicvoidcheckPermission(){// 校验token、角色、权限}4. 接口限流 / 防重复提交用途防止恶意请求、接口被刷、表单重复提交。实现环绕通知 Redis 计数/锁5. 缓存统一管理用途查询先查缓存不存在再查库自动更新缓存。实现环绕通知Spring CacheCacheable底层也是 AOP。6. 性能监控 / 统计用途统计方法执行耗时、慢查询、接口QPS、异常率。实现环绕通知记录开始/结束时间7. 全局异常处理用途统一捕获业务方法异常包装成标准响应返回。实现异常通知8. 操作日志 / 审计日志用途记录谁、在什么时间、做了什么操作、修改了什么数 据。实现后置通知 自定义注解AOP 相关概念1、 Aspect 切面 封装共通业务逻辑的封装共通业务逻辑的类叫切面类 用切面类创建的对象叫切面对象 Aspect 生成代理对象。2、 JoinPoint 连接点 用来说明共通业务逻辑 所嵌入的位置 一般封装了方法的信息上3、 PointCut 切点 多个连接点组成的一个集合 (后面会讲用表达式来表达切点 pointCut)4、 Target 目标 要加强的对象5、 Proxy 代理 增强之后的对象目标6、 Advice 通知 五大时机目标方法调用之前(前置通知Before)目标方法调用之后后置通知AfterReturning目标方法调用前后环绕通知 Round目标 方法最终通知最终通知After和后置通知的区别是当发生异常时仍然执行目标方法出现异常异常通知发生异常时才会执行AfterThrowing切点表达式的写法三种bean限定、类型、方法5.1 bean 限定表达式bean(bean对象在容器的id)支持统配。如 dao5.2 类型限定表达式Within (类型限定) 表达式中最后一部分必须是类型举例: com.xdl.dao.代表给dao 包下类型加入切面逻辑 但不包含子包5.3 方法限定表达式execution(方法限定表达式)execution方法限定表达式的4构成是 权限修饰 方法返回值类型(可以不写) 方法名(参数列表) throws 异常aop:before method“printSysTime” pointcut“execution(* * Account())”/aop:before method“printSysTime” pointcut“execution(* *Account())”/注意方法返回值 方法名() 是必须的 其它可选 void *Account()Spring AOP 五种通知类型面试精简背诵版Before 前置通知目标方法执行之前执行不能阻止目标方法运行拿不到返回值。AfterReturning 后置返回通知目标方法正常执行完毕、无异常后执行能获取方法返回值抛异常不会执行。AfterThrowing 异常通知目标方法抛出异常时才执行可以捕获异常信息。After 最终通知无论目标方法正常结束还是抛异常最后都会执行类似 finally。Around 环绕通知功能最强包裹目标方法可以在方法前、后、异常、最终都自定义处理能控制是否执行目标方法、修改入参、修改返回值。执行顺序背下来环绕前置 → 前置通知 → 目标方法执行 → 环绕后置 → 正常返回通知/异常通知 → 最终通知AOP 环绕通知示例代码完整代码importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.springframework.stereotype.Component;/** * 环绕通知示例记录方法执行耗时、入参、出参 */Aspect// 切面ComponentpublicclassMyAroundAdvice{// 1. 定义切点匹配 com.example.service 包下所有类的所有方法Pointcut(execution(* com.example.service.*.*(..)))publicvoidpointcut(){}// 2. 环绕通知核心Around(pointcut())publicObjectaround(ProceedingJoinPointjoinPoint){longstartSystem.currentTimeMillis();Objectresultnull;try{// 目标方法执行前 System.out.println(【环绕前置】方法开始执行入参joinPoint.getArgs());// 执行目标方法必须调用否则业务方法不执行resultjoinPoint.proceed();// 目标方法正常执行后 System.out.println(【环绕后置】方法正常结束返回值result);}catch(Throwablee){// 目标方法抛出异常时 System.out.println(【环绕异常】方法执行异常e.getMessage());thrownewRuntimeException(e);// 异常继续抛出}finally{// 最终执行 longtimeSystem.currentTimeMillis()-start;System.out.println(【环绕最终】方法耗时timems);}returnresult;// 返回目标方法执行结果}}面试必背 3 个核心点环绕通知注解Around必须参数ProceedingJoinPoint用来执行目标方法核心方法proceed()——不调用这个方法业务逻辑不会执行使用AOP代理模式脱敏字段如何处理1. 字段脱敏注解importjava.lang.annotation.*;Target(ElementType.FIELD)Retention(RetentionPolicy.RUNTIME)publicinterfaceDesensitize{/** 脱敏类型PHONE-手机号、ID_CARD-身份证、EMAIL-邮箱 */Stringtype();}2. 脱敏工具类publicclassDesensitizeUtil{// 手机号脱敏 13812345678 → 138****5678publicstaticStringphone(Stringstr){if(strnull||str.length()!11){returnstr;}returnstr.substring(0,3)****str.substring(7);}// 身份证脱敏publicstaticStringidCard(Stringstr){if(strnull||str.length()15){returnstr;}returnstr.substring(0,6)********str.substring(str.length()-4);}// 邮箱脱敏publicstaticStringemail(Stringstr){if(strnull||!str.contains()){returnstr;}String[]splitstr.split();returnsplit[0].charAt(0)****split[1];}}3. 统一日志智能脱敏 AOP 切面核心importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.springframework.stereotype.Component;importjava.lang.reflect.Field;importjava.util.Arrays;AspectComponentpublicclassLogAndDesensitizeAspect{// 全局拦截所有controller接口统一日志Pointcut(execution(* com.xxx.controller..*.*(..)))publicvoidpointcut(){}Around(pointcut())publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{longstartSystem.currentTimeMillis();// 1. 前置打印请求信息StringmethodNamejoinPoint.getSignature().getName();Object[]argsjoinPoint.getArgs();System.out.println(接口方法methodName请求入参args);// 2. 执行目标方法ObjectresultjoinPoint.proceed();// 3. 智能判断实体有Desensitize字段才做脱敏没有直接跳过if(hasDesensitizeField(result)){doDesensitize(result);}// 4. 后置打印脱敏后返回值、耗时longcostSystem.currentTimeMillis()-start;System.out.println(接口返回值(脱敏后)result执行耗时costms);returnresult;}/** * 判断实体是否包含Desensitize注解字段 */privatebooleanhasDesensitizeField(Objectobj){if(objnull){returnfalse;}Class?clazzobj.getClass();// 只要有一个字段标记了脱敏注解就需要脱敏returnArrays.stream(clazz.getDeclaredFields()).anyMatch(field-field.isAnnotationPresent(Desensitize.class));}/** * 反射执行字段脱敏 */privatevoiddoDesensitize(Objectobj)throwsIllegalAccessException{Class?clazzobj.getClass();Field[]fieldsclazz.getDeclaredFields();for(Fieldfield:fields){// 只处理加了脱敏注解的字段if(!field.isAnnotationPresent(Desensitize.class)){continue;}field.setAccessible(true);Objectvaluefield.get(obj);if(!(valueinstanceofString)){continue;}Stringcontent(String)value;Desensitizedesensitizefield.getAnnotation(Desensitize.class);Stringtypedesensitize.type();// 根据类型脱敏StringnewContentswitch(type){casePHONE-DesensitizeUtil.phone(content);caseID_CARD-DesensitizeUtil.idCard(content);caseEMAIL-DesensitizeUtil.email(content);default-content;};field.set(obj,newContent);}}}4. 实体类使用示例publicclassUserVO{privateStringuserName;Desensitize(typePHONE)privateStringphone;Desensitize(typeID_CARD)privateStringidCard;// getter/setter}5. 业务使用说明普通接口返回普通VO无任何脱敏注解字段→ 只打日志不走反射、不脱敏无性能损耗敏感接口返回含手机号/身份证VO字段上加Desensitize→ AOP自动识别自动脱敏不需要在Controller/Service方法上加任何注解零侵入。二、面试标准话术直接背诵1. 方案思路话术我项目中AOP环绕通知统一拦截所有Controller接口做统一日志记录、接口耗时统计。返回结果后不全局强制反射脱敏而是先做智能判断第一步判断返回值实体中是否存在标注Desensitize的敏感字段第二步如果没有脱敏字段直接跳过脱敏逻辑不做任何反射处理避免性能浪费第三步如果实体带有脱敏注解字段再通过反射遍历字段根据注解类型做手机号、身份证、邮箱脱敏。2. 对比优劣话术不像全局一刀切所有接口都反射避免无用反射、性能开销小也不需要在每个敏感接口方法上加自定义注解减少业务侵入只需要在VO敏感字段上加脱敏注解即可配置简单、通用性强、易维护完美兼顾大部分普通接口只打日志、少数敏感接口自动脱敏的业务场景。3. 面试官追问为什么不用方法注解标记脱敏接口如果用方法注解指定哪些接口脱敏需要手动逐个给接口加注解繁琐且侵入业务全局全部脱敏又会对大量无敏感字段的接口做无效反射浪费性能最优方案就是切面全局拦截做通用日志实体字段加脱敏标记程序自动识别按需脱敏兼顾性能、通用性和低侵入。