首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一些实际的AOP实战案例

一些实际的AOP实战案例

原创
作者头像
搜罗万相
发布2025-11-03 17:14:56
发布2025-11-03 17:14:56
6400
举报
文章被收录于专栏:Java开发Java开发

以下是 6个Spring AOP高频实战案例,覆盖日志、权限、事务、缓存等企业级开发场景,每个案例包含完整代码实现、核心逻辑解析和使用场景说明,可直接集成到Spring Boot项目中。

案例1:接口统一日志记录(最常用)

场景说明

记录所有Controller层接口的请求参数、响应结果、执行耗时、IP地址等信息,便于问题排查和接口监控。

技术要点

  • 环绕通知(@Around):覆盖方法执行全生命周期
  • JoinPoint:获取方法签名、参数等信息
  • 排除静态资源接口,避免日志冗余

完整实现

代码语言:java
复制
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Arrays;

@Aspect
@Component
@Slf4j
public class ApiLogAspect {

    // 切入点:匹配所有Controller层接口(排除静态资源)
    @Pointcut("execution(public * com.example.demo.controller..*.*(..)) && !execution(public * com.example.demo.controller.*.*(String))")
    public void apiLogPointcut() {}

    @Around("apiLogPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取请求上下文信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteAddr();
        String url = request.getRequestURL().toString();
        String method = request.getMethod();

        // 2. 记录请求信息
        String className = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("=== 接口请求开始 ===");
        log.info("IP: {}, URL: {}, 请求方式: {}", ip, url, method);
        log.info("类名: {}, 方法名: {}, 请求参数: {}", className, methodName, JSONUtil.toJsonStr(args));

        // 3. 记录执行时间
        long startTime = System.currentTimeMillis();
        Object result;
        try {
            // 执行目标方法
            result = joinPoint.proceed();
        } catch (Exception e) {
            // 4. 记录异常信息
            log.error("接口执行异常 | 类名: {} | 方法名: {} | 异常信息: {}", className, methodName, e.getMessage(), e);
            throw e; // 重新抛出异常,不影响全局异常处理
        }
        long costTime = System.currentTimeMillis() - startTime;

        // 5. 记录响应结果
        log.info("接口执行成功 | 响应结果: {} | 耗时: {}ms", JSONUtil.toJsonStr(result), costTime);
        log.info("=== 接口请求结束 ===\n");

        return result;
    }
}

效果演示

代码语言:txt
复制
=== 接口请求开始 ===
IP: 127.0.0.1, URL: http://localhost:8080/user/1, 请求方式: GET
类名: com.example.demo.controller.UserController, 方法名: getUserById, 请求参数: [1]
接口执行成功 | 响应结果: {"id":1,"name":"张三","age":25} | 耗时: 15ms
=== 接口请求结束 ===

案例2:基于自定义注解的权限校验

场景说明

通过自定义注解(如@NeedLogin@NeedAdmin)控制接口访问权限,未登录用户或无权限用户直接拦截并返回异常。

技术要点

  • 前置通知(@Before):方法执行前校验权限
  • 自定义注解 + 注解属性(如requireAdmin
  • ThreadLocal存储当前登录用户信息

完整实现

步骤1:创建自定义注解
代码语言:java
复制
import java.lang.annotation.*;

@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可获取
@Documented
public @interface NeedLogin {
    // 是否需要管理员权限(默认不需要)
    boolean requireAdmin() default false;
}
步骤2:实现权限切面
代码语言:java
复制
import com.example.demo.annotation.NeedLogin;
import com.example.demo.exception.UnauthorizedException;
import com.example.demo.util.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(1) // 优先级高于日志切面,先校验权限再记录日志
public class AuthAspect {

    // 切入点:匹配所有标注@NeedLogin的方法
    @Pointcut("@annotation(com.example.demo.annotation.NeedLogin)")
    public void authPointcut() {}

    @Before("authPointcut() && @annotation(needLogin)")
    public void doBefore(NeedLogin needLogin) {
        // 1. 校验用户是否登录(从ThreadLocal获取当前用户)
        Long userId = UserContext.getCurrentUserId();
        if (userId == null) {
            log.warn("未登录用户尝试访问受保护接口");
            throw new UnauthorizedException("请先登录");
        }

        // 2. 若需要管理员权限,进一步校验
        if (needLogin.requireAdmin()) {
            boolean isAdmin = UserContext.isAdmin();
            if (!isAdmin) {
                log.warn("非管理员用户[{}]尝试访问管理员接口", userId);
                throw new UnauthorizedException("无管理员权限,禁止访问");
            }
        }

        log.info("用户[{}]权限校验通过,允许访问接口", userId);
    }
}
步骤3:使用注解控制接口权限
代码语言:java
复制
import com.example.demo.annotation.NeedLogin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    // 普通接口:仅需登录
    @NeedLogin
    @GetMapping("/order/create")
    public String createOrder() {
        return "订单创建成功";
    }

    // 管理员接口:需登录且为管理员
    @NeedLogin(requireAdmin = true)
    @GetMapping("/order/delete/{id}")
    public String deleteOrder(Long id) {
        return "订单删除成功";
    }
}

