
一、引言在 SpringBoot 项目里我们经常会遇到一些横切在多个方法之间的通用逻辑比如记录操作日志、统计方法耗时、接口鉴权、事务控制等等。如果把这些逻辑硬编码进每个业务方法里代码会变得臃肿且难以维护——每个方法开头都来一段long start System.currentTimeMillis();每个方法里都try-catch记录异常这显然不优雅。Spring AOPAspect-Oriented Programming面向切面编程 就是为解决这类问题而生的。它的核心思想是把这些散落在各处的横切关注点从业务代码中抽离出来集中到一个地方统一处理让业务代码只关心业务本身。这篇文章我会带你从 AOP 的基本概念讲起覆盖 5 种通知类型、切入点表达式写法最后用自定义注解 AOP 实现日志记录 / 方法耗时统计两个实战案例收尾代码全部可运行。二、什么是 AOPAOP 是对 OOP面向对象编程的一种补充。OOP 擅长纵向的组织——把属性和方法封装成类但像日志、权限、事务这类横切多个模块的逻辑OOP 很难优雅地抽象只能靠复制粘贴或继承最终造成代码散布scattering和代码纠缠tangling。AOP 的做法是不修改原业务方法在方法执行前后织入额外的逻辑。底层原理主要有两种JDK 动态代理基于接口被代理对象必须实现接口。CGLIB 动态代理基于继承通过生成目标类的子类来实现代理。SpringBoot 2.x 默认spring.aop.proxy-target-classtrue即默认走 CGLIB无需接口也能代理。三、AOP 核心概念理解这几个术语是看懂 AOP 代码的前提术语说明切面Aspect切面 切入点 通知是横切逻辑的载体用Aspect标注连接点JoinPoint程序执行过程中能被拦截的点通常是方法调用切入点Pointcut一组连接点的集合通过切入点表达式匹配通知Advice在切入点何时、做什么的逻辑如Before、Around织入Weaving把通知应用到目标对象、创建代理的过程一句话串起来在满足切入点表达式的方法连接点上执行切面里定义的通知逻辑。四、切入点表达式切入点表达式Pointcut Expression用来圈定哪些方法需要被拦截。语法为plaintextexecution(修饰符? 返回类型 包名.类名.方法名(参数列表) 异常?)几个常用例子java// 匹配 com.demo.service 包下所有类的所有方法 execution(* com.demo.service.*.*(..)) // 匹配 service 包及其子包下所有方法 execution(* com.demo.service..*.*(..)) // 匹配所有 save 开头的方法 execution(* com.demo.service.*.save*(..)) // 匹配返回 String、第一个参数为 Long 的方法 execution(String com.demo.service.*.*(Long, ..))符号含义*匹配任意一个字符/一段用在返回类型、方法名、单个参数上..匹配任意多个字符/任意参数用在包名、参数列表上也可以直接用注解做切入点非常常用java// 匹配所有被 GetMapping 标注的方法 annotation(org.springframework.web.bind.annotation.GetMapping) // 匹配所有被自定义注解 Log 标注的方法 annotation(com.demo.annotation.Log)五、五种通知类型先引入依赖SpringBoot 自带 AOP只需加 starterxmldependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency下面用一个切面类把 5 种通知都演示一遍javaimport org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; Aspect // 标记为切面 Component // 交给 Spring 管理 public class DemoAspect { // 1. 前置通知目标方法执行前 Before(execution(* com.demo.service.*.*(..))) public void before(JoinPoint joinPoint) { System.out.println(【前置】方法即将执行 joinPoint.getSignature().getName()); } // 2. 后置通知目标方法执行后无论是否异常都会执行 After(execution(* com.demo.service.*.*(..))) public void after(JoinPoint joinPoint) { System.out.println(【后置】方法执行结束 joinPoint.getSignature().getName()); } // 3. 返回通知目标方法正常返回后执行可拿到返回值 AfterReturning(pointcut execution(* com.demo.service.*.*(..)), returning result) public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println(【返回】返回值 result); } // 4. 异常通知目标方法抛异常后执行可拿到异常 AfterThrowing(pointcut execution(* com.demo.service.*.*(..)), throwing ex) public void afterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println(【异常】 joinPoint.getSignature().getName() 抛出了 ex.getMessage()); } // 5. 环绕通知功能最强手动控制方法执行 Around(execution(* com.demo.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println(【环绕-前】); Object result pjp.proceed(); // 手动调用目标方法 System.out.println(【环绕-后】); return result; } }注意环绕通知里pjp.proceed()必须手动调用目标方法否则方法根本不会执行Around和Before等同时存在时Around优先级最高会包住其他通知。六、实战一自定义注解 AOP 实现日志记录单独用execution表达式有时不够灵活——我们可能只想给部分方法加日志。这时用自定义注解最方便想给哪个方法记录日志就在方法上打一个Log注解。Step 1定义注解javaimport java.lang.annotation.*; Target(ElementType.METHOD) // 作用在方法上 Retention(RetentionPolicy.RUNTIME) // 运行时保留AOP 才能读到 public interface Log { String value() default ; // 日志描述 }Step 2编写切面javaimport org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.util.Arrays; Aspect Component public class LogAspect { // 拦截所有带 Log 注解的方法 Around(annotation(log)) public Object around(ProceedingJoinPoint pjp, Log log) throws Throwable { MethodSignature signature (MethodSignature) pjp.getSignature(); String methodName signature.getMethod().getName(); Object[] args pjp.getArgs(); System.out.println(【日志】开始执行 methodName 参数 Arrays.toString(args) 描述 log.value()); Object result pjp.proceed(); // 执行目标方法 System.out.println(【日志】执行完成 methodName 返回值 result); return result; } }Step 3在业务方法上使用javaimport org.springframework.stereotype.Service; Service public class OrderService { Log(创建订单) public String createOrder(String productId, int count) { // 业务逻辑... return 订单创建成功 productId x count; } // 不打注解则不记录日志 public String queryOrder(String id) { return 订单 id; } }这样createOrder会有日志queryOrder没有粒度完全可控。七、实战二方法耗时统计统计方法执行时长也是 AOP 的经典场景。这里用一个注解Timed来标记需要计时的方法javaimport java.lang.annotation.*; Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Timed { }切面代码javaimport org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; Aspect Component public class TimedAspect { Around(annotation(com.demo.annotation.Timed)) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { long cost System.currentTimeMillis() - start; MethodSignature signature (MethodSignature) pjp.getSignature(); System.out.println(【耗时】 signature.getDeclaringType().getSimpleName() . signature.getMethod().getName() 执行耗时 cost ms); } } }使用示例javaService public class ReportService { Timed public void generateReport() { try { Thread.sleep(1500); // 模拟耗时业务 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }运行后控制台会输出类似【耗时】ReportService.generateReport 执行耗时1501 ms。八、总结这篇文章我们从 AOP 的概念入手梳理了它的核心术语然后讲了切入点表达式的写法、5 种通知类型的用法最后用两个实战案例落地了 AOP 最典型的应用——日志记录和方法耗时统计。几个关键点再帮你划一下重点AOP 本质是代理SpringBoot 默认 CGLIB无需接口也能代理。通知顺序Around→Before→ 目标方法 →AfterReturning/AfterThrowing→AfterAround始终在最外层。Around必须调用proceed()否则目标方法不会执行。自定义注解 AOP是最灵活、最推荐的使用方式能把横切逻辑的粒度精确控制到单个方法。