跳到主要内容 Java AOP 技术详解 | 极客日志
Java java
Java AOP 技术详解 Java AOP 面向切面编程技术详解。涵盖 AOP 基本概念、与 OOP 关系、核心组件术语如切面连接点通知切入点等。介绍 AspectJ 与 Spring AOP 实现方式及原理,重点解析 Spring AOP 的代理机制(JDK/CGLIB)、配置方式、切面表达式语法及五种通知类型。提供日志、事务、安全、缓存等实际应用场景代码示例。总结最佳实践与注意事项,包括单一职责、性能优化、内部调用陷阱及异常处理,帮助开发者有效分离横切关注点,提升系统模块化与维护性。
字节跳动 发布于 2026/2/6 更新于 2026/4/18 1 浏览Java AOP 技术详解
1. AOP 基本概念与原理
1.1 什么是 AOP
AOP (Aspect-Oriented Programming) 面向切面编程,是一种编程范式,旨在通过横切关注点的分离,提高代码的模块化程度。它允许开发者将横切关注点(如日志记录、事务管理、性能监控、安全控制等)与业务逻辑分离,使系统更加松耦合、可维护性更高。
AOP 的核心理念是:将那些与业务无关,但为业务模块所共同调用的逻辑或责任,封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性 。
1.2 AOP 与 OOP 的关系
1.2.1 面向对象编程(OOP)的局限性
面向对象编程(OOP)通过封装、继承和多态三大特性,帮助我们构建模块化、可复用的代码。然而,在实际开发中,OOP 仍然面临一些挑战:
横切关注点的分散 :像日志、事务、安全等横切关注点会分散到多个类中,导致代码重复和难以维护
关注点耦合 :业务逻辑与横切关注点混合在一起,使核心业务逻辑不够纯粹
修改横切关注点困难 :当需要修改横切逻辑时,需要修改多个类,增加了维护成本和引入错误的风险
1.2.2 AOP 与 OOP 的互补关系 AOP 不是 OOP 的替代品,而是对 OOP 的补充和完善:
OOP 关注的是纵向 的继承体系和对象结构
AOP 关注的是横向 的横切关注点处理
两者结合使用,可以构建更加清晰、模块化的软件系统
AOP 允许我们在不修改原有代码的情况下,通过声明式方式为代码添加新的功能,实现了功能的横向扩展。
1.3 AOP 的历史发展 AOP 的概念最早由 Xerox PARC 的研究人员在 1990 年代提出,作为 AspectJ 项目的一部分。随着时间的推移,AOP 逐渐成为企业应用开发中的重要技术:
1997 年:Gregor Kiczales 等人正式提出 AOP 概念
2001 年:AspectJ 项目发布,成为 Java 平台上最成熟的 AOP 实现
2004 年:Spring 框架引入 AOP 支持,使其成为企业级 Java 开发的标准实践
近年来:随着微服务架构的兴起,AOP 在分布式系统监控、跟踪等方面发挥着越来越重要的作用
1.4 AOP 的工作原理
1.4.1 代理模式 代理模式是 AOP 实现的基础,通过创建目标对象的代理,在不修改目标对象代码的情况下,拦截其方法调用并添加额外功能。
1.4.2 织入(Weaving) 织入是将切面应用到目标对象并创建新的代理对象的过程。根据织入时机的不同,可以分为:
编译时织入 :如 AspectJ,在编译阶段将切面代码织入到目标类中
类加载时织入 :在 JVM 加载类时,通过字节码增强技术织入切面
运行时织入 :如 Spring AOP,在运行时通过动态代理技术(JDK 动态代理或 CGLIB)创建代理对象
1.4.3 字节码增强 AOP 实现通常需要对字节码进行操作,主要技术包括:
动态代理 :JDK 动态代理(基于接口)和 CGLIB(基于类继承)
字节码操作库 :如 ASM、Javassist、ByteBuddy 等,可以直接操作 Java 字节码
1.5 AOP 的优势
关注点分离 :业务逻辑与横切关注点分离,使代码更加清晰
代码复用 :横切关注点只需要实现一次,可以应用到多个业务模块
易于维护 :当横切逻辑需要修改时,只需修改一处代码
非侵入性 :不需要修改原有业务代码,降低了对核心业务的影响
声明式编程 :通过配置或注解的方式声明切面,减少样板代码
提高开发效率 :开发者可以专注于核心业务逻辑的开发
1.6 AOP 的典型应用场景
日志记录 :记录方法调用、参数、返回值等信息
事务管理 :控制事务的开始、提交、回滚
性能监控 :统计方法执行时间、调用次数等性能指标
安全控制 :权限检查、访问控制
异常处理 :统一的异常捕获和处理
缓存管理 :缓存数据的存取和失效控制
分布式追踪 :在微服务架构中跟踪请求流程
数据校验 :输入参数的合法性检查
通过 AOP,这些横切关注点可以被优雅地处理,而不会污染核心业务代码。
2. AOP 核心组件与术语 在 AOP 的世界里,有一些核心概念和术语需要理解。这些概念共同构成了 AOP 的基本理论框架,是学习和应用 AOP 的基础。
2.1 切面(Aspect) 切面 是横切关注点的模块化表示,它封装了横切逻辑和其应用范围的定义。在 Spring AOP 中,切面通常是一个带有 @Aspect 注解的类。
切面可以看作是包含了一组相关通知和切入点定义的模块化单元。例如,一个日志切面可能包含记录方法调用的各种通知,以及定义哪些方法需要被记录日志的切入点。
2.2 连接点(Join Point) 连接点 是程序执行过程中可以被拦截并插入额外代码的点。在 Java 中,连接点通常是方法调用、字段访问、异常抛出等。
在 Spring AOP 中,连接点特指方法执行,因为 Spring AOP 主要关注方法级别的拦截。每个连接点代表程序执行流程中的一个特定位置。
2.3 通知(Advice) 通知 是在连接点执行的代码,它定义了在特定时机需要执行的横切行为。通知可以分为几种不同的类型:
前置通知(Before Advice) :在连接点执行前执行
后置通知(After Advice) :在连接点执行后执行,无论是否发生异常
返回通知(After Returning Advice) :在连接点正常执行完成后执行
异常通知(After Throwing Advice) :在连接点执行过程中抛出异常时执行
环绕通知(Around Advice) :包围连接点执行,可以在方法调用前后自定义行为,是最强大的通知类型
2.4 切入点(Pointcut) 切入点 是一组连接点的定义,它决定了通知将应用到哪些方法上。切入点通常使用表达式来指定目标方法的集合。
切入点表达式可以基于方法签名、注解、包路径等多种条件进行匹配。在 Spring AOP 中,常用的切入点表达式是 AspectJ 表达式。
2.5 目标对象(Target Object) 目标对象 是被一个或多个切面通知的对象,也称为被通知对象。在 Spring AOP 中,目标对象总是被代理的对象。
当客户端调用目标对象的方法时,实际上是调用代理对象的方法,代理对象再根据切面定义执行相应的通知,然后调用目标对象的实际方法。
2.6 代理(Proxy) 代理 是 AOP 框架创建的对象,它包装目标对象并拦截方法调用,根据切面定义执行相应的通知。
JDK 动态代理 :当目标对象实现了接口时使用
CGLIB 代理 :当目标对象没有实现接口时使用
2.7 织入(Weaving) 织入 是将切面应用到目标对象并创建代理对象的过程。织入可以在不同的时机进行:
编译时织入 :使用特殊的编译器在编译阶段将切面织入目标类
类加载时织入 :使用特殊的类加载器在类加载阶段织入切面
运行时织入 :在程序运行时通过动态代理技术织入切面,这是 Spring AOP 采用的方式
2.8 引介(Introduction) 引介 是一种特殊的通知,它允许向目标类添加新的方法或字段。通过引介,可以动态地修改类的行为,而不需要修改类的源代码。
在 Spring AOP 中,引介可以用于向目标对象添加新的接口实现。
2.9 横切关注点(Cross-cutting Concern) 横切关注点 是指那些跨越多个模块、影响系统多个部分的功能,如日志、事务、安全等。这些关注点通常难以用传统的面向对象方法进行模块化。
AOP 的核心目标就是将这些横切关注点从业务逻辑中分离出来,实现更好的模块化。
2.10 织入器(Weaver) 织入器 是负责将切面织入到目标对象中并创建代理对象的组件。在不同的 AOP 实现中,织入器的实现方式和工作时机可能不同。
2.11 AOP 术语之间的关系
切面 包含了通知 和切入点 的定义
通知 定义了在何时执行什么样的代码
切入点 定义了在哪些连接点 上应用通知
连接点 是程序执行过程中可被拦截的具体位置
目标对象 是被通知的对象
代理 是织入了切面逻辑的目标对象的封装
织入 是将切面应用到目标对象并创建代理的过程
理解这些核心概念和它们之间的关系,对于掌握 AOP 的工作原理和正确应用 AOP 至关重要。
2.12 核心术语图示
连接点 :是城市里的各个十字路口
切入点 :是规定哪些十字路口需要设置交通信号灯的规则
通知 :是在十字路口执行的具体交通控制行为(红灯停、绿灯行)
切面 :是包含了交通规则(切入点)和交通控制行为(通知)的整体方案
目标对象 :是在路上行驶的车辆
代理 :是在车辆行驶过程中执行交通规则的交通协管员
织入 :是将交通规则应用到道路和车辆上的过程
3. AOP 实现方式 AOP 可以通过多种方式实现,不同的实现方式有各自的特点和适用场景。下面介绍几种主要的 AOP 实现方式及其对比。
3.1 AspectJ AspectJ 是最成熟、功能最强大的 AOP 实现,它是一个独立的 AOP 框架,可以与任何 Java 项目集成。AspectJ 提供了完整的 AOP 功能,包括丰富的连接点类型、强大的切入点表达式和多种织入方式。
3.1.1 工作原理
编译时织入(Compile-time Weaving) :使用 AspectJ 编译器(ajc)在编译时将切面代码织入到目标类中
编译后织入(Post-compile Weaving) :对已编译的 class 文件进行织入
加载时织入(Load-time Weaving,LTW) :在类加载时通过 Java Agent 技术进行织入
3.1.2 特点
功能完整 :支持所有 AOP 核心概念,包括字段、构造函数等连接点
性能优秀 :编译时织入生成的代码性能最好
强大的切入点表达式 :提供了丰富的语法来精确定义切入点
独立于容器 :不依赖于任何特定的框架或容器
3.1.3 应用场景
需要全面 AOP 功能的复杂应用
对性能要求较高的系统
需要在编译期验证 AOP 配置的场景
3.2 Spring AOP Spring AOP 是 Spring 框架的一部分,它提供了基于代理的 AOP 实现,集成在 Spring 的依赖注入机制中。
3.2.1 工作原理
当目标对象实现了接口时,使用JDK 动态代理
当目标对象没有实现接口时,使用CGLIB 代理
Spring AOP 只支持方法级别的连接点
3.2.2 特点
与 Spring 无缝集成 :可以直接利用 Spring 的依赖注入功能
声明式配置 :支持基于注解和 XML 的声明式配置
运行时织入 :在程序运行时动态创建代理对象
轻量级 :相比于 AspectJ 更加轻量,适合与 Spring 框架一起使用
3.2.3 应用场景
Spring 应用中的方法拦截和增强
事务管理、日志记录、安全控制等常见横切关注点
不需要访问非方法连接点的场景
3.3 JBoss AOP JBoss AOP 是 JBoss 应用服务器提供的 AOP 框架,它提供了类似 AspectJ 的功能,但更紧密地集成在 JBoss 应用服务器中。
3.3.1 工作原理
3.3.2 特点
与 JBoss 服务器深度集成
支持拦截器链模式
提供声明式配置
支持丰富的连接点类型
3.3.3 应用场景
在 JBoss 应用服务器中运行的企业应用
需要与 JBoss 容器集成的 AOP 需求
3.4 Google Guice AOP Google Guice 是一个轻量级的依赖注入框架,它也提供了基本的 AOP 功能。
3.4.1 工作原理 Guice AOP 基于 JDK 动态代理实现,只支持接口代理。
3.4.2 特点
轻量级 :相比 Spring AOP 更加轻量
简洁的 API :使用起来比较简单直观
与 Guice DI 集成 :与 Guice 的依赖注入功能紧密集成
功能有限 :只支持基本的 AOP 功能,不支持字段拦截等高级特性
3.4.3 应用场景
使用 Guice 作为 DI 容器的项目
需要简单 AOP 功能的轻量级应用
3.5 自定义 AOP 实现 除了使用现成的 AOP 框架,还可以基于代理模式等设计模式实现自定义的 AOP 功能。
3.5.1 基于代理模式 通过手动创建代理类,可以实现简单的 AOP 功能:
静态代理 :手动编写代理类
动态代理 :使用 JDK 动态代理或 CGLIB 等库在运行时创建代理
3.5.2 特点
完全自定义 :可以根据具体需求定制 AOP 实现
灵活性高 :可以精确控制代理行为
开发成本高 :需要自己实现所有代理逻辑
功能有限 :通常只能支持基本的方法拦截
3.5.3 应用场景
学习 AOP 原理
特定的自定义拦截需求
不依赖第三方 AOP 框架的场景
3.6 不同 AOP 实现方式的对比 实现方式 织入时机 支持的连接点 性能 易用性 集成性 适用场景 AspectJ 编译时/加载时/运行时 全面(方法、字段、构造函数等) 最好(编译时织入) 中等(需要额外学习) 可与任何框架集成 复杂 AOP 需求,对性能要求高 Spring AOP 运行时 仅方法 中等 高(与 Spring 无缝集成) 与 Spring 框架紧密集成 Spring 应用中的常见横切关注点 JBoss AOP 编译时/加载时/运行时 全面 较好 中等 与 JBoss 服务器集成 JBoss 应用服务器中的应用 Guice AOP 运行时 仅方法(仅接口) 中等 高 与 Guice DI 集成 使用 Guice 的轻量级应用 自定义 AOP 视实现而定 通常仅方法 视实现而定 低 无依赖 学习或特定需求
3.7 如何选择适合的 AOP 实现
项目技术栈 :如果已经使用 Spring 框架,Spring AOP 通常是最自然的选择
AOP 需求的复杂度 :简单的方法拦截使用 Spring AOP 即可,复杂的 AOP 需求可能需要 AspectJ
性能要求 :对性能要求极高的场景,AspectJ 的编译时织入可能更合适
开发成本 :Spring AOP 配置简单,学习成本低;AspectJ 功能强大但学习曲线较陡
部署环境 :在特定容器中运行的应用可能需要使用容器提供的 AOP 实现
在实际项目中,Spring AOP 和 AspectJ 的结合使用是一种常见的模式:使用 Spring AOP 处理简单的方法拦截,对于更复杂的 AOP 需求,则使用 AspectJ 的能力。Spring 框架也提供了对 AspectJ 的集成支持,使得这种组合使用更加方便。
4. Spring AOP 详解 Spring AOP 是 Spring 框架的核心功能之一,它提供了声明式的企业级服务,如事务管理、安全控制等。下面详细介绍 Spring AOP 的核心概念、实现机制和使用方法。
4.1 Spring AOP 的基本原理 Spring AOP 建立在动态代理机制之上,它使用代理拦截对目标对象的方法调用,并在代理中执行切面逻辑。Spring AOP 主要关注方法级别的拦截,提供了一种轻量级的 AOP 实现。
4.1.1 代理选择机制 Spring AOP 会根据目标对象的类型自动选择适当的代理实现:
当目标对象实现了至少一个接口时,Spring 使用JDK 动态代理
当目标对象没有实现任何接口时,Spring 使用CGLIB 代理
从 Spring 3.2 开始,Spring 可以通过配置强制使用 CGLIB 代理,即使目标对象实现了接口。
4.1.2 代理创建时机 在 Spring 容器启动时,当一个 bean 被定义为切面或需要被切面增强时,Spring 会为其创建代理对象。代理对象在容器中被管理,客户端获取的是代理对象而不是原始目标对象。
4.2 Spring AOP 的核心组件 Spring AOP 的核心组件对应于 AOP 的基本概念:
4.2.1 切面(Aspect) 在 Spring 中,切面通常是一个带有 @Aspect 注解的类,它包含通知和切入点的定义。
@Aspect
@Component
public class LoggingAspect {
}
4.2.2 通知(Advice) Spring AOP 支持五种类型的通知,通过不同的注解来标识:
@Before:前置通知
@After:后置通知
@AfterReturning:返回通知
@AfterThrowing:异常通知
@Around:环绕通知
4.2.3 切入点(Pointcut) Spring AOP 使用 AspectJ 的切入点表达式语言来定义切入点,通过 @Pointcut 注解来声明:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods () {}
4.2.4 目标对象(Target)
4.2.5 代理(Proxy) Spring 创建的代理对象,它包装了目标对象并添加了切面逻辑。
4.3 Spring AOP 的配置方式 Spring AOP 支持两种主要的配置方式:基于注解的配置和基于 XML 的配置。
4.3.1 基于注解的配置 基于注解的配置是 Spring AOP 推荐的配置方式,更加简洁和灵活。
在配置类上添加 @EnableAspectJAutoProxy 注解:
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AppConfig {}
创建带有 @Aspect 和 @Component 注解的切面类:
@Aspect
@Component
public class LoggingAspect {
}
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods () {}
@Before("serviceMethods()")
public void logBefore (JoinPoint joinPoint) {
System.out.println("Method: " + joinPoint.getSignature().getName());
}
4.3.2 基于 XML 的配置 对于偏好 XML 配置的项目,Spring AOP 也提供了完整的 XML 配置支持。
<aop:config >
<aop:aspect id ="loggingAspect" ref ="loggingAspectBean" >
<aop:pointcut id ="serviceMethods" expression ="execution(* com.example.service.*.*(..))" />
<aop:before pointcut-ref ="serviceMethods" method ="logBefore" />
<aop:after pointcut-ref ="serviceMethods" method ="logAfter" />
</aop:aspect >
</aop:config >
<bean id ="loggingAspectBean" class ="com.example.aspect.LoggingAspect" />
4.4 Spring AOP 与 AspectJ 的集成 虽然 Spring AOP 本身是一个轻量级实现,但它提供了与功能更强大的 AspectJ 的无缝集成。Spring 对 AspectJ 的集成主要体现在以下几个方面:
4.4.1 共享切入点表达式 Spring AOP 使用 AspectJ 的切入点表达式语法,这使得 Spring AOP 和 AspectJ 之间的迁移更加容易。
4.4.2 AspectJ 注解支持 Spring AOP 支持使用 AspectJ 的注解(如 @Aspect、@Before、@After 等)来定义切面,简化了配置。
4.4.3 加载时织入(Load-Time Weaving, LTW) 对于需要更强大 AOP 功能的场景,Spring 提供了对 AspectJ LTW 的集成支持,可以在 JVM 类加载时织入切面。
<dependency >
<groupId > org.aspectj</groupId >
<artifactId > aspectjrt</artifactId >
<version > 1.9.9.1</version >
</dependency >
<dependency >
<groupId > org.aspectj</groupId >
<artifactId > aspectjweaver</artifactId >
<version > 1.9.9.1</version >
</dependency >
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {}
<context:load-time-weaver />
-javaagent:/path/to/aspectjweaver.jar
4.5 Spring AOP 的实现原理 Spring AOP 的核心实现基于两个关键组件:
4.5.1 ProxyFactoryBean ProxyFactoryBean 是 Spring 创建 AOP 代理的核心类,它实现了 Spring 的 FactoryBean 接口,可以生成代理对象。ProxyFactoryBean 负责:
管理通知器(Advisor)
选择代理策略(JDK 或 CGLIB)
创建并配置代理对象
4.5.2 AspectJExpressionPointcut AspectJExpressionPointcut 是 Spring 对 AspectJ 切入点表达式的实现,它负责解析和评估切入点表达式,判断一个方法调用是否匹配给定的切入点。
4.6 Spring AOP 的代理机制详解
4.6.1 JDK 动态代理 JDK 动态代理是基于 java.lang.reflect.Proxy 类和 InvocationHandler 接口实现的。当使用 JDK 动态代理时:
Spring 创建一个实现了目标对象所有接口的代理类
当调用代理对象的方法时,调用会被转发到 InvocationHandler 的 invoke 方法
在 invoke 方法中,Spring 执行相应的通知,然后调用目标对象的原始方法
优点 :不需要额外的依赖,是 JDK 自带的功能
缺点 :只能代理接口,不能代理类
4.6.2 CGLIB 代理 CGLIB(Code Generation Library)是一个强大的代码生成库,它通过继承目标类来创建代理对象。当使用 CGLIB 代理时:
Spring 创建一个继承自目标类的代理类
代理类重写目标类的方法
当调用代理对象的方法时,会执行重写的方法,在其中执行通知,然后调用父类的原始方法
优点 :可以代理类,不限于接口
缺点 :需要引入额外的依赖,代理类不能是 final 的,被代理的方法不能是 final 的
4.6.3 代理选择策略
如果目标对象实现了接口,默认使用 JDK 动态代理
如果目标对象没有实现接口,使用 CGLIB 代理
可以通过配置强制使用 CGLIB 代理,即使目标对象实现了接口
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {}
<aop:aspectj-autoproxy proxy-target-class ="true" />
4.7 Spring AOP 的局限性 虽然 Spring AOP 是一个强大的工具,但它也有一些局限性:
仅支持方法级连接点 :Spring AOP 只支持方法级别的拦截,不支持字段访问、构造函数等其他类型的连接点
基于代理的实现 :Spring AOP 通过代理实现,只能拦截通过代理对象的方法调用,不能拦截对象内部的方法调用
运行时织入 :Spring AOP 是运行时织入的,这可能会带来一定的性能开销
容器内的限制 :Spring AOP 只能增强由 Spring 容器管理的 bean
对于需要突破这些限制的场景,可以考虑使用 AspectJ 的完整功能。
4.8 Spring AOP 的最佳实践 在使用 Spring AOP 时,遵循以下最佳实践可以提高代码质量和性能:
精确定义切入点 :使用精确的切入点表达式,避免不必要的方法拦截
优先使用注解配置 :基于注解的配置更加简洁和类型安全
合理使用通知类型 :根据需求选择最合适的通知类型,环绕通知应谨慎使用
避免在通知中执行复杂逻辑 :通知应该保持简洁,复杂逻辑应委托给其他组件
注意代理的局限性 :了解代理的工作原理,避免在对象内部调用会绕过代理
使用 @Order 控制切面顺序 :当有多个切面时,使用 @Order 注解控制切面的执行顺序
5. 切面表达式与通知类型
5.1 切点表达式语法 切点表达式是 AOP 中定义连接点的核心机制,它决定了哪些方法调用会被拦截和处理。在 Spring AOP 中,切点表达式使用 AspectJ 的表达式语言。
5.1.1 切点表达式的基本语法 AspectJ 的切点表达式主要由以下几个部分组成:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
modifiers-pattern :方法的修饰符(如 public、private),可选
return-type-pattern :方法的返回类型,必填,* 表示任意返回类型
declaring-type-pattern :方法所属的类或接口,可选
method-name-pattern :方法名,必填,* 表示任意方法名
param-pattern :方法参数,必填,() 表示无参,(..) 表示任意参数,(String) 表示单个 String 参数
throws-pattern :抛出的异常类型,可选
5.1.2 切点表达式的通配符
*:匹配任意数量的字符(但不包括包分隔符)
..:匹配任意数量的字符,包括包分隔符,用于匹配子包或任意参数列表
+:匹配指定类型及其子类或实现类
5.1.3 常用切点表达式示例
@Pointcut("execution(public * com.example.service.*.*(..))")
@Pointcut("execution(* com.example..*.*(..))")
@Pointcut("execution(* *..*Service.*(..))")
@Pointcut("execution(void *.*(String))")
@Pointcut("execution(* *.*(..) throws Exception)")
5.2 基于注解的切点表达式 除了使用 execution 表达式外,Spring AOP 还支持基于注解的切点表达式,这在实际开发中非常有用。
5.2.1 @annotation 表达式 @annotation 表达式用于匹配被特定注解标注的方法:
@Pointcut("@annotation(com.example.annotation.Log)")
5.2.2 @within 表达式 @within 表达式用于匹配特定注解标注的类中的所有方法:
@Pointcut("@within(org.springframework.stereotype.Service)")
5.2.3 @args 表达式 @args 表达式用于匹配参数被特定注解标注的方法:
@Pointcut("@args(com.example.annotation.Valid)")
5.3 切点表达式的组合 在实际应用中,我们经常需要组合多个切点表达式来满足复杂的需求。Spring AOP 支持使用逻辑运算符来组合切点表达式:
&&(或 and):逻辑与,匹配同时满足两个表达式的连接点
||(或 or):逻辑或,匹配满足任一表达式的连接点
!(或 not):逻辑非,匹配不满足表达式的连接点
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(com.example.annotation.Log)")
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.controller.*.*(..))")
@Pointcut("!execution(private * *.*(..))")
5.4 通知类型详解 通知(Advice)是 AOP 中的核心概念,它定义了在连接点执行的代码。Spring AOP 支持以下几种通知类型:
5.4.1 前置通知(Before Advice) 前置通知在目标方法执行之前执行。它可以获取连接点的信息,但不能阻止目标方法的执行(除非抛出异常)。
@Component
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore (JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Before method: " + method + ", arguments: " + Arrays.toString(args));
}
}
5.4.2 后置通知(After Returning Advice) 后置通知在目标方法成功执行后执行,它可以访问目标方法的返回值。
@Component
@Aspect
public class LoggingAspect {
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning (JoinPoint joinPoint, Object result) {
String method = joinPoint.getSignature().getName();
System.out.println("After returning method: " + method + ", result: " + result);
}
}
5.4.3 异常通知(After Throwing Advice) 异常通知在目标方法抛出异常后执行,它可以访问抛出的异常。
@Component
@Aspect
public class LoggingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
public void logAfterThrowing (JoinPoint joinPoint, Exception exception) {
String method = joinPoint.getSignature().getName();
System.out.println("After throwing method: " + method + ", exception: " + exception.getMessage());
}
}
5.4.4 最终通知(After Advice) 最终通知在目标方法执行完成后执行,无论方法是正常返回还是抛出异常。它类似于 try-catch-finally 中的 finally 块。
@Component
@Aspect
public class LoggingAspect {
@After("execution(* com.example.service.*.*(..))")
public void logAfter (JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
System.out.println("After method: " + method);
}
}
5.4.5 环绕通知(Around Advice) 环绕通知是功能最强大的通知类型,它可以在目标方法执行前后执行自定义逻辑,甚至可以决定是否执行目标方法、修改方法的参数或返回值。
@Component
@Aspect
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String method = proceedingJoinPoint.getSignature().getName();
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("Around before: " + method + ", arguments: " + Arrays.toString(args));
try {
Object result = proceedingJoinPoint.proceed();
System.out.println("Around after returning: " + method + ", result: " + result);
return result;
} catch (Throwable throwable) {
System.out.println("Around after throwing: " + method + ", exception: " + throwable.getMessage());
throw throwable;
} finally {
System.out.println("Around finally: " + method);
}
}
}
5.5 通知的执行顺序 当同一个连接点匹配多个通知时,通知的执行顺序如下:
环绕通知的前置部分
前置通知
目标方法执行
环绕通知的后置部分
后置通知
最终通知
环绕通知的前置部分
前置通知
目标方法执行(抛出异常)
异常通知
最终通知
环绕通知的异常处理部分
5.6 通知的参数绑定 在通知方法中,我们可以绑定各种上下文信息,如连接点信息、方法参数、返回值等。
5.6.1 绑定 JoinPoint 所有通知方法都可以绑定 JoinPoint 参数,用于获取连接点的信息:
@Before("execution(* com.example.service.*.*(..))")
public void logBefore (JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object target = joinPoint.getTarget();
Object thisObj = joinPoint.getThis();
Object[] args = joinPoint.getArgs();
}
5.6.2 绑定方法参数 我们可以使用 args() 表达式来绑定方法参数:
@Before("execution(* com.example.service.*.*(String, ..)) && args(name, ..)")
public void logWithName (JoinPoint joinPoint, String name) {
System.out.println("Method called with name: " + name);
}
5.6.3 绑定返回值 @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logReturning (JoinPoint joinPoint, Object result) {
System.out.println("Method returned: " + result);
}
5.6.4 绑定异常 @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
public void logException (JoinPoint joinPoint, Exception exception) {
System.out.println("Method threw: " + exception.getMessage());
}
5.7 切点表达式的性能考虑 切点表达式的定义会直接影响 AOP 的性能,因此在编写切点表达式时,应考虑以下性能优化原则:
尽可能精确 :避免使用过于宽泛的表达式,如 execution(* *.*(..))
优先使用类型匹配 :先通过类名或包名缩小范围,再进行方法名和参数匹配
避免使用复杂的组合表达式 :复杂的逻辑组合会增加表达式解析的开销
合理使用切点表达式引用 :对于重复使用的表达式,使用 @Pointcut 定义并引用
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods () {}
@Before("serviceMethods()")
public void logServiceMethod (JoinPoint joinPoint) {
}
5.8 切点表达式的调试技巧 在实际开发中,编写正确的切点表达式可能会遇到挑战。以下是一些调试切点表达式的技巧:
使用 Spring AOP 的日志级别 :设置 org.springframework.aop 的日志级别为 DEBUG 或 TRACE,可以看到 AOP 的详细执行信息
使用简单表达式逐步构建 :从简单的表达式开始,逐步添加条件
使用 @Pointcut 定义并测试 :将复杂的表达式分解为多个简单表达式,分别测试
使用 AspectJ 的 ajc 编译器 :AspectJ 提供了更严格的表达式检查
通过这些技巧,我们可以更有效地编写和调试切点表达式,确保它们正确地匹配所需的连接点。
6. AOP 实际应用场景 AOP 在企业应用开发中有着广泛的应用,下面介绍一些常见的实际应用场景。
6.1 日志记录 日志记录是 AOP 最常见的应用场景之一。通过 AOP,我们可以在不修改业务代码的情况下,统一记录方法的调用信息、参数、执行时间等。
6.1.1 操作日志记录 记录用户的关键操作,如登录、修改数据、删除操作等,便于系统审计和问题追踪。
6.1.2 性能日志记录
6.1.3 异常日志记录
6.2 事务管理 在企业应用中,事务管理是一个核心需求。通过 AOP,我们可以实现声明式事务管理,简化事务控制代码。
6.2.1 声明式事务 使用 AOP 在方法执行前后自动开启和提交事务,当方法抛出异常时自动回滚事务。
6.2.2 事务传播行为控制 通过 AOP 控制事务的传播行为,如 REQUIRED、REQUIRES_NEW、SUPPORTS 等。
6.3 安全控制 AOP 可以用于实现系统的安全控制,如权限验证、访问控制等。
6.3.1 权限验证
6.3.2 访问控制
6.3.3 接口限流
6.4 缓存管理
6.4.1 缓存查询结果
6.4.2 缓存失效控制
6.5 异常处理 通过 AOP 统一处理系统异常,提供友好的错误信息。
6.5.1 统一异常处理
6.5.2 异常信息国际化
6.6 分布式追踪 在微服务架构中,AOP 可以用于实现分布式追踪,监控请求的流转过程。
6.6.1 链路追踪 记录请求的调用链路,包括服务间的调用关系和执行时间。
6.6.2 分布式事务协调
6.7 数据校验 通过 AOP 实现方法参数的数据校验,确保数据的合法性。
6.7.1 输入参数校验
6.7.2 业务规则校验 根据业务规则对数据进行校验,确保业务逻辑的正确性。
6.8 异步处理 通过 AOP 实现方法的异步执行,提高系统的并发处理能力。
6.8.1 异步方法调用
6.8.2 事件发布 通过 AOP 实现事件的自动发布,解耦事件的生产者和消费者。
7. 代码示例 本节提供一些常见 AOP 应用场景的完整代码示例,帮助读者更好地理解和应用 AOP。
7.1 日志记录切面示例
7.1.1 自定义日志注解 package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value () default "" ;
}
7.1.2 日志切面实现 package com.example.aspect;
import com.example.annotation.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Pointcut("@annotation(com.example.annotation.Log)")
public void logPointCut () {}
@Before("logPointCut()")
public void logBefore (JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
logger.info("[Log] 操作描述:{}" , logAnnotation.value());
logger.info("[Log] 方法名:{}.{}" , joinPoint.getTarget().getClass().getName(), method.getName());
logger.info("[Log] 参数:{}" , Arrays.toString(joinPoint.getArgs()));
}
@Around("logPointCut()")
public Object logAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.info("[Log] 执行时间:{}ms" , (endTime - startTime));
logger.info("[Log] 返回结果:{}" , result);
return result;
} catch (Throwable throwable) {
logger.error("[Log] 执行异常:{}" , throwable.getMessage(), throwable);
throw throwable;
}
}
@AfterReturning(pointcut = "logPointCut()", returning = "result")
public void logAfterReturning (JoinPoint joinPoint, Object result) {
logger.info("[Log] 方法执行成功" );
}
@AfterThrowing(pointcut = "logPointCut()", throwing = "exception")
public void logAfterThrowing (JoinPoint joinPoint, Exception exception) {
logger.error("[Log] 方法执行异常:{}" , exception.getMessage());
}
}
7.1.3 使用示例 package com.example.service;
import com.example.annotation.Log;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Log("查询用户信息")
public User getUserById (Long userId) {
return new User (userId, "张三" , 25 );
}
@Log("创建用户")
public User createUser (User user) {
return user;
}
}
7.2 事务管理切面示例
7.2.1 自定义事务注解 package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
String propagation () default "REQUIRED" ;
String isolation () default "DEFAULT" ;
int timeout () default -1 ;
boolean readOnly () default false ;
Class<? extends Throwable >[] rollbackFor() default {};
Class<? extends Throwable >[] noRollbackFor() default {};
}
7.2.2 事务切面实现 package com.example.aspect;
import com.example.annotation.Transactional;
import com.example.transaction.TransactionManager;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class TransactionAspect {
@Autowired
private TransactionManager transactionManager;
@Pointcut("@annotation(com.example.annotation.Transactional)")
public void transactionPointCut () {}
@Around("transactionPointCut()")
public Object transactionAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Transactional transactional = method.getAnnotation(Transactional.class);
transactionManager.beginTransaction();
try {
Object result = proceedingJoinPoint.proceed();
transactionManager.commit();
return result;
} catch (Throwable throwable) {
if (shouldRollback(throwable, transactional)) {
transactionManager.rollback();
} else {
transactionManager.commit();
}
throw throwable;
}
}
private boolean shouldRollback (Throwable throwable, Transactional transactional) {
for (Class<? extends Throwable > noRollbackClass : transactional.noRollbackFor()) {
if (noRollbackClass.isAssignableFrom(throwable.getClass())) {
return false ;
}
}
Class<? extends Throwable >[] rollbackClasses = transactional.rollbackFor();
if (rollbackClasses.length > 0 ) {
for (Class<? extends Throwable > rollbackClass : rollbackClasses) {
if (rollbackClass.isAssignableFrom(throwable.getClass())) {
return true ;
}
}
return false ;
}
return throwable instanceof RuntimeException;
}
}
7.2.3 使用示例 package com.example.service;
import com.example.annotation.Transactional;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryService inventoryService;
@Transactional(rollbackFor = Exception.class)
public Order createOrder (Order order) {
Order savedOrder = orderRepository.save(order);
inventoryService.reduceStock(order.getProductId(), order.getQuantity());
return savedOrder;
}
}
7.3 安全控制切面示例
7.3.1 自定义权限注解 package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value () ;
}
7.3.2 权限切面实现 package com.example.aspect;
import com.example.annotation.RequirePermission;
import com.example.exception.PermissionDeniedException;
import com.example.security.SecurityContext;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class PermissionAspect {
@Autowired
private SecurityContext securityContext;
@Pointcut("@annotation(com.example.annotation.RequirePermission)")
public void permissionPointCut () {}
@Before("permissionPointCut()")
public void checkPermission (JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequirePermission requirePermission = method.getAnnotation(RequirePermission.class);
String requiredPermission = requirePermission.value();
if (!securityContext.hasPermission(requiredPermission)) {
throw new PermissionDeniedException ("您没有权限执行此操作:" + requiredPermission);
}
}
}
7.3.3 使用示例 package com.example.controller;
import com.example.annotation.RequirePermission;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/admin")
public class AdminController {
@RequirePermission("user:list")
@GetMapping("/users")
public List<User> listUsers () {
}
@RequirePermission("user:create")
@PostMapping("/users")
public User createUser (@RequestBody User user) {
}
@RequirePermission("user:delete")
@DeleteMapping("/users/{id}")
public void deleteUser (@PathVariable Long id) {
}
}
7.4 缓存切面示例
7.4.1 自定义缓存注解 package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String key () ;
int expire () default 3600 ;
}
7.4.2 缓存切面实现 package com.example.aspect;
import com.example.annotation.Cacheable;
import com.example.cache.CacheManager;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class CacheAspect {
@Autowired
private CacheManager cacheManager;
@Pointcut("@annotation(com.example.annotation.Cacheable)")
public void cachePointCut () {}
@Around("cachePointCut()")
public Object cacheAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Cacheable cacheable = method.getAnnotation(Cacheable.class);
String key = generateCacheKey(cacheable.key(), proceedingJoinPoint);
Object cachedValue = cacheManager.get(key);
if (cachedValue != null ) {
return cachedValue;
}
Object result = proceedingJoinPoint.proceed();
cacheManager.put(key, result, cacheable.expire());
return result;
}
private String generateCacheKey (String keyPattern, ProceedingJoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
return keyPattern + ":" + className + ":" + methodName + ":" + args;
}
}
7.4.3 使用示例 package com.example.service;
import com.example.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Cacheable(key = "product", expire = 7200)
public Product getProductById (Long productId) {
return productRepository.findById(productId).orElse(null );
}
@Cacheable(key = "product:list")
public List<Product> listProducts () {
return productRepository.findAll();
}
}
7.5 完整的 Spring Boot AOP 示例 下面是一个完整的 Spring Boot 应用示例,展示如何在实际项目中使用 AOP。
7.5.1 Maven 依赖 <dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-aop</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-web</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-data-jpa</artifactId >
</dependency >
<dependency >
<groupId > com.h2database</groupId >
<artifactId > h2</artifactId >
<scope > runtime</scope >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
<optional > true</optional >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
<scope > test</scope >
</dependency >
</dependencies >
7.5.2 应用主类 package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopDemoApplication {
public static void main (String[] args) {
SpringApplication.run(AopDemoApplication.class, args);
}
}
7.5.3 实体类 package com.example.entity;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String password;
private String role;
}
7.5.4 Repository package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository <User, Long> {
User findByUsername (String username) ;
}
7.5.5 Service package com.example.service;
import com.example.annotation.Log;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Log("获取用户列表")
public List<User> listUsers () {
return userRepository.findAll();
}
@Log("根据 ID 获取用户")
public User getUserById (Long id) {
return userRepository.findById(id).orElse(null );
}
@Log("创建用户")
@Transactional
public User createUser (User user) {
return userRepository.save(user);
}
@Log("更新用户")
@Transactional
public User updateUser (User user) {
return userRepository.save(user);
}
@Log("删除用户")
@Transactional
public void deleteUser (Long id) {
userRepository.deleteById(id);
}
}
7.5.6 Controller package com.example.controller;
import com.example.annotation.RequirePermission;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
@RequirePermission("user:list")
public List<User> listUsers () {
return userService.listUsers();
}
@GetMapping("/{id}")
@RequirePermission("user:view")
public User getUserById (@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@RequirePermission("user:create")
public User createUser (@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping("/{id}")
@RequirePermission("user:update")
public User updateUser (@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.updateUser(user);
}
@DeleteMapping("/{id}")
@RequirePermission("user:delete")
public void deleteUser (@PathVariable Long id) {
userService.deleteUser(id);
}
}
7.5.7 自定义注解 package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value () default "" ;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value () ;
}
7.5.8 切面实现 package com.example.aspect;
import com.example.annotation.Log;
import com.example.annotation.RequirePermission;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("@annotation(com.example.annotation.Log)")
public void logPointCut () {}
@Around("logPointCut()")
public Object logAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
logger.info("[Log] 操作:{}" , logAnnotation.value());
logger.info("[Log] 方法:{}.{}" , proceedingJoinPoint.getTarget().getClass().getName(), method.getName());
logger.info("[Log] 参数:{}" , Arrays.toString(proceedingJoinPoint.getArgs()));
try {
Object result = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.info("[Log] 耗时:{}ms" , (endTime - startTime));
logger.info("[Log] 结果:{}" , result);
return result;
} catch (Throwable throwable) {
logger.error("[Log] 异常:{}" , throwable.getMessage(), throwable);
throw throwable;
}
}
}
@Aspect
@Component
public class PermissionAspect {
private static final Logger logger = LoggerFactory.getLogger(PermissionAspect.class);
@Pointcut("@annotation(com.example.annotation.RequirePermission)")
public void permissionPointCut () {}
@Before("permissionPointCut()")
public void checkPermission (JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequirePermission requirePermission = method.getAnnotation(RequirePermission.class);
String requiredPermission = requirePermission.value();
logger.info("[Permission] 检查权限:{}" , requiredPermission);
boolean hasPermission = true ;
if (!hasPermission) {
throw new RuntimeException ("权限不足,需要权限:" + requiredPermission);
}
}
}
@Aspect
@Component
public class PerformanceAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethodPointCut () {}
@Around("serviceMethodPointCut()")
public Object performanceAround (ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
if (endTime - startTime > 100 ) {
logger.warn("[Performance] 方法执行较慢:{}.{},耗时:{}ms" , proceedingJoinPoint.getTarget().getClass().getName(), proceedingJoinPoint.getSignature().getName(), (endTime - startTime));
}
return result;
} catch (Throwable throwable) {
logger.error("[Performance] 方法执行异常:{}" , throwable.getMessage());
throw throwable;
}
}
}
7.5.9 配置文件
spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
path: /h2-console
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.aop: TRACE
通过这个完整的示例,我们可以看到如何在实际项目中使用 AOP 来实现日志记录、权限控制和性能监控等功能。这些切面可以帮助我们保持业务代码的简洁性,同时提供横切关注点的统一处理。
8. AOP 最佳实践和注意事项 在使用 AOP 的过程中,遵循最佳实践可以帮助我们避免常见的陷阱,提高系统的可维护性和性能。本节将介绍 AOP 的最佳实践和需要注意的事项。
8.1 最佳实践
8.1.1 保持切面的单一职责 每个切面应该只关注一个横切关注点,避免在一个切面中实现多个不相关的功能。这样可以提高切面的内聚性和可维护性。
@Aspect
@Component
public class LoggingAspect {
}
@Aspect
@Component
public class TransactionAspect {
}
@Aspect
@Component
public class MultiPurposeAspect {
}
8.1.2 使用自定义注解定义切点 使用自定义注解来定义切点,可以提高代码的可读性和灵活性。通过注解,我们可以精确地控制哪些方法需要应用切面。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value () default "" ;
}
@Aspect
@Component
public class LoggingAspect {
@Pointcut("@annotation(com.example.annotation.Loggable)")
public void loggableMethods () {}
}
@Service
public class UserService {
@Loggable("获取用户信息")
public User getUserById (Long id) {
}
}
8.1.3 合理使用不同类型的通知 根据需求选择合适的通知类型,避免滥用环绕通知。对于简单的前后处理,可以使用 @Before 和 @AfterReturning 等更简单的通知类型。
@Before("logPointCut()")
public void validateParameters (JoinPoint joinPoint) {
}
@Around("cachePointCut()")
public Object cacheAround (ProceedingJoinPoint pjp) throws Throwable {
}
8.1.4 避免在切面中执行业务逻辑 切面应该只负责横切关注点的处理,不应该包含业务逻辑。业务逻辑应该保留在目标对象中。
@Around("orderPointCut()")
public Object processOrder (ProceedingJoinPoint pjp) throws Throwable {
Order order = (Order) pjp.getArgs()[0 ];
if (order.getAmount() > 1000 ) {
order.setDiscount(0.9 );
}
return pjp.proceed();
}
@Service
public class OrderService {
public Order createOrder (Order order) {
if (order.getAmount() > 1000 ) {
order.setDiscount(0.9 );
}
return orderRepository.save(order);
}
}
8.1.5 适当抽象通用逻辑 对于多个切面中可能共享的逻辑,应该进行适当的抽象,避免代码重复。
public class LogUtils {
public static void logMethodEntry (Logger logger, String methodName, Object[] args) {
logger.info("Entering method: {}, args: {}" , methodName, Arrays.toString(args));
}
public static void logMethodExit (Logger logger, String methodName, Object result) {
logger.info("Exiting method: {}, result: {}" , methodName, result);
}
}
@Aspect
@Component
public class ServiceLoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLoggingAspect.class);
@Before("serviceMethods()")
public void logBefore (JoinPoint joinPoint) {
LogUtils.logMethodEntry(logger, joinPoint.getSignature().getName(), joinPoint.getArgs());
}
}
8.1.6 使用 SpEL 表达式增强切点灵活性 在 Spring AOP 中,可以使用 Spring 表达式语言 (SpEL) 来增强切点的灵活性,特别是在处理注解属性时。
@Around("@annotation(loggable) && args(.., @org.springframework.web.bind.annotation.RequestBody requestBody)")
public Object logWithRequestBody (ProceedingJoinPoint pjp, Loggable loggable, Object requestBody) throws Throwable {
logger.info("Operation: {}, Request Body: {}" , loggable.value(), requestBody);
return pjp.proceed();
}
8.1.7 合理设置切面的优先级 当多个切面应用于同一个连接点时,需要合理设置切面的执行顺序,以确保功能的正确性。
@Aspect
@Component
@Order(1)
public class TransactionAspect {
}
@Aspect
@Component
@Order(2)
public class LoggingAspect {
}
@Aspect
@Component
@Order(3)
public class PerformanceAspect {
}
8.1.8 编写单元测试验证切面行为 为切面编写单元测试,确保切面的行为符合预期,特别是在异常处理等边缘情况下。
@SpringBootTest
public class LoggingAspectTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
public void testLoggingAspect () {
when (userRepository.findById(1L )).thenReturn(Optional.of(new User (1L , "test" , "[email protected] " )));
User user = userService.getUserById(1L );
assertNotNull(user);
}
}
8.2 注意事项
8.2.1 避免过度使用 AOP AOP 是一个强大的工具,但不应该过度使用。只有当功能确实是横切关注点且在多个地方重复出现时,才考虑使用 AOP。
是否是横切关注点?
是否在多个地方重复出现?
使用 AOP 是否能提高代码的可维护性?
8.2.2 注意 AOP 的性能影响 AOP 会引入额外的代理对象和方法调用,可能会对性能产生一定影响。在性能敏感的场景中,需要谨慎使用。
避免在高频调用的方法上使用复杂的切面
优化切点表达式,使其尽可能精确
避免在切面中执行耗时操作
8.2.3 了解 Spring AOP 的局限性 Spring AOP 基于动态代理,有一定的局限性,如只能拦截方法级别的连接点、不能拦截构造方法等。在需要更强大功能时,考虑使用 AspectJ。
只能增强 Spring 容器中的 bean
只能拦截方法级别的连接点
对于 final 方法、static 方法、private 方法无法增强
不能拦截构造方法
不能修改方法的返回类型
8.2.4 注意 this 和 target 的区别 在切点表达式中,this() 和 target() 有重要区别。this() 匹配代理对象类型,而 target() 匹配目标对象类型。
this(com.example.UserService): 匹配代理对象是 UserService 类型的所有方法
target(com.example.UserService): 匹配目标对象是 UserService 类型的所有方法
8.2.5 避免循环依赖 在使用 AOP 时,需要注意避免切面和目标对象之间的循环依赖,这可能导致 Spring 容器启动失败。
切面尽量不要依赖具体的业务组件
使用接口而非实现类进行依赖注入
合理设计切面的依赖关系
8.2.6 注意异常处理 在环绕通知中,需要正确处理异常,确保不会吞掉目标方法抛出的异常,同时也要确保切面自身的异常不会影响目标方法的行为。
@Around("logPointCut()")
public Object logAround (ProceedingJoinPoint pjp) throws Throwable {
try {
logger.info("Before method execution" );
Object result = pjp.proceed();
logger.info("After method execution" );
return result;
} catch (Throwable throwable) {
logger.error("Exception in method: {}" , throwable.getMessage());
throw throwable;
}
}
8.2.7 注意参数和返回值的修改 在环绕通知中,可以修改方法的参数和返回值,但这种做法应该谨慎使用,因为它可能会导致代码行为难以理解和调试。
@Around("transactionalMethods()")
public Object modifyParameters (ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (args.length > 0 && args[0 ] instanceof User) {
User user = (User) args[0 ];
user.setLastUpdated(new Date ());
}
return pjp.proceed(args);
}
8.2.8 注意 static 方法和 final 方法 Spring AOP 基于动态代理,无法增强 static 方法和 final 方法。如果需要增强这些方法,需要使用 AspectJ 的编译时或加载时织入。
将 static 方法改为实例方法
使用 AspectJ 进行编译时织入
重构代码结构,避免需要增强这些方法
8.2.9 注意内部方法调用 在同一个类中,方法内部调用其他方法时,被调用的方法不会被 AOP 增强,因为此时调用的是目标对象的方法,而不是代理对象的方法。
@Service
public class UserService {
@Transactional
public void updateUser (User user) {
validateUser(user);
}
@Loggable
public void validateUser (User user) {
}
}
注入自身的代理对象
重构代码,将方法拆分到不同的类中
使用 AspectJ 的编译时织入
8.2.10 注意多线程环境下的 AOP 在多线程环境下使用 AOP 时,需要注意线程安全问题,特别是当切面中使用共享状态时。
避免在切面中使用实例变量存储状态
如果必须使用共享状态,确保正确同步
考虑使用 ThreadLocal 存储线程本地状态
8.3 性能优化
8.3.1 优化切点表达式 精确的切点表达式可以减少需要代理的方法数量,提高 AOP 的性能。
使用具体的类名和方法名,而不是通配符
避免使用过于宽泛的表达式,如 execution(* *(..))
优先使用注解驱动的切点,如 @annotation(...)
execution(* com.example.service.*.*(..))
execution(public *com.example.service.UserService.getUserById(Long))
@annotation(com.example.annotation.Loggable)
8.3.2 减少切面的数量 过多的切面会增加代理对象的复杂性和方法调用的层级,影响性能。合理合并功能相似的切面。
将功能相关的横切关注点合并到一个切面中
使用不同的通知类型处理不同阶段的逻辑
避免创建过于细粒度的切面
8.3.3 避免在切面中执行耗时操作 在切面中执行耗时操作会直接影响目标方法的性能。应该避免在切面中进行网络请求、数据库操作等耗时操作。
网络请求(如调用外部 API)
数据库查询或更新
文件 I/O 操作
复杂的计算或序列化
使用异步处理耗时操作
将耗时操作放入消息队列
优化算法,减少计算复杂度
8.3.4 使用缓存减少重复计算 在切面中,如果有需要重复计算的逻辑,可以使用缓存来优化性能。
@Aspect
@Component
public class SecurityAspect {
private final LoadingCache<String, Boolean> permissionCache = CacheBuilder.newBuilder()
.maximumSize(1000 )
.expireAfterWrite(10 , TimeUnit.MINUTES)
.build(new CacheLoader <String, Boolean>() {
@Override
public Boolean load (String key) {
return calculatePermission(key);
}
});
@Before("@annotation(requirePermission)")
public void checkPermission (JoinPoint joinPoint, RequirePermission requirePermission) {
String userId = getCurrentUserId();
String permissionKey = userId + ":" + requirePermission.value();
try {
boolean hasPermission = permissionCache.get(permissionKey);
if (!hasPermission) {
throw new AccessDeniedException ("Access denied" );
}
} catch (ExecutionException e) {
throw new RuntimeException ("Failed to check permission" , e);
}
}
}
8.3.5 选择合适的 AOP 实现 根据需求选择合适的 AOP 实现,对于简单的需求,Spring AOP 已经足够;对于复杂的需求,可以考虑使用 AspectJ。
大多数 Spring 项目:使用 Spring AOP
需要增强非 Spring bean 或 final/static 方法:使用 AspectJ
性能敏感的场景:考虑 AspectJ 的编译时织入
8.4 常见陷阱和解决方案
8.4.1 代理对象类型转换问题 在使用 Spring AOP 时,由于使用了动态代理,可能会遇到代理对象不能转换为目标对象类型的问题。
@Service
public class UserService implements UserServiceInterface {
}
UserService userService = (UserService) applicationContext.getBean("userService" );
面向接口编程,注入接口而不是实现类
使用 AopContext.currentProxy() 获取当前代理对象
配置 Spring 使用 CGLIB 代理:@EnableAspectJAutoProxy(proxyTargetClass = true)
8.4.2 内部方法调用问题 如前所述,在同一个类中,方法内部调用其他方法时,被调用的方法不会被 AOP 增强。
@Service
public class UserService {
@Autowired
private ApplicationContext applicationContext;
private UserService selfProxy;
@PostConstruct
public void init () {
selfProxy = applicationContext.getBean(UserService.class);
}
public void updateUser (User user) {
selfProxy.validateUser(user);
}
@Loggable
public void validateUser (User user) {
}
}
使用 AopContext.currentProxy()
@Service
public class UserService {
public void updateUser (User user) {
UserService selfProxy = (UserService) AopContext.currentProxy();
selfProxy.validateUser(user);
}
@Loggable
public void validateUser (User user) {
}
}
8.4.3 异常被吞掉的问题 在环绕通知中,如果没有正确处理异常,可能会导致异常被吞掉,使问题难以排查。
@Around("logPointCut()")
public Object logAround (ProceedingJoinPoint pjp) {
try {
logger.info("Before method" );
return pjp.proceed();
} catch (Throwable t) {
logger.error("Error: {}" , t.getMessage());
return null ;
}
}
@Around("logPointCut()")
public Object logAround (ProceedingJoinPoint pjp) throws Throwable {
try {
logger.info("Before method" );
return pjp.proceed();
} catch (Throwable t) {
logger.error("Error: {}" , t.getMessage(), t);
throw t;
} finally {
logger.info("After method" );
}
}
8.4.4 事务不回滚的问题 在使用声明式事务时,可能会遇到事务不回滚的情况,这通常是因为异常类型不匹配或异常被吞掉。
抛出的是检查型异常,而默认情况下只有非检查型异常才会回滚
异常被捕获但没有重新抛出
方法不是 public 的
使用 rollbackFor 属性指定需要回滚的异常类型
@Transactional(rollbackFor = Exception.class)
public void saveUser (User user) {
}
确保异常被正确抛出,没有被吞掉
确保方法是 public 的
8.4.5 切面执行顺序问题 当多个切面应用于同一个连接点时,如果执行顺序不正确,可能会导致功能异常。
@Aspect
@Component
@Order(1)
public class FirstAspect {
}
@Aspect
@Component
@Order(2)
public class SecondAspect {
}
遵循合理的切面顺序:事务切面通常在最外层,然后是安全切面,最后是日志等监控切面
8.5 总结 AOP 是一种强大的编程范式,可以有效地分离横切关注点,提高代码的模块化和可维护性。在使用 AOP 时,我们应该:
遵循单一职责原则 :每个切面只关注一个横切关注点
合理使用不同类型的通知 :根据需求选择合适的通知类型
注意性能影响 :避免过度使用 AOP,优化切点表达式
了解局限性 :清楚 Spring AOP 的局限性,在必要时使用 AspectJ
编写单元测试 :为切面编写单元测试,确保行为正确
避免常见陷阱 :注意内部方法调用、异常处理等常见问题
通过遵循这些最佳实践和注意事项,我们可以更好地利用 AOP 来构建可维护、高性能的应用程序。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online