案例3:方法执行耗时监控(性能告警)

场景说明

监控核心业务方法(如支付、下单)的执行耗时,当耗时超过阈值(如500ms)时触发告警,便于优化性能瓶颈。

技术要点

  • 环绕通知(@Around):统计执行时间
  • 自定义阈值参数(通过注解属性配置)
  • 告警机制(日志打印/邮件通知/钉钉机器人)

完整实现

步骤1:创建耗时监控注解
代码语言:java
复制
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MonitorTime {
    // 耗时阈值(默认500ms)
    long threshold() default 500;
    // 告警文案
    String alertMsg() default "方法执行耗时过长";
}
步骤2:实现耗时监控切面
代码语言:java
复制
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;

@Aspect
@Component
@Slf4j
public class TimeMonitorAspect {

    @Pointcut("@annotation(com.example.demo.annotation.MonitorTime)")
    public void timeMonitorPointcut() {}

    @Around("timeMonitorPointcut() && @annotation(monitorTime)")
    public Object doAround(ProceedingJoinPoint joinPoint, MonitorTime monitorTime) throws Throwable {
        long threshold = monitorTime.threshold();
        String alertMsg = monitorTime.alertMsg();

        // 记录开始时间
        long startTime = System.currentTimeMillis();
        Object result;
        try {
            result = joinPoint.proceed();
        } finally {
            // 计算耗时
            long costTime = System.currentTimeMillis() - startTime;
            String methodInfo = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();

            // 耗时超过阈值,触发告警
            if (costTime > threshold) {
                log.error("[性能告警] {} | 方法: {} | 实际耗时: {}ms | 阈值: {}ms",
                        alertMsg, methodInfo, costTime, threshold);
                // 此处可扩展:调用钉钉机器人/邮件服务发送告警
            } else {
                log.info("方法[{}]执行耗时: {}ms(未超过阈值{}ms)", methodInfo, costTime, threshold);
            }
        }

        return result;
    }
}
步骤3:在核心方法上使用注解
代码语言:java
复制
import com.example.demo.annotation.MonitorTime;
import org.springframework.stereotype.Service;

@Service
public class PayService {

    // 监控支付方法,阈值800ms
    @MonitorTime(threshold = 800, alertMsg = "支付接口响应缓慢")
    public boolean pay(Long orderId, BigDecimal amount) {
        // 模拟支付逻辑(可能耗时)
        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return true;
    }
}

案例4:全局异常统一处理

场景说明

捕获所有Controller层和Service层抛出的异常,统一返回标准化响应格式(如{"code":500,"msg":"系统异常","data":null}),避免直接返回堆栈信息给前端。

技术要点

  • @RestControllerAdvice:Spring MVC提供的全局异常处理注解(本质是AOP)
  • @ExceptionHandler:指定捕获的异常类型
  • 分类处理不同异常(业务异常、参数异常、系统异常)

完整实现

代码语言:java
复制
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import com.example.demo.exception.UnauthorizedException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice(basePackages = "com.example.demo") // 仅处理指定包下的异常
@Slf4j
public class GlobalExceptionAspect {

    // 处理业务异常(自定义异常)
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        log.error("业务异常: {}", e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    // 处理未授权异常
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED) // 返回401状态码
    public Result handleUnauthorizedException(UnauthorizedException e) {
        log.error("未授权异常: {}", e.getMessage());
        return Result.fail(401, e.getMessage());
    }

    // 处理参数校验异常(如@Valid注解)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 返回400状态码
    public Result handleParamValidException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder errorMsg = new StringBuilder("参数校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = errorMsg.substring(0, errorMsg.length() - 2);
        log.error(msg);
        return Result.fail(400, msg);
    }

    // 处理系统异常(兜底)
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回500状态码
    public Result handleSystemException(Exception e) {
        log.error("系统异常:", e); // 打印完整堆栈信息
        return Result.fail(500, "系统繁忙,请稍后再试");
    }
}

