当前位置: 首页 > backend >正文

从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 大核心价值

  1. 减少重复代码:共性逻辑(日志、计时)只需写一次,避免 “复制粘贴”;
  2. 代码无侵入:不修改原有业务代码,降低维护成本(后续改共性逻辑只需动切面);
  3. 提高开发效率:开发者专注于核心业务,无需关注日志、权限等辅助逻辑;
  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)需用ProceedingJoinPointJoinPoint的子类),因为要调用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("异常后通知:目标方法抛异常后");

    }

}

执行顺序测试结果
  1. 无异常时

环绕通知前 → 前置通知 → 目标方法 → 环绕通知后 → 返回后通知 → 后置通知

日志输出:

环绕通知:目标方法执行前

前置通知:目标方法执行前

目标方法执行中...

环绕通知:目标方法执行后

返回后通知:目标方法正常执行后

后置通知:目标方法执行后(无论是否异常)

  1. 有异常时(目标方法抛NullPointerException):

环绕通知前 → 前置通知 → 目标方法异常 → 异常后通知 → 后置通知(环绕通知后不执行)

日志输出:

环绕通知:目标方法执行前

前置通知:目标方法执行前

目标方法执行中...(抛异常)

异常后通知:目标方法抛异常后

后置通知:目标方法执行后(无论是否异常)

3.3 切入点(PointCut):匹配目标方法的 “规则”

切入点是 “哪些方法需要被增强” 的规则,用切入点表达式描述,Spring 支持 2 种常用表达式:execution@annotation

四、AOP 进阶:从 “会用” 到 “用好” 的关键技巧

掌握基础后,这些进阶技巧能让你在实际项目中更灵活地使用 AOP。

4.1 @PointCut:抽取公共切入点,避免重复代码

如果多个通知用相同的切入点表达式(如execution(* com.example.service.*.*(..))),可通过@PointCut抽取,提高复用性。

用法步骤:
  1. 定义一个无逻辑的方法(如pt()),用@PointCut标注并指定表达式;
  2. 其他通知通过 “方法名 ()” 引用该切入点。

示例:

@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更灵活。

核心思路

  1. 自定义一个注解(如@OperateLog);
  2. 在需要增强的方法上标注该注解;
  3. 切入点表达式用@annotation(注解全类名)匹配。
实现步骤:
  1. 自定义注解

import java.lang.annotation.*;

// 注解作用在方法上

@Target(ElementType.METHOD)

// 注解保留到运行时(AOP需在运行时扫描)

@Retention(RetentionPolicy.RUNTIME)

public @interface OperateLog {

    // 可添加属性(如日志类型),默认值为"operate"

    String type() default "operate";

}

  1. 在目标方法上标注注解

@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();

    }

}

  1. 编写切面类,用 @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 切入点表达式书写建议(提高精准度,避免误匹配)

  1. 方法命名规范:增删改查方法统一前缀(如 saveXXX、deleteXXX、updateXXX、getXXX),方便execution匹配;
  2. 基于接口匹配:优先匹配接口方法(如execution(* com.example.service.UserService.*(..))),而非实现类,增强扩展性;
  3. 缩小匹配范围:包名尽量不用..(避免匹配过多子包),用*匹配单个包(如com.example.service.*.*(..));
  4. 优先用 @annotation:无规则方法用自定义注解匹配,灵活且易维护。

五、实战案例:AOP+ThreadLocal 实现操作日志持久化

学完理论,我们通过一个实战案例巩固 ——记录所有增删改接口的操作日志到数据库,这是企业项目的高频需求。

5.1 需求分析

需记录的日志信息:

  • 操作人 ID(当前登录用户);
  • 操作时间;
  • 执行类名、方法名;
  • 方法参数(如新增用户的姓名、年龄);
  • 方法返回值;
  • 方法执行耗时(ms)。

5.2 技术选型

  • 通知类型:@Around(需前后计时 + 获取返回值);
  • 切入点:@annotation(灵活匹配增删改方法);
  • 当前用户传递:ThreadLocal(同一请求线程内共享用户 ID,避免参数传递冗余);
  • 持久化:MyBatis(插入日志到数据库)。

