以下是 6个Spring AOP高频实战案例,覆盖日志、权限、事务、缓存等企业级开发场景,每个案例包含完整代码实现、核心逻辑解析和使用场景说明,可直接集成到Spring Boot项目中。
记录所有Controller层接口的请求参数、响应结果、执行耗时、IP地址等信息,便于问题排查和接口监控。
@Around):覆盖方法执行全生命周期JoinPoint:获取方法签名、参数等信息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;
}
}=== 接口请求开始 ===
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
=== 接口请求结束 ===通过自定义注解(如@NeedLogin、@NeedAdmin)控制接口访问权限,未登录用户或无权限用户直接拦截并返回异常。
@Before):方法执行前校验权限requireAdmin)import java.lang.annotation.*;
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可获取
@Documented
public @interface NeedLogin {
// 是否需要管理员权限(默认不需要)
boolean requireAdmin() default false;
}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);
}
}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 "订单删除成功";
}
}监控核心业务方法(如支付、下单)的执行耗时,当耗时超过阈值(如500ms)时触发告警,便于优化性能瓶颈。
@Around):统计执行时间import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MonitorTime {
// 耗时阈值(默认500ms)
long threshold() default 500;
// 告警文案
String alertMsg() default "方法执行耗时过长";
}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;
}
}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;
}
}捕获所有Controller层和Service层抛出的异常,统一返回标准化响应格式(如{"code":500,"msg":"系统异常","data":null}),避免直接返回堆栈信息给前端。
@RestControllerAdvice:Spring MVC提供的全局异常处理注解(本质是AOP)@ExceptionHandler:指定捕获的异常类型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}对高频查询方法(如根据ID查询用户、商品信息)添加缓存,减少数据库访问压力,提高接口响应速度。
@Around):控制缓存查询和写入时机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;
}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;
}
}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, 执行目标方法 → 查询数据库并写入缓存缓存命中 | key: user:getUserById:1 → 直接返回缓存数据,不查询数据库记录用户的关键操作(如创建订单、修改密码、删除数据),包含操作人、操作时间、操作内容、IP地址等信息,用于安全审计和行为追溯。
@AfterReturning):仅在方法成功执行后记录import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateAudit {
// 操作模块(如订单、用户、商品)
String module();
// 操作类型(如创建、修改、删除)
String type();
// 操作描述(如"创建订单")
String desc();
}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);
}
}
}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规则、日志字段、权限逻辑)。使用时需注意:
proceed(),且不吞噬异常;原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。