效果演示

  • 业务异常返回:{"code":1001,"msg":"订单不存在","data":null}
  • 参数异常返回:{"code":400,"msg":"参数校验失败:name:用户名不能为空, age:年龄必须大于0","data":null}
  • 系统异常返回:{"code":500,"msg":"系统繁忙,请稍后再试","data":null}

案例5:Redis缓存增强(方法结果缓存)

场景说明

对高频查询方法(如根据ID查询用户、商品信息)添加缓存,减少数据库访问压力,提高接口响应速度。

技术要点

  • 环绕通知(@Around):控制缓存查询和写入时机
  • RedisTemplate:操作Redis缓存
  • 自定义注解指定缓存key和过期时间

完整实现

步骤1:创建缓存注解
代码语言:java
复制
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisCache {
    // 缓存key前缀(默认使用方法全类名)
    String prefix() default "";
    // 过期时间(默认30分钟)
    long expire() default 30;
    // 时间单位(默认分钟)
    TimeUnit timeUnit() default TimeUnit.MINUTES;
}
步骤2:实现缓存切面
代码语言:java
复制
import cn.hutool.core.util.StrUtil;
import com.example.demo.annotation.RedisCache;
import lombok.extern.slf4j.Slf4j;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class RedisCacheAspect {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(com.example.demo.annotation.RedisCache)")
    public void redisCachePointcut() {}

    @Around("redisCachePointcut() && @annotation(redisCache)")
    public Object doAround(ProceedingJoinPoint joinPoint, RedisCache redisCache) throws Throwable {
        // 1. 构建缓存key(前缀 + 方法名 + 参数)
        String prefix = StrUtil.isBlank(redisCache.prefix()) 
                ? joinPoint.getSignature().getDeclaringTypeName() 
                : redisCache.prefix();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String key = prefix + ":" + methodName + ":" + StrUtil.join("-", args);

        // 2. 先查询缓存
        Object cacheValue = redisTemplate.opsForValue().get(key);
        if (cacheValue != null) {
            log.info("缓存命中 | key: {}", key);
            return cacheValue;
        }

        // 3. 缓存未命中,执行目标方法
        log.info("缓存未命中 | key: {}, 执行目标方法", key);
        Object result = joinPoint.proceed();

        // 4. 将结果写入缓存
        long expire = redisCache.expire();
        TimeUnit timeUnit = redisCache.timeUnit();
        redisTemplate.opsForValue().set(key, result, expire, timeUnit);
        log.info("缓存写入成功 | key: {}, 过期时间: {} {}", key, expire, timeUnit);

        return result;
    }
}
步骤3:在查询方法上使用注解
代码语言:java
复制
import com.example.demo.annotation.RedisCache;
import com.example.demo.entity.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 缓存用户查询结果,过期时间1小时
    @RedisCache(prefix = "user", expire = 1, timeUnit = TimeUnit.HOURS)
    public User getUserById(Long id) {
        // 模拟数据库查询(实际开发中是MyBatis查询)
        log.info("查询数据库 | 用户ID: {}", id);
        User user = new User();
        user.setId(id);
        user.setName("张三");
        user.setAge(25);
        return user;
    }
}

效果演示

  • 第一次调用:缓存未命中 | key: user:getUserById:1, 执行目标方法 → 查询数据库并写入缓存
  • 1小时内再次调用:缓存命中 | key: user:getUserById:1 → 直接返回缓存数据,不查询数据库

案例6:操作审计日志(记录关键行为)

场景说明

记录用户的关键操作(如创建订单、修改密码、删除数据),包含操作人、操作时间、操作内容、IP地址等信息,用于安全审计和行为追溯。

技术要点

  • 后置返回通知(@AfterReturning):仅在方法成功执行后记录
  • 自定义注解存储操作描述
  • 异步记录日志(避免影响主流程性能)

完整实现

步骤1:创建审计注解
代码语言:java
复制
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateAudit {
    // 操作模块(如订单、用户、商品)
    String module();
    // 操作类型(如创建、修改、删除)
    String type();
    // 操作描述(如"创建订单")
    String desc();
}
步骤2:实现审计切面(异步)
代码语言:java
复制
import com.example.demo.annotation.OperateAudit;
import com.example.demo.entity.OperateLog;
import com.example.demo.service.OperateLogService;
import com.example.demo.util.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import java.time.LocalDateTime;