5.3 完整实现步骤

步骤 1:准备数据库表和实体类
  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 '操作日志表';

  1. 实体类(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 大高频场景

  1. 日志记录:操作日志、访问日志、异常日志;
  2. 性能监控:方法耗时统计、接口 QPS 统计;
  3. 权限控制:接口访问权限校验(如判断用户是否有管理员权限);
  4. 事务控制:Spring 事务底层就是用 AOP 实现(@Transactional注解),自动开启 / 提交 / 回滚事务。

6.2 避坑要点(新手必看)

  1. @Around 必须调用 proceed ():否则目标方法不会执行,导致业务异常;
  2. @Around 返回值必须为 Object:否则调用方(如 Controller)拿不到返回值;
  3. ThreadLocal 用完必须 remove ():线程池复用会导致脏数据,需在 Filter 或 AOP finally 块中调用remove()
  4. 切入点表达式不要过度宽泛:如execution(* com.example..*(..))会匹配所有包的方法,影响性能;
  5. 通知顺序要明确:多切面时用@Order指定顺序,避免依赖默认字母顺序导致逻辑混乱。

6.3 性能影响说明

Spring AOP 基于动态代理(JDK 动态代理或 CGLIB),性能损耗极小(单方法增强耗时通常在 1ms 以内),完全满足企业级项目需求。除非是高频调用的核心方法(如每秒调用 10 万次),否则无需担心性能问题。

七、总结:掌握 AOP,让你的代码 “降维打击”

AOP 不是 “炫技工具”,而是解决 “共性逻辑与业务逻辑分离” 的最佳方案。掌握它,你能:

  • 写出更简洁的业务代码(无冗余日志、权限逻辑);
  • 大幅降低维护成本(改共性逻辑只需动切面);
  • 理解 Spring 事务、权限框架等底层原理(很多框架都基于 AOP)。

如果你觉得这篇文章对你有帮助,别忘了点赞 + 收藏

http://www.xdnf.cn/news/18654.html

相关文章:

  • KEPServerEX——工业数据采集与通信的标准化平台
  • 服务器(Linux)新账户搭建Pytorch深度学习环境
  • Devops之Jenkins:Jenkins服务器中的slave节点是什么?我们为什么要使用slave节点?如何添加一个windows slave节点?
  • 云计算之中间件与数据库
  • 机器学习:贝叶斯派
  • 2025年金九银十Java面试场景题大全:高频考点+深度解析+实战方案
  • 【C++详解】哈希表概念与实现 开放定址法和链地址法、处理哈希冲突、哈希函数介绍
  • Linux 进阶之性能调优,文件管理,网络安全
  • Java 22 新特性及具体应用
  • c++ 常用接口设计
  • CSS 进阶用法
  • Linux camera 驱动流程介绍(rgb: ov02k10)(chatgpt version)
  • Java 20 新特性及具体应用
  • 关于并查集
  • Text Blocks:告别字符串拼接地狱
  • 量子链(Qtum)分布式治理协议
  • 单词搜索+回溯法
  • Linux内核ELF文件签名验证机制的设计与实现(C/C++代码实现)
  • 源滚滚React消息通知框架v1.0.2使用教程
  • 《支付回调状态异常的溯源与架构级修复》
  • 【RAGFlow代码详解-3】核心服务
  • Linux驱动之DMA(三)
  • ubuntu中网卡的 IP 及网关配置设置为永久生效
  • Maxwell学习笔记
  • 8月精选!Windows 11 25H2 【版本号:26200.5733】
  • 从技术精英到“芯”途末路:一位工程师的沉沦与救赎
  • IC验证 APB 项目(二)——框架结构(总)
  • 项目编译 --- 基于cmake ninja编译 rtos项目
  • COSMIC智能化编写工具:革命性提升软件文档生成效率
  • 20.13 ChatGLM3 QLoRA微调实战:3步实现高效低资源训练