HelloWorld AOP 应用教程

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

HelloWorld 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 项目骨架,或者把切点表达式的更多例子列成清单,方便你快速拷贝使用。

返回首页