@Aspect
@Component
@Slf4j
public class OperateAuditAspect {

    @Resource
    private OperateLogService operateLogService; // 日志存储服务(需自行实现)

    @Pointcut("@annotation(com.example.demo.annotation.OperateAudit)")
    public void operateAuditPointcut() {}

    // 异步记录日志(使用@Async注解,需开启@EnableAsync)
    @AfterReturning(value = "operateAuditPointcut() && @annotation(operateAudit)", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, OperateAudit operateAudit, Object result) {
        try {
            // 1. 获取操作人信息
            Long userId = UserContext.getCurrentUserId();
            String username = UserContext.getCurrentUsername();

            // 2. 获取请求信息
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            String ip = request.getRemoteAddr();
            String url = request.getRequestURL().toString();

            // 3. 构建审计日志对象
            OperateLog log = new OperateLog();
            log.setUserId(userId);
            log.setUsername(username);
            log.setModule(operateAudit.module());
            log.setType(operateAudit.type());
            log.setDesc(operateAudit.desc());
            log.setIp(ip);
            log.setUrl(url);
            log.setMethod(joinPoint.getSignature().getName());
            log.setParams(joinPoint.getArgs().toString());
            log.setResult(result.toString());
            log.setOperateTime(LocalDateTime.now());

            // 4. 存储日志(异步执行,不阻塞主流程)
            operateLogService.save(log);
            log.info("审计日志记录成功 | 操作人: {} | 模块: {} | 操作: {}", username, operateAudit.module(), operateAudit.type());
        } catch (Exception e) {
            log.error("审计日志记录失败", e);
        }
    }
}
步骤3:在关键操作方法上使用注解
代码语言:java
复制
import com.example.demo.annotation.OperateAudit;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @OperateAudit(module = "订单", type = "创建", desc = "用户创建订单")
    public Long createOrder(OrderCreateDTO dto) {
        // 创建订单逻辑
        Long orderId = 10001L;
        return orderId;
    }

    @OperateAudit(module = "订单", type = "取消", desc = "管理员取消订单")
    public boolean cancelOrder(Long orderId) {
        // 取消订单逻辑
        return true;
    }
}

案例总结与扩展

案例

核心通知类型

适用场景

扩展方向

接口日志

@Around

所有接口监控、问题排查

集成ELK栈实现日志可视化

权限校验

@Before

接口访问控制、权限分级

结合Spring Security实现更复杂权限

耗时监控

@Around

性能优化、瓶颈识别

接入Prometheus+Grafana监控告警

全局异常

@RestControllerAdvice

统一响应格式、前端友好

增加异常统计和告警机制

Redis缓存

@Around

高频查询、减轻DB压力

实现缓存更新/删除、缓存穿透防护

操作审计

@AfterReturning

安全审计、行为追溯

日志加密存储、定期归档

这些案例覆盖了AOP在企业级开发中的核心应用,可根据实际业务需求调整细节(如缓存key规则、日志字段、权限逻辑)。使用时需注意:

  1. 切入点表达式尽量精准,避免大范围匹配导致性能损耗;
  2. 环绕通知必须调用proceed(),且不吞噬异常;
  3. 耗时操作(如日志存储、缓存写入)建议异步执行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 案例1:接口统一日志记录(最常用)
    • 场景说明
    • 技术要点
    • 完整实现
    • 效果演示
  • 案例2:基于自定义注解的权限校验
    • 场景说明
    • 技术要点
    • 完整实现
      • 步骤1:创建自定义注解
      • 步骤2:实现权限切面
      • 步骤3:使用注解控制接口权限
  • 案例3:方法执行耗时监控(性能告警)
    • 场景说明
    • 技术要点
    • 完整实现
      • 步骤1:创建耗时监控注解
      • 步骤2:实现耗时监控切面
      • 步骤3:在核心方法上使用注解
  • 案例4:全局异常统一处理
    • 场景说明
    • 技术要点
    • 完整实现
    • 效果演示
  • 案例5:Redis缓存增强(方法结果缓存)
    • 场景说明
    • 技术要点
    • 完整实现
      • 步骤1:创建缓存注解
      • 步骤2:实现缓存切面
      • 步骤3:在查询方法上使用注解
    • 效果演示
  • 案例6:操作审计日志(记录关键行为)
    • 场景说明
    • 技术要点
    • 完整实现
      • 步骤1:创建审计注解
      • 步骤2:实现审计切面(异步)
      • 步骤3:在关键操作方法上使用注解
  • 案例总结与扩展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档