从0开始学习Java+AI知识点总结-25.web实战(AOP)
一、AOP 核心认知:为什么它是后端开发的 “效率神器”?
1.1 什么是 AOP?
AOP(Aspect Oriented Programming,面向切面编程),本质是面向特定方法编程—— 它不侵入原有业务代码,而是通过 “切面” 对指定方法进行功能增强(如日志、计时、事务控制等)。
可以这么理解:业务方法是 “主线”,日志、权限等共性逻辑是 “辅助线”,AOP 让 “辅助线” 与 “主线” 分离,却能在需要时自动附着到 “主线” 上。
1.2 没有 AOP 时,我们有多 “麻烦”?(原始方案 VS AOP 方案)
以 “统计方法执行耗时” 为例,对比两种实现方式:
原始方案:重复代码泛滥
每个业务方法都要写 “开始计时→执行方法→结束计时→打印日志” 的逻辑,代码冗余且难维护:
// 业务方法1:查询列表 public List<User> list() { // 重复计时代码 long beginTime = System.currentTimeMillis(); // 核心业务逻辑 List<User> userList = userMapper.list(); // 重复日志代码 long endTime = System.currentTimeMillis(); log.info("list方法耗时:{}ms", endTime - beginTime); return userList; } // 业务方法2:删除数据 public void delete(Integer id) { // 重复计时代码 long beginTime = System.currentTimeMillis(); // 核心业务逻辑 userMapper.delete(id); // 重复日志代码 long endTime = System.currentTimeMillis(); log.info("delete方法耗时:{}ms", endTime - beginTime); } // 更多方法...每个都要加重复代码 |
AOP 方案:一次编写,全局复用
只需写一个 “计时切面”,所有目标方法自动生效,无需修改业务代码:
// 切面类:统计耗时的共性逻辑 @Aspect @Component @Slf4j public class TimeStatAspect { // 环绕通知:匹配指定方法(如service包下所有方法) @Around("execution(* com.example.service.*.*(..))") public Object statTime(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 计时开始 long beginTime = System.currentTimeMillis(); // 2. 执行原始业务方法 Object result = joinPoint.proceed(); // 3. 计时结束+日志 long endTime = System.currentTimeMillis(); log.info("{}方法耗时:{}ms", joinPoint.getSignature().getName(), endTime - beginTime); return result; } } |
1.3 AOP 的 4 大核心价值
- 减少重复代码:共性逻辑(日志、计时)只需写一次,避免 “复制粘贴”;
- 代码无侵入:不修改原有业务代码,降低维护成本(后续改共性逻辑只需动切面);
- 提高开发效率:开发者专注于核心业务,无需关注日志、权限等辅助逻辑;
- 维护更便捷:共性逻辑集中管理,比如要改日志格式,只需修改切面类。
二、Spring AOP 快速入门:3 步实现方法耗时统计
掌握 AOP 无需复杂配置,Spring Boot 已封装好 starter,3 步即可上手。
2.1 步骤 1:导入 AOP 依赖
在pom.xml中引入 Spring Boot AOP starter(无需指定版本,Spring Boot 会自动管理):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> |
2.2 步骤 2:编写切面类
切面类是 AOP 的核心,需用@Aspect标识 “这是一个切面”,用@Component注入 Spring 容器,再通过 “通知注解” 定义增强逻辑。
以 “统计 service 层所有方法耗时” 为例:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; // 1. @Aspect:标识当前类为切面类 // 2. @Component:注入Spring容器 @Aspect @Component public class TimeStatAspect { private static final Logger log = LoggerFactory.getLogger(TimeStatAspect.class); // 3. 环绕通知:@Around + 切入点表达式(匹配service包下所有类的所有方法) @Around("execution(* com.example.service.*.*(..))") public Object statMethodTime(ProceedingJoinPoint joinPoint) throws Throwable { // ① 记录开始时间 long startTime = System.currentTimeMillis();
// ② 执行原始业务方法(必须调用,否则目标方法不执行) Object result = joinPoint.proceed();
// ③ 记录结束时间+计算耗时 long endTime = System.currentTimeMillis(); long costTime = endTime - startTime;
// ④ 打印日志(获取方法名、类名等信息) String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getName(); log.info("[AOP耗时统计] 类:{} | 方法:{} | 耗时:{}ms", className, methodName, costTime);
// ⑤ 返回原始方法的返回值(否则调用方拿不到结果) return result; } } |
2.3 步骤 3:测试验证
启动 Spring Boot 项目,调用 service 层方法(如userService.list()),控制台会自动输出耗时日志:
[AOP耗时统计] 类:com.example.service.UserServiceImpl | 方法:list | 耗时:35ms [AOP耗时统计] 类:com.example.service.UserServiceImpl | 方法:getById | 耗时:12ms |
至此,你已实现了 AOP 的核心功能!无需修改任何业务代码,就能全局统计方法耗时。
三、AOP 核心概念:5 个术语必须吃透(附代码解析)
要灵活使用 AOP,必须理解以下 5 个核心概念 —— 它们是后续进阶的基础。
概念 | 定义 | 代码对应示例 |
连接点(JoinPoint) | 可被 AOP 控制的方法(如 service 层的 list、delete 方法),包含方法执行时的信息(类名、参数等) | joinPoint.getTarget().getClass().getName()(获取类名) |
通知(Advice) | 抽离的共性功能(如耗时统计、日志记录),体现为一个方法,有 5 种类型 | 上述statMethodTime方法(环绕通知) |
切入点(PointCut) | 匹配连接点的 “规则”(哪些方法需要被增强),用表达式描述 | execution(* com.example.service.*.*(..)) |
切面(Aspect) | 通知 + 切入点的组合(“对哪些方法,执行什么共性逻辑”) | TimeStatAspect类(@Aspect 标识) |
目标对象(Target) | 被 AOP 增强的原始对象(如UserServiceImpl实例) | joinPoint.getTarget()(获取目标对象) |
代理对象(Proxy) | Spring 动态生成的对象,代理目标对象并注入增强逻辑(开发者无需手动创建) | Spring 自动生成,无需代码干预 |
3.1 连接点(JoinPoint):获取方法执行信息的 “工具”
Spring 用JoinPoint抽象连接点,通过它可获取目标方法的关键信息:
- getTarget():获取目标对象(原始业务对象,如UserServiceImpl);
- getSignature():获取方法签名(含方法名、返回值类型);
- getArgs():获取方法参数(如delete方法的id参数);
注意:
- 环绕通知(@Around)需用ProceedingJoinPoint(JoinPoint的子类),因为要调用proceed()执行目标方法;
- 其他通知(@Before、@After 等)用JoinPoint即可。
示例(获取方法参数):
@Before("execution(* com.example.service.*.delete(..))") public void beforeDelete(JoinPoint joinPoint) { // 获取delete方法的参数(如id=10) Object[] args = joinPoint.getArgs(); log.info("删除操作的参数:{}", args[0]); // 输出:删除操作的参数:10 } |
3.2 通知(Advice):5 种类型详解(执行时机 + 场景)
通知是 AOP 的 “执行逻辑”,根据执行时机不同,Spring 提供 5 种通知类型,核心是环绕通知。
通知注解 | 执行时机 | 适用场景 | 注意事项 |
@Around | 目标方法执行前 + 执行后 | 耗时统计、事务控制(需前后操作) | 必须调用proceed(),返回值为Object |
@Before | 目标方法执行前 | 权限校验、参数校验 | 无返回值,不能阻止目标方法执行(除非抛异常) |
@After | 目标方法执行后(无论是否抛异常) | 资源释放(如关闭流) | 无返回值 |
@AfterReturning | 目标方法正常执行后(无异常) | 返回值处理(如格式化结果) | 可获取目标方法返回值 |
@AfterThrowing | 目标方法抛异常后 | 异常记录、告警(如发送邮件) | 可获取异常信息 |
5 种通知代码示例(含执行顺序测试)
@Aspect @Component @Slf4j public class AdviceDemoAspect { // 抽取公共切入点(避免重复写表达式) @Pointcut("execution(* com.example.service.*.testAdvice(..))") private void testPointCut() {} // 1. 环绕通知 @Around("testPointCut()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { log.info("环绕通知:目标方法执行前"); Object result = joinPoint.proceed(); // 执行目标方法 log.info("环绕通知:目标方法执行后"); return result; } // 2. 前置通知 @Before("testPointCut()") public void beforeAdvice(JoinPoint joinPoint) { log.info("前置通知:目标方法执行前"); } // 3. 后置通知 @After("testPointCut()") public void afterAdvice(JoinPoint joinPoint) { log.info("后置通知:目标方法执行后(无论是否异常)"); } // 4. 返回后通知 @AfterReturning("testPointCut()") public void afterReturningAdvice(JoinPoint joinPoint) { log.info("返回后通知:目标方法正常执行后"); } // 5. 异常后通知 @AfterThrowing("testPointCut()") public void afterThrowingAdvice(JoinPoint joinPoint) { log.info("异常后通知:目标方法抛异常后"); } } |
执行顺序测试结果
- 无异常时:
环绕通知前 → 前置通知 → 目标方法 → 环绕通知后 → 返回后通知 → 后置通知
日志输出:
环绕通知:目标方法执行前 前置通知:目标方法执行前 目标方法执行中... 环绕通知:目标方法执行后 返回后通知:目标方法正常执行后 后置通知:目标方法执行后(无论是否异常) |
- 有异常时(目标方法抛NullPointerException):
环绕通知前 → 前置通知 → 目标方法异常 → 异常后通知 → 后置通知(环绕通知后不执行)
日志输出:
环绕通知:目标方法执行前 前置通知:目标方法执行前 目标方法执行中...(抛异常) 异常后通知:目标方法抛异常后 后置通知:目标方法执行后(无论是否异常) |
3.3 切入点(PointCut):匹配目标方法的 “规则”
切入点是 “哪些方法需要被增强” 的规则,用切入点表达式描述,Spring 支持 2 种常用表达式:execution和@annotation。
四、AOP 进阶:从 “会用” 到 “用好” 的关键技巧
掌握基础后,这些进阶技巧能让你在实际项目中更灵活地使用 AOP。
4.1 @PointCut:抽取公共切入点,避免重复代码
如果多个通知用相同的切入点表达式(如execution(* com.example.service.*.*(..))),可通过@PointCut抽取,提高复用性。
用法步骤:
- 定义一个无逻辑的方法(如pt()),用@PointCut标注并指定表达式;
- 其他通知通过 “方法名 ()” 引用该切入点。
示例:
@Aspect @Component @Slf4j public class PointCutDemoAspect { // 1. 抽取公共切入点(private:仅当前切面可用;public:其他切面也可引用) @Pointcut("execution(* com.example.service.*.*(..))") public void servicePointCut() {} // 2. 引用切入点:无需重复写表达式 @Before("servicePointCut()") public void beforeAdvice() { log.info("前置通知:service层方法执行前"); } @Around("servicePointCut()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { log.info("环绕通知:service层方法执行前"); Object result = joinPoint.proceed(); log.info("环绕通知:service层方法执行后"); return result; } } |
跨切面引用切入点:
若其他切面需引用该切入点,只需指定 “全类名。方法名 ()”:
@Aspect @Component @Slf4j public class OtherAspect { // 引用PointCutDemoAspect的公共切入点 @After("com.example.aop.PointCutDemoAspect.servicePointCut()") public void afterAdvice() { log.info("其他切面:service层方法执行后"); } } |
4.2 多切面执行顺序:用 @Order 控制优先级
当多个切面同时匹配一个目标方法时(如 “耗时统计切面” 和 “日志切面”),默认按切面类名的字母顺序执行:
- 前置通知:字母靠前的切面先执行;
- 后置通知:字母靠前的切面后执行。
若需自定义顺序,用@Order(数字)标注切面类,数字越小,优先级越高:
- 前置通知:数字小的先执行;
- 后置通知:数字小的后执行。
示例:
定义 3 个切面,用@Order指定顺序:
// 切面1:Order=1(优先级最高) @Aspect @Component @Order(1) @Slf4j public class Aspect1 { @Before("execution(* com.example.service.*.*(..))") public void before() { log.info("Aspect1:前置通知"); } @After("execution(* com.example.service.*.*(..))") public void after() { log.info("Aspect1:后置通知"); } } // 切面2:Order=2 @Aspect @Component @Order(2) @Slf4j public class Aspect2 { @Before("execution(* com.example.service.*.*(..))") public void before() { log.info("Aspect2:前置通知"); } @After("execution(* com.example.service.*.*(..))") public void after() { log.info("Aspect2:后置通知"); } } |
执行结果:
Aspect1:前置通知(Order小先执行) Aspect2:前置通知 目标方法执行中... Aspect2:后置通知(Order大先执行) Aspect1:后置通知(Order小后执行) |
4.3 切入点表达式:2 种常用方式 + 通配符技巧
切入点表达式是 AOP 的 “核心规则”,掌握 2 种常用方式和通配符,能精准匹配目标方法。
方式 1:execution(按方法签名匹配,最常用)
语法:execution(访问修饰符? 返回值 包名.类名.?方法名(参数) throws 异常?)
- 带?的部分可省略;
- 支持通配符:*(单个任意符号)、..(多个任意符号,如任意包、任意参数)。
常用示例(覆盖 90% 场景):
表达式 | 含义 |
execution(* com.example.service.*.*(..)) | 匹配 com.example.service 包下所有类的所有方法(任意返回值、任意参数) |
execution(* com.example.service..*.save(..)) | 匹配 com.example.service 及其子包下所有类的 save 方法(如 UserService.save、OrderService.save) |
execution(com.example.common.Result com.example.service..*.update(*)) | 匹配 service 及其子包下返回值为 Result、且有 1 个任意参数的 update 方法 |
execution(* com.example.service.UserService.list()) | 精准匹配 UserService 的 list 方法(无参数) |
execution(* com.example.service.*.delete(Integer)) | 匹配 service 包下所有类的 delete 方法(参数为 Integer) |
组合表达式(用 &&、||、!):
- 匹配 save 或 delete 方法:execution(* com.example.service.*.save(..)) || execution(* com.example.service.*.delete(..))
- 不匹配 list 方法:execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.list(..))
方式 2:@annotation(按注解匹配,更灵活)
当目标方法无统一命名规则(如部分增删改方法名是 add、remove、edit),用execution表达式会很繁琐,此时用@annotation更灵活。
核心思路:
- 自定义一个注解(如@OperateLog);
- 在需要增强的方法上标注该注解;
- 切入点表达式用@annotation(注解全类名)匹配。
实现步骤:
- 自定义注解:
import java.lang.annotation.*; // 注解作用在方法上 @Target(ElementType.METHOD) // 注解保留到运行时(AOP需在运行时扫描) @Retention(RetentionPolicy.RUNTIME) public @interface OperateLog { // 可添加属性(如日志类型),默认值为"operate" String type() default "operate"; } |
- 在目标方法上标注注解:
@Service public class UserService { // 标注需要记录日志的方法 @OperateLog(type = "add") public void addUser(User user) { userMapper.insert(user); } @OperateLog(type = "delete") public void removeUser(Integer id) { userMapper.delete(id); } // 不标注:不被AOP增强 public List<User> listUser() { return userMapper.list(); } } |
- 编写切面类,用 @annotation 匹配:
@Aspect @Component @Slf4j public class OperateLogAspect { // 切入点:匹配标注了@OperateLog的方法 @Pointcut("@annotation(com.example.anno.OperateLog)") private void operateLogPointCut() {} // 环绕通知:记录操作日志 @Around("operateLogPointCut() && @annotation(logAnno)") public Object recordOperateLog(ProceedingJoinPoint joinPoint, OperateLog logAnno) throws Throwable { // 获取注解的type属性(如"add"、"delete") String logType = logAnno.type(); log.info("操作日志类型:{}", logType); // 获取方法参数、类名等信息 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); log.info("操作日志:类={}, 方法={}, 参数={}", className, methodName, args); // 执行目标方法 Object result = joinPoint.proceed(); log.info("操作日志:方法执行完成,返回值={}", result); return result; } } |
4.4 切入点表达式书写建议(提高精准度,避免误匹配)
- 方法命名规范:增删改查方法统一前缀(如 saveXXX、deleteXXX、updateXXX、getXXX),方便execution匹配;
- 基于接口匹配:优先匹配接口方法(如execution(* com.example.service.UserService.*(..))),而非实现类,增强扩展性;
- 缩小匹配范围:包名尽量不用..(避免匹配过多子包),用*匹配单个包(如com.example.service.*.*(..));
- 优先用 @annotation:无规则方法用自定义注解匹配,灵活且易维护。
五、实战案例:AOP+ThreadLocal 实现操作日志持久化
学完理论,我们通过一个实战案例巩固 ——记录所有增删改接口的操作日志到数据库,这是企业项目的高频需求。
5.1 需求分析
需记录的日志信息:
- 操作人 ID(当前登录用户);
- 操作时间;
- 执行类名、方法名;
- 方法参数(如新增用户的姓名、年龄);
- 方法返回值;
- 方法执行耗时(ms)。
5.2 技术选型
- 通知类型:@Around(需前后计时 + 获取返回值);
- 切入点:@annotation(灵活匹配增删改方法);
- 当前用户传递:ThreadLocal(同一请求线程内共享用户 ID,避免参数传递冗余);
- 持久化:MyBatis(插入日志到数据库)。
5.3 完整实现步骤
步骤 1:准备数据库表和实体类
- 建表语句(MySQL):
CREATE TABLE `operate_log` ( `id` int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', `operate_user_id` int unsigned COMMENT '操作人ID', `operate_time` datetime COMMENT '操作时间', `class_name` varchar(100) COMMENT '执行类名', `method_name` varchar(100) COMMENT '执行方法名', `method_params` varchar(1000) COMMENT '方法参数(JSON格式)', `return_value` varchar(2000) COMMENT '返回值(JSON格式)', `cost_time` int COMMENT '执行耗时(ms)' ) COMMENT '操作日志表'; |
- 实体类(OperateLog):
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor public class OperateLog { private Integer id; private Integer operateUserId; // 操作人ID private LocalDateTime operateTime; // 操作时间 private String className; // 执行类名 private String methodName; // 执行方法名 private String methodParams; // 方法参数 private String returnValue; // 返回值 private Long costTime; // 耗时(ms) } |
步骤 2:编写 Mapper 接口(OperateLogMapper)
import com.example.pojo.OperateLog; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OperateLogMapper { // 插入操作日志 @Insert("INSERT INTO operate_log (operate_user_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " + "VALUES (#{operateUserId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})") void insert(OperateLog operateLog); } |
步骤 3:自定义日志注解(@OperateLog)
import java.lang.annotation.*; @Target(ElementType.METHOD) // 作用于方法 @Retention(RetentionPolicy.RUNTIME) // 运行时保留 public @interface OperateLog { } |
步骤 4:ThreadLocal 工具类(传递当前用户 ID)
当前登录用户 ID 通常在 Filter 中解析 JWT 令牌获得,需传递给 AOP 切面,用 ThreadLocal 实现线程内数据共享。
/** * ThreadLocal工具类:存储当前登录用户ID * 注意:用完必须调用remove(),避免线程池复用导致脏数据 */ public class CurrentUserHolder { // 静态ThreadLocal:每个线程单独存储一份数据 private static final ThreadLocal<Integer> USER_ID_HOLDER = new ThreadLocal<>(); // 设置当前用户ID public static void setUserId(Integer userId) { USER_ID_HOLDER.set(userId); } // 获取当前用户ID public static Integer getUserId() { return USER_ID_HOLDER.get(); } // 移除当前用户ID(必须调用,避免内存泄漏) public static void removeUserId() { USER_ID_HOLDER.remove(); } } |
步骤 5:Filter 中解析 JWT 并设置用户 ID
在 JWT 过滤器中解析令牌,获取用户 ID 并存入 ThreadLocal,请求结束后移除:
import com.example.utils.CurrentUserHolder; import com.example.utils.JwtUtils; import io.jsonwebtoken.Claims; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j @WebFilter(urlPatterns = "/*") // 拦截所有请求 public class JwtFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; // 1. 排除登录接口(无需校验JWT) String uri = request.getRequestURI(); if (uri.contains("/login")) { chain.doFilter(request, response); return; } // 2. 获取请求头中的JWT令牌 String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); // 未登录 return; } // 3. 解析JWT令牌,获取用户ID try { Claims claims = JwtUtils.parseJwt(token); Integer userId = Integer.valueOf(claims.get("userId").toString()); // 4. 存入ThreadLocal CurrentUserHolder.setUserId(userId); // 5. 放行请求 chain.doFilter(request, response); } catch (Exception e) { response.setStatus(401); // 令牌无效 return; } finally { // 6. 请求结束,移除ThreadLocal中的数据(避免内存泄漏) CurrentUserHolder.removeUserId(); } } } |
步骤 6:编写 AOP 切面类(核心逻辑)
import com.example.anno.OperateLog; import com.example.mapper.OperateLogMapper; import com.example.pojo.OperateLog; import com.example.utils.CurrentUserHolder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Arrays; @Aspect @Component public class OperateLogAspect { @Autowired private OperateLogMapper operateLogMapper; @Autowired private ObjectMapper objectMapper; // 用于将参数/返回值转为JSON // 切入点:匹配标注@OperateLog的方法 @Pointcut("@annotation(com.example.anno.OperateLog)") private void operateLogPointCut() {} // 环绕通知:记录日志并插入数据库 @Around("operateLogPointCut()") public Object recordOperateLog(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 记录开始时间 long startTime = System.currentTimeMillis(); // 2. 执行目标方法(Controller层的增删改方法) Object result = joinPoint.proceed(); // 3. 记录结束时间,计算耗时 long endTime = System.currentTimeMillis(); long costTime = endTime - startTime; // 4. 构建OperateLog对象 OperateLog log = new OperateLog(); // 4.1 操作人ID(从ThreadLocal获取) log.setOperateUserId(CurrentUserHolder.getUserId()); // 4.2 操作时间 log.setOperateTime(LocalDateTime.now()); // 4.3 执行类名(Controller类名) log.setClassName(joinPoint.getTarget().getClass().getName()); // 4.4 执行方法名 log.setMethodName(joinPoint.getSignature().getName()); // 4.5 方法参数(转为JSON字符串) Object[] args = joinPoint.getArgs(); log.setMethodParams(objectMapper.writeValueAsString(args)); // 4.6 方法返回值(转为JSON字符串) log.setReturnValue(objectMapper.writeValueAsString(result)); // 4.7 执行耗时 log.setCostTime(costTime); // 5. 插入数据库 operateLogMapper.insert(log); // 6. 返回目标方法的结果 return result; } } |
步骤 7:在 Controller 方法上标注 @OperateLog
import com.example.anno.OperateLog; import com.example.common.Result; import com.example.pojo.User; import com.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; // 新增用户:标注@OperateLog,会被AOP记录日志 @OperateLog @PostMapping public Result addUser(@RequestBody User user) { userService.addUser(user); return Result.success("新增用户成功"); } // 删除用户:标注@OperateLog @OperateLog @DeleteMapping("/{id}") public Result deleteUser(@PathVariable Integer id) { userService.removeUser(id); return Result.success("删除用户成功"); } // 查询用户:不标注,不记录日志 @GetMapping public Result listUser() { return Result.success(userService.listUser()); } } |
5.4 测试效果
调用/users(POST)新增用户后,查看数据库operate_log表:
id | operate_user_id | operate_time | class_name | method_name | method_params | return_value | cost_time |
1 | 1001 | 2024-08-20 15:30:22 | com.example.controller.UserController | addUser | [{"id":null,"name":"张三","age":25}] | {"code":200,"msg":"新增用户成功","data":null} | 56 |
日志信息完整记录,且无需修改任何业务代码!
六、AOP 常见应用场景与避坑指南
6.1 AOP 在实际项目中的 3 大高频场景
- 日志记录:操作日志、访问日志、异常日志;
- 性能监控:方法耗时统计、接口 QPS 统计;
- 权限控制:接口访问权限校验(如判断用户是否有管理员权限);
- 事务控制:Spring 事务底层就是用 AOP 实现(@Transactional注解),自动开启 / 提交 / 回滚事务。
6.2 避坑要点(新手必看)
- @Around 必须调用 proceed ():否则目标方法不会执行,导致业务异常;
- @Around 返回值必须为 Object:否则调用方(如 Controller)拿不到返回值;
- ThreadLocal 用完必须 remove ():线程池复用会导致脏数据,需在 Filter 或 AOP finally 块中调用remove();
- 切入点表达式不要过度宽泛:如execution(* com.example..*(..))会匹配所有包的方法,影响性能;
- 通知顺序要明确:多切面时用@Order指定顺序,避免依赖默认字母顺序导致逻辑混乱。
6.3 性能影响说明
Spring AOP 基于动态代理(JDK 动态代理或 CGLIB),性能损耗极小(单方法增强耗时通常在 1ms 以内),完全满足企业级项目需求。除非是高频调用的核心方法(如每秒调用 10 万次),否则无需担心性能问题。
七、总结:掌握 AOP,让你的代码 “降维打击”
AOP 不是 “炫技工具”,而是解决 “共性逻辑与业务逻辑分离” 的最佳方案。掌握它,你能:
- 写出更简洁的业务代码(无冗余日志、权限逻辑);
- 大幅降低维护成本(改共性逻辑只需动切面);
- 理解 Spring 事务、权限框架等底层原理(很多框架都基于 AOP)。
如果你觉得这篇文章对你有帮助,别忘了点赞 + 收藏。