要用 AOP 做一个 HelloWorld 示例,关键在于把“横切关注点”从业务里抽离出来:引入 AOP 依赖、启用切面支持、写一个目标方法,再写切面类定义切点与通知(前置/后置/异常/环绕),运行观察通知何时织入,从而直观理解 AOP 的执行模型与代理机制。

一、先讲清楚:AOP 到底是什么,为什么要做 HelloWorld
想象你在写一个咖啡店程序:每次做咖啡前后都要记录日志、统计耗时、做权限校验。你可以在每个做咖啡的方法里插入这些代码,但那样会让业务逻辑变得杂乱无章。AOP(面向切面编程)就是为了解耦这些“横切关注点”,把日志、权限、事务等单独封装,按需织入到业务代码中。一个 HelloWorld 示例的目的,就是最低成本、最直观地演示切点(Pointcut)、通知(Advice)、切面(Aspect)和织入(Weaving)的关系。
二、核心概念快速过一遍(像给新手讲清楚)
什么是切面、切点、通知、连接点、织入?
- 切面(Aspect):把一类横切关注点(如日志)封装成的模块,类似“插件”。
- 连接点(Join point):程序执行过程中的一个可被插入的点,比如方法调用、方法执行等。
- 切点(Pointcut):用表达式匹配一组连接点,告诉框架“把切面应用到哪些方法上”。
- 通知(Advice):在匹配的连接点上实际执行的动作,如前置(before)、后置(after)、异常(after-throwing)、环绕(around)。
- 织入(Weaving):把切面与目标对象结合的过程,运行时或编译时完成,常见于 Spring AOP 的运行时代理织入。
类比一下(方便记忆)
把业务方法想象成一道主菜,日志、权限、事务就是配菜。AOP 就像厨房助手:按需在主菜上加配菜,但主厨(业务代码)本身不用关心配菜的具体如何制作。
三、用 Spring AOP 来做一个 HelloWorld(最常见也是最实用)
下面以 Spring Boot/ Spring Framework 注解方式示例,步骤清晰、能马上运行检验。
环境与依赖(Maven)
典型依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
spring-boot-starter-aop 会带入 Spring AOP 与 AspectJ 的必要依赖,适合快速试验。
第一步:写一个简单的目标类(业务类)
public class HelloService {
public String sayHello(String name) {
System.out.println("HelloService: executing sayHello");
return "Hello, " + name;
}
}
这是我们要“增强”的方法。
第二步:写一个切面类(Aspect)
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.HelloService.*(..))")
public void helloMethods() {}
@Before("helloMethods()")
public void beforeAdvice(JoinPoint jp) {
System.out.println("Before: entering " + jp.getSignature());
}
@AfterReturning(pointcut = "helloMethods()", returning = "ret")
public void afterReturningAdvice(JoinPoint jp, Object ret) {
System.out.println("AfterReturning: " + jp.getSignature() + " returned " + ret);
}
@AfterThrowing(pointcut = "helloMethods()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint jp, Throwable ex) {
System.out.println("AfterThrowing: " + ex.getMessage());
}
@Around("helloMethods()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Around: before proceed");
Object result = pjp.proceed();
System.out.println("Around: after proceed");
return result;
}
}
注解说明:@Aspect 标识切面,@Pointcut 定义匹配表达式,@Before/@AfterReturning/@AfterThrowing/@Around 是不同类型的通知。
第三步:在配置中启用 AOP
Spring Boot 一般自动配置了 AOP,但若使用 Spring Framework,需在配置类加上:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}
EnableAspectJAutoProxy 启用基于注解的切面支持,底层会创建代理来织入切面。
第四步:运行并观察(如何检验)
- 启动应用,调用 HelloService.sayHello(“World”)。
- 观察控制台输出,按顺序会看到 Before、Around 的 before、HelloService 输出、Around 的 after、AfterReturning 等。
- 如果方法抛出异常,会看到 AfterThrowing 被触发。
通过这个流程,你能直观理解“什么时候切面代码会被执行”。
四、通知类型与触发时机(用表格整理更清楚)
| 通知类型 | 触发时机 | 是否能控制方法执行 |
| 前置(Before) | 方法调用之前 | 否(只能执行额外逻辑) |
| 后置(AfterReturning) | 方法正常返回之后 | 否(可访问返回值) |
| 异常(AfterThrowing) | 方法抛异常时 | 否(可访问异常对象) |
| 最终(After / finally) | 方法执行结束(无论是否异常) | 否 |
| 环绕(Around) | 包裹方法执行,可在执行前后插入逻辑 | 是(可决定是否执行目标方法) |
五、代理实现与织入时机:理解 JDK 动态代理与 CGLIB
Spring AOP 默认对接口使用 JDK 动态代理,对类使用 CGLIB(如没有接口)。这会影响到几个实际问题:
- 若目标类只有 public 方法且是通过接口引用调用,Spring 会创建 JDK 代理,代理类型是接口,实现类方法的 self-invocation(即同类内部方法相互调用)不会被代理拦截。
- 若使用 CGLIB,则基于子类代理,private/ final 方法不能被代理(或纤细行为不同)。
- 你可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB。
常见坑:类内部方法互相调用时切面不生效(因为是直接调用而非通过代理)。解决方法:把要增强的方法放到另一个由 Spring 管理的 Bean 中,或使用 AspectJ 的编译时/类装载时织入。
六、深入:切点表达式怎么写(经常犯错的地方)
常用表达式示例:
- execution(* com.example..*(..)):匹配 com.example 包及子包下所有方法
- execution(public * *(..)):匹配所有 public 方法
- @annotation(org.springframework.transaction.annotation.Transactional):匹配标注了某注解的方法
- within(com.example.service..*):匹配指定类型或包内的方法
注意:execution 是最常用的,参数类型匹配和返回值匹配要写清楚,别忘记包名和通配符。参数绑定可以用 args(argName) 来获取方法参数。
参数绑定示例
@Before("execution(* com.example.HelloService.sayHello(..)) && args(name)")
public void logName(String name) {
System.out.println("Name arg: " + name);
}
这样在调用 sayHello(“Alice”) 时,切面方法就能拿到参数“Alice”。
七、常见实战用例与注意事项
- 日志和性能监控:使用环绕通知记录方法耗时;注意尽量避免在环绕中做阻塞操作。
- 权限校验:可在前置或环绕通知中判断当前用户权限,若无权限可抛出异常阻断执行。
- 事务管理:Spring 的事务本身基于 AOP,通常建议用 Spring 的事务注解而不是自己用切面实现事务。
- 异常处理:后置异常通知可做统一日志和统计,但不要吞掉异常(除非确实需要),以免掩盖问题。
- 切点粒度:切点过大会影响性能与可维护性,过小又容易漏掉场景,通常以包或注解为单位比较合理。
八、调试与测试技巧(快速定位问题)
- 在切面中打印 JoinPoint 的签名与目标类名,确认切点是否生效。
- 对环绕通知先只打印日志,确认顺序,再逐步加入功能,避免一次性改变太多逻辑。
- 注意单元测试中要把切面和被代理的 Bean 都纳入 Spring 上下文,或用 @SpringBootTest 进行集成测试。
- 如果内部调用不生效,先确认是否是 self-invocation 导致的(这是最常见的问题)。
九、进阶路线:Spring AOP vs AspectJ
简单区别:
- Spring AOP:基于代理、运行时织入,适合大多数业务场景,限制是只能对 Spring 管理的 Bean 生效,且织入粒度受代理机制限制。
- AspectJ:支持编译时、类加载时织入,功能更强,可以织入非 Spring 管理的类、更细粒度的连接点(如字段访问)。但配置和学习成本更高。
建议:先用 Spring AOP 做 HelloWorld 和常规增强;若需要更强的能力(比如监控非 Spring 类),再考虑 AspectJ。
十、性能和安全的工程考虑
- 切点匹配表达式会在创建代理时被解析,过于复杂的切点会影响启动时间,但运行时影响一般较小。
- 环绕通知相比前置/后置会增加额外开销,尽量把耗时逻辑放到后端异步处理或监控系统。
- 不要在切面内引入大量状态(如缓存大量对象),切面应保持轻量无副作用。
十一、常见问题速查(FAQ 风格)
Q:为什么我的切面没生效?
- Bean 可能不是由 Spring 管理(未被注入到容器)。
- 使用了 self-invocation,内部方法调用不会经过代理。
- 切点表达式写错或包名不匹配。
- 没有启用 @EnableAspectJAutoProxy(在非 Spring Boot 场景下)。
Q:如何测试环绕通知中 target 方法是否真的没执行?
在环绕中不要调用 pjp.proceed(),然后运行单元测试断言目标方法副作用不存在;此外打印日志顺序也能帮助验证。
十二、实践小贴士(那些工作中会用到的真实技巧)
- 把通用横切逻辑(如日志)放到基础模块,切点用注解限定(比如 @Monitor),便于管理。
- 对关键路径采用显式注解(而非包匹配),便于阅读与维护。
- 在开发环境给切面加上可配置的开关(配置中心或 profile),方便排查问题时临时关闭。
- 对于分布式系统,AOP 可用于链路追踪(注入 traceId),但要确保线程上下文(如 MDC)正确传递。
十三、参考书目与资料(便于深入研究)
- 《Spring 实战》(Craig Walls) —— 了解 Spring AOP 的基本用法与实践。
- 《AspectJ in Action》 —— 深入理解 AspectJ 的语法与高级织入。
- Spring 官方文档(AOP 和事务章节) —— 精确的配置与行为说明。
写到这里,实际上做一个 HelloWorld AOP 示例并不复杂,重点是把理论(切面、切点、通知、织入)和具体代码联系起来,通过逐步运行与验证感受执行顺序与代理行为——这是理解 AOP 最直接的方式。要是你想要,我可以把上面的示例代码整理成一个可运行的 Spring Boot 项目骨架,或者把切点表达式的更多例子列成清单,方便你快速拷贝使用。






