【SpringBoot】AOP 自定义注解实战:从日志记录到业务开关 📅 2026/7/15 12:15:21 1. 为什么需要AOP与自定义注解组合刚接触SpringBoot时我总在Controller里写满重复的日志代码。直到某次排查线上问题发现日志格式五花八门才意识到需要统一处理。这就是AOP自定义注解的典型场景——将横切关注点从业务代码中剥离。想象你家的电路系统AOP像是埋在墙里的电线自定义注解就是墙上的开关。当你想给房间加盏灯新功能只需装个新开关加注解不用重新布线修改原有代码。这种组合带来的三大优势降低耦合度日志、权限等非业务代码不再侵入Controller动态开关能力通过注解参数实现功能的热启停代码可读性注解声明即文档一看就懂方法特殊逻辑2. 基础环境搭建2.1 必备依赖配置在pom.xml中只需添加一个starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency实测发现SpringBoot 2.3版本会自动引入AspectJ相关依赖。建议配合Lombok使用非必须dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency2.2 核心注解速览这几个注解构成了我们的武器库注解作用域生命周期说明Target注解定义-指定注解能用在哪里Retention注解定义-决定注解保留到哪个阶段Aspect类运行时声明这是个切面类Pointcut方法运行时定义拦截规则3. 日志记录实战3.1 定义日志注解先来个简单版日志注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface MethodLog { String module() default ; boolean printArgs() default true; }这个注解允许我们标记需要日志的方法指定所属业务模块控制是否打印入参3.2 切面实现细节完整切面代码如下Aspect Component Slf4j public class LogAspect { Pointcut(annotation(com.example.demo.annotation.MethodLog)) public void logPointcut() {} Around(logPointcut()) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature (MethodSignature) joinPoint.getSignature(); MethodLog annotation signature.getMethod().getAnnotation(MethodLog.class); long start System.currentTimeMillis(); String methodName signature.getDeclaringTypeName() . signature.getName(); try { // 打印入参 if (annotation.printArgs()) { log.info([{}] {} 入参: {}, annotation.module(), methodName, Arrays.toString(joinPoint.getArgs())); } Object result joinPoint.proceed(); // 打印出参及耗时 log.info([{}] {} 耗时: {}ms 结果: {}, annotation.module(), methodName, System.currentTimeMillis() - start, JsonUtils.toJson(result)); return result; } catch (Exception e) { log.error([{}] {} 异常: {}, annotation.module(), methodName, e.getMessage()); throw e; } } }踩坑提醒获取方法注解时要用signature.getMethod()而非joinPoint.getTarget().getClass()使用Around时必须调用proceed()否则方法不会执行复杂对象建议用JSON序列化打印避免toString()问题3.3 使用示例在Controller方法上添加注解RestController RequestMapping(/order) public class OrderController { PostMapping(/create) MethodLog(module 订单中心, printArgs true) public Order createOrder(RequestBody OrderCreateDTO dto) { // 业务逻辑 } }执行后会输出类似日志[订单中心] com.example.OrderController.createOrder 入参: [OrderCreateDTO(...)] [订单中心] com.example.OrderController.createOrder 耗时: 45ms 结果: {orderId:123,...}4. 业务开关进阶4.1 设计开关注解更实用的场景是业务开关比如双十一的限流Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface FeatureToggle { String featureKey(); String errorMessage() default 功能暂不可用; boolean enableLog() default true; }4.2 实现动态开关结合配置中心实现动态生效Aspect Component RequiredArgsConstructor public class FeatureAspect { private final ConfigService configService; Around(annotation(toggle)) public Object checkFeature(ProceedingJoinPoint pjp, FeatureToggle toggle) throws Throwable { boolean isActive configService.getBool(toggle.featureKey()); if (!isActive) { if (toggle.enableLog()) { log.warn(功能关闭被拦截: {}, toggle.featureKey()); } throw new BizException(toggle.errorMessage()); } return pjp.proceed(); } }4.3 实战应用在支付接口添加熔断开关PostMapping(/pay) FeatureToggle(featureKey payment.enabled, errorMessage 支付系统维护中) public PayResult pay(RequestBody PayRequest request) { // 支付逻辑 }当配置中心将payment.enabled设为false时所有支付请求会立即返回错误信息避免雪崩效应。5. 性能监控方案5.1 定义监控注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface TimeMonitor { TimeUnit unit() default TimeUnit.MILLISECONDS; int threshold() default 100; }5.2 监控切面实现Aspect Component Slf4j public class MonitorAspect { Around(annotation(monitor)) public Object monitorMethod(ProceedingJoinPoint pjp, TimeMonitor monitor) throws Throwable { long start System.nanoTime(); try { return pjp.proceed(); } finally { long cost monitor.unit().convert( System.nanoTime() - start, TimeUnit.NANOSECONDS); if (cost monitor.threshold()) { log.warn(方法执行超时: {}.{} 耗时: {}{}, pjp.getSignature().getDeclaringTypeName(), pjp.getSignature().getName(), cost, monitor.unit().name().toLowerCase()); } } } }5.3 对接告警系统可以扩展切面将超时信息发送到监控平台if (cost monitor.threshold()) { alertService.send(new AlertMsg() .setMethod(pjp.getSignature().toShortString()) .setCostTime(cost) .setThreshold(monitor.threshold())); }6. 最佳实践与避坑指南执行顺序问题多个切面作用于同一方法时用Order控制顺序Aspect Order(1) // 数字越小优先级越高 public class LogAspect {...}内部调用失效同类中A调用B时B的注解会失效。解决方案// 正确做法 Autowired private SelfProxy selfProxy; public void A() { selfProxy.B(); // 通过代理对象调用 }性能优化建议切点表达式尽量精确避免用execution(* com..*(..))这种宽泛匹配频繁执行的代码中避免在切面里做IO操作使用缓存减少注解元数据解析开销调试技巧在application.properties中添加logging.level.org.springframework.aopDEBUG可以看到代理创建过程和拦截器链7. 扩展应用场景7.1 权限控制Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Permission { String[] roles(); } // 切面实现 Around(annotation(permission)) public Object checkPermission(ProceedingJoinPoint pjp, Permission permission) { User user SecurityContext.getCurrentUser(); if (!ArrayUtils.contains(permission.roles(), user.getRole())) { throw new ForbiddenException(无权限访问); } return pjp.proceed(); }7.2 数据脱敏Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface SensitiveData { MaskType type(); } // 使用示例 GetMapping(/userInfo) SensitiveData(type MaskType.PHONE) public UserInfo getUser() {...}7.3 重试机制Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RetryOnFailure { int attempts() default 3; Class? extends Throwable[] retryFor(); } // 切面实现 Around(annotation(retry)) public Object retry(ProceedingJoinPoint pjp, RetryOnFailure retry) throws Throwable { int attempts 0; do { try { return pjp.proceed(); } catch (Exception e) { if (!ArrayUtils.contains(retry.retryFor(), e.getClass())) { throw e; } if (attempts retry.attempts()) { throw new RetryFailedException(重试次数耗尽); } Thread.sleep(100 * attempts); } } while (true); }这些实战案例都是我在电商系统中真实应用过的方案。记住一个原则当发现自己在重复编写相似的辅助代码时就是考虑AOP注解的好时机。