含义和作用
含义:指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式;
作用:提供组件和切面的复用程度,提高程序灵活性,有利于切面和组件解耦 。
Java AOP 应用场景
AOP(面向切面编程)核心作用:在不修改业务代码的前提下,统一处理横切逻辑(日志、事务、权限等),解耦、复用、易维护。
一、AOP 核心思想(面试必背)
- 横切关注点:日志、事务、权限、缓存、监控等通用功能
- 切面(Aspect):封装通用逻辑的类
- 通知(Advice):什么时候执行(前置/后置/环绕/异常/最终)
- 切点(Pointcut):在哪里执行(匹配哪些方法)
二、高频应用场景(面试重点)
1. 统一日志记录(最常用)
用途:自动记录方法入参、出参、执行时间、调用链路,不用每个方法手写日志。
实现:环绕通知/前置+后置通知
// 示例:记录接口入参、出参、耗时@Around("execution(* com.xxx.service.*.*(..))")publicObjectlogAround(ProceedingJoinPointjoinPoint)throwsThrowable{// 入参日志// 执行目标方法// 出参+耗时日志}2. 声明式事务管理
用途:方法执行前开启事务,成功提交,异常回滚。
实现:Spring@Transactional底层就是 AOP
核心:自动管理事务生命周期,无需手动commit/rollback
3. 权限控制 / 安全校验
用途:接口调用前校验用户是否登录、是否拥有权限。
实现:前置通知
@Before("execution(* com.xxx.controller.*.*(..))")publicvoidcheckPermission(){// 校验token、角色、权限}4. 接口限流 / 防重复提交
用途:防止恶意请求、接口被刷、表单重复提交。
实现:环绕通知 + Redis 计数/锁
5. 缓存统一管理
用途:查询先查缓存,不存在再查库,自动更新缓存。
实现:环绕通知
Spring Cache(@Cacheable)底层也是 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),支持统配。如 :dao
5.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){longstart=System.currentTimeMillis();Objectresult=null;try{// ========== 目标方法执行前 ==========System.out.println("【环绕前置】方法开始执行,入参:"+joinPoint.getArgs());// 执行目标方法(必须调用,否则业务方法不执行)result=joinPoint.proceed();// ========== 目标方法正常执行后 ==========System.out.println("【环绕后置】方法正常结束,返回值:"+result);}catch(Throwablee){// ========== 目标方法抛出异常时 ==========System.out.println("【环绕异常】方法执行异常:"+e.getMessage());thrownewRuntimeException(e);// 异常继续抛出}finally{// ========== 最终执行 ==========longtime=System.currentTimeMillis()-start;System.out.println("【环绕最终】方法耗时:"+time+"ms");}returnresult;// 返回目标方法执行结果}}面试必背 3 个核心点
- 环绕通知注解:
@Around - 必须参数:
ProceedingJoinPoint(用来执行目标方法) - 核心方法:
proceed()——不调用这个方法,业务逻辑不会执行
使用AOP代理模式,脱敏字段如何处理
1. 字段脱敏注解
importjava.lang.annotation.*;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public@interfaceDesensitize{/** 脱敏类型:PHONE-手机号、ID_CARD-身份证、EMAIL-邮箱 */Stringtype();}2. 脱敏工具类
publicclassDesensitizeUtil{// 手机号脱敏 13812345678 → 138****5678publicstaticStringphone(Stringstr){if(str==null||str.length()!=11){returnstr;}returnstr.substring(0,3)+"****"+str.substring(7);}// 身份证脱敏publicstaticStringidCard(Stringstr){if(str==null||str.length()<15){returnstr;}returnstr.substring(0,6)+"********"+str.substring(str.length()-4);}// 邮箱脱敏publicstaticStringemail(Stringstr){if(str==null||!str.contains("@")){returnstr;}String[]split=str.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;@Aspect@ComponentpublicclassLogAndDesensitizeAspect{// 全局拦截所有controller接口,统一日志@Pointcut("execution(* com.xxx.controller..*.*(..))")publicvoidpointcut(){}@Around("pointcut()")publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{longstart=System.currentTimeMillis();// 1. 前置:打印请求信息StringmethodName=joinPoint.getSignature().getName();Object[]args=joinPoint.getArgs();System.out.println("接口方法:"+methodName+",请求入参:"+args);// 2. 执行目标方法Objectresult=joinPoint.proceed();// 3. 智能判断:实体有@Desensitize字段才做脱敏,没有直接跳过if(hasDesensitizeField(result)){doDesensitize(result);}// 4. 后置:打印脱敏后返回值、耗时longcost=System.currentTimeMillis()-start;System.out.println("接口返回值(脱敏后):"+result+",执行耗时:"+cost+"ms");returnresult;}/** * 判断实体是否包含@Desensitize注解字段 */privatebooleanhasDesensitizeField(Objectobj){if(obj==null){returnfalse;}Class<?>clazz=obj.getClass();// 只要有一个字段标记了脱敏注解,就需要脱敏returnArrays.stream(clazz.getDeclaredFields()).anyMatch(field->field.isAnnotationPresent(Desensitize.class));}/** * 反射执行字段脱敏 */privatevoiddoDesensitize(Objectobj)throwsIllegalAccessException{Class<?>clazz=obj.getClass();Field[]fields=clazz.getDeclaredFields();for(Fieldfield:fields){// 只处理加了脱敏注解的字段if(!field.isAnnotationPresent(Desensitize.class)){continue;}field.setAccessible(true);Objectvalue=field.get(obj);if(!(valueinstanceofString)){continue;}Stringcontent=(String)value;Desensitizedesensitize=field.getAnnotation(Desensitize.class);Stringtype=desensitize.type();// 根据类型脱敏StringnewContent=switch(type){case"PHONE"->DesensitizeUtil.phone(content);case"ID_CARD"->DesensitizeUtil.idCard(content);case"EMAIL"->DesensitizeUtil.email(content);default->content;};field.set(obj,newContent);}}}4. 实体类使用示例
publicclassUserVO{privateStringuserName;@Desensitize(type="PHONE")privateStringphone;@Desensitize(type="ID_CARD")privateStringidCard;// getter/setter}5. 业务使用说明
- 普通接口返回普通VO:无任何脱敏注解字段→ 只打日志,不走反射、不脱敏,无性能损耗;
- 敏感接口返回含手机号/身份证VO:字段上加@Desensitize→ AOP自动识别,自动脱敏;
- 不需要在Controller/Service方法上加任何注解,零侵入。
二、面试标准话术(直接背诵)
1. 方案思路话术
我项目中AOP环绕通知统一拦截所有Controller接口,做统一日志记录、接口耗时统计。
返回结果后不全局强制反射脱敏,而是先做智能判断:
第一步,判断返回值实体中是否存在标注@Desensitize的敏感字段;
第二步,如果没有脱敏字段,直接跳过脱敏逻辑,不做任何反射处理,避免性能浪费;
第三步,如果实体带有脱敏注解字段,再通过反射遍历字段,根据注解类型做手机号、身份证、邮箱脱敏。
2. 对比优劣话术
- 不像全局一刀切所有接口都反射,避免无用反射、性能开销小;
- 也不需要在每个敏感接口方法上加自定义注解,减少业务侵入;
- 只需要在VO敏感字段上加脱敏注解即可,配置简单、通用性强、易维护;
- 完美兼顾大部分普通接口只打日志、少数敏感接口自动脱敏的业务场景。
3. 面试官追问:为什么不用方法注解标记脱敏接口?
如果用方法注解指定哪些接口脱敏,需要手动逐个给接口加注解,繁琐且侵入业务;
全局全部脱敏又会对大量无敏感字段的接口做无效反射,浪费性能;
最优方案就是切面全局拦截做通用日志,实体字段加脱敏标记,程序自动识别按需脱敏,兼顾性能、通用性和低侵入。