Appearance
Spring AOP Schema-based 配置详解 🎯
什么是 Schema-based AOP?
NOTE
Schema-based AOP 是 Spring 提供的基于 XML 配置的 AOP 实现方式,它使用 <aop:*>
命名空间标签来定义切面、切点和通知,是 @AspectJ 注解方式的 XML 替代方案。
核心设计哲学
Spring 设计 Schema-based AOP 的初衷是为那些偏好 XML 配置或需要在不支持注解的环境中使用 AOP 的开发者提供一个完整的解决方案。它与 @AspectJ 风格在功能上完全等价,但采用了声明式的 XML 配置方式。
基础配置结构
XML Schema 导入
首先需要在 Spring 配置文件中导入 AOP 命名空间:
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop // [!code highlight]
https://www.springframework.org/schema/aop/spring-aop.xsd"> // [!code highlight]
</beans>
基本配置结构
xml
<aop:config>
<!-- 切点定义 -->
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<!-- 切面定义 -->
<aop:aspect id="myAspect" ref="aspectBean">
<!-- 通知定义 -->
<aop:before pointcut-ref="businessService" method="doSomething"/>
</aop:aspect>
</aop:config>
<!-- 切面实现Bean -->
<bean id="aspectBean" class="com.example.MyAspect"/>
WARNING
<aop:config>
使用了 Spring 的自动代理机制,如果你已经使用了 BeanNameAutoProxyCreator
等显式自动代理,可能会产生冲突。建议只使用其中一种方式。
切面声明详解
1. 声明切面
切面是一个普通的 Java 对象,通过 XML 配置将其声明为切面:
kotlin
// 普通的 Kotlin 类,包含切面逻辑
class LoggingAspect {
fun logBefore() {
println("方法执行前的日志记录")
}
fun logAfter() {
println("方法执行后的日志记录")
}
fun logAround(joinPoint: ProceedingJoinPoint): Any? {
println("环绕通知开始")
val result = joinPoint.proceed()
println("环绕通知结束")
return result
}
}
xml
<aop:config>
<aop:aspect id="loggingAspect" ref="loggingBean">
<aop:before pointcut="execution(* com.example.service.*.*(..))"
method="logBefore"/>
<aop:after pointcut="execution(* com.example.service.*.*(..))"
method="logAfter"/>
</aop:aspect>
</aop:config>
<bean id="loggingBean" class="com.example.LoggingAspect"/>
2. 切点声明
全局切点定义
xml
<aop:config>
<!-- 可在多个切面间共享的命名切点 -->
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:pointcut id="dataAccessOperation"
expression="execution(* com.xyz.dao.*.*(..))"/>
</aop:config>
切面内切点定义
xml
<aop:config>
<aop:aspect id="myAspect" ref="aspectBean">
<!-- 切面内部的切点,仅该切面可用 -->
<aop:pointcut id="localPointcut"
expression="execution(* com.example.service.*.*(..))"/>
<aop:before pointcut-ref="localPointcut" method="beforeAdvice"/>
</aop:aspect>
</aop:config>
切点表达式语法优化
TIP
在 XML 中,可以使用 and
、or
、not
关键字替代 &&
、||
、!
,避免 XML 转义问题。
xml
<!-- 推荐写法 -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<!-- 避免使用(需要转义) -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
通知类型详解
1. Before 通知(前置通知)
在目标方法执行前运行:
kotlin
class SecurityAspect {
fun checkAccess() {
println("执行权限检查...")
// 权限验证逻辑
}
}
xml
<aop:config>
<aop:aspect id="securityAspect" ref="securityBean">
<aop:before pointcut="execution(* com.example.service.UserService.*(..))"
method="checkAccess"/>
</aop:aspect>
</aop:config>
<bean id="securityBean" class="com.example.SecurityAspect"/>
2. After Returning 通知(返回后通知)
在目标方法正常返回后执行,可以获取返回值:
kotlin
class AuditAspect {
fun auditSuccess(result: Any?) {
println("操作成功,返回值:$result")
// 审计日志记录
}
}
xml
<aop:config>
<aop:aspect id="auditAspect" ref="auditBean">
<aop:after-returning
pointcut="execution(* com.example.service.*.*(..))"
returning="result"
method="auditSuccess"/>
</aop:aspect>
</aop:config>
<bean id="auditBean" class="com.example.AuditAspect"/>
3. After Throwing 通知(异常通知)
在目标方法抛出异常时执行:
kotlin
class ExceptionHandlingAspect {
fun handleException(ex: Exception) {
println("捕获到异常:${ex.message}")
// 异常处理逻辑,如发送告警、记录错误日志等
}
fun handleDataAccessException(ex: DataAccessException) {
println("数据访问异常:${ex.message}")
// 特定异常类型的处理
}
}
xml
<aop:config>
<aop:aspect id="exceptionAspect" ref="exceptionBean">
<!-- 处理所有异常 -->
<aop:after-throwing
pointcut="execution(* com.example.service.*.*(..))"
throwing="ex"
method="handleException"/>
<!-- 处理特定类型异常 -->
<aop:after-throwing
pointcut="execution(* com.example.dao.*.*(..))"
throwing="ex"
method="handleDataAccessException"/>
</aop:aspect>
</aop:config>
<bean id="exceptionBean" class="com.example.ExceptionHandlingAspect"/>
4. After (Finally) 通知
无论方法如何退出都会执行:
xml
<aop:config>
<aop:aspect id="cleanupAspect" ref="cleanupBean">
<aop:after pointcut="execution(* com.example.service.*.*(..))"
method="cleanup"/>
</aop:aspect>
</aop:config>
5. Around 通知(环绕通知)
最强大的通知类型,可以完全控制方法的执行:
kotlin
class PerformanceAspect {
fun measurePerformance(joinPoint: ProceedingJoinPoint): Any? {
val startTime = System.currentTimeMillis()
return try {
println("开始执行方法:${joinPoint.signature.name}")
val result = joinPoint.proceed()
result
} finally {
val endTime = System.currentTimeMillis()
println("方法执行耗时:${endTime - startTime}ms")
}
}
}
xml
<aop:config>
<aop:aspect id="performanceAspect" ref="performanceBean">
<aop:around pointcut="execution(* com.example.service.*.*(..))"
method="measurePerformance"/>
</aop:aspect>
</aop:config>
<bean id="performanceBean" class="com.example.PerformanceAspect"/>
IMPORTANT
Around 通知必须调用 ProceedingJoinPoint.proceed()
来执行目标方法,否则目标方法不会被执行。
实际业务场景示例
场景:分布式锁重试机制
在微服务环境中,经常需要处理并发冲突和重试逻辑:
kotlin
@Component
class RetryAspect : Ordered {
private val maxRetries = 3
private val order = 1
fun executeWithRetry(joinPoint: ProceedingJoinPoint): Any? {
var attempts = 0
var lastException: PessimisticLockingFailureException? = null
do {
attempts++
try {
println("第 $attempts 次尝试执行:${joinPoint.signature.name}")
return joinPoint.proceed()
} catch (ex: PessimisticLockingFailureException) {
lastException = ex
println("执行失败,准备重试...")
Thread.sleep(100 * attempts) // 递增延迟
}
} while (attempts <= maxRetries)
throw lastException!!
}
override fun getOrder(): Int = order
}
xml
<aop:config>
<aop:aspect id="retryAspect" ref="retryBean" order="1">
<aop:pointcut id="businessOperations"
expression="execution(* com.example.service.*.*(..))
and @annotation(com.example.Retryable)"/>
<aop:around pointcut-ref="businessOperations"
method="executeWithRetry"/>
</aop:aspect>
</aop:config>
<bean id="retryBean" class="com.example.RetryAspect">
<property name="maxRetries" value="3"/>
<property name="order" value="1"/>
</bean>
kotlin
@Service
class OrderService {
@Retryable // 自定义注解标记需要重试的方法
fun processOrder(orderId: String): OrderResult {
// 可能因为并发冲突失败的业务逻辑
return orderRepository.updateOrderStatus(orderId, OrderStatus.PROCESSED)
}
}
场景:方法参数验证和日志记录
kotlin
class ValidationAspect {
fun validateAndLog(joinPoint: ProceedingJoinPoint,
userId: String,
operation: String): Any? {
// 参数验证
if (userId.isBlank()) {
throw IllegalArgumentException("用户ID不能为空")
}
// 记录操作日志
println("用户 $userId 正在执行操作:$operation")
val startTime = System.currentTimeMillis()
return try {
val result = joinPoint.proceed()
println("操作成功完成,耗时:${System.currentTimeMillis() - startTime}ms")
result
} catch (ex: Exception) {
println("操作失败:${ex.message}")
throw ex
}
}
}
xml
<aop:config>
<aop:aspect id="validationAspect" ref="validationBean">
<aop:pointcut id="userOperations"
expression="execution(* com.example.service.UserService.*(String, ..))
and args(userId, ..)"/>
<aop:around pointcut-ref="userOperations"
method="validateAndLog"
arg-names="userId,operation"/>
</aop:aspect>
</aop:config>
<bean id="validationBean" class="com.example.ValidationAspect"/>
Introduction(引入)功能
Introduction 允许为现有类添加新的接口实现,这是一个非常强大的功能:
kotlin
interface UsageTracked {
fun incrementUseCount()
fun getUseCount(): Int
}
class DefaultUsageTracked : UsageTracked {
private var useCount = 0
override fun incrementUseCount() {
useCount++
}
override fun getUseCount(): Int = useCount
}
kotlin
class UsageTrackingAspect {
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
println("使用次数已记录,当前总计:${usageTracked.getUseCount()}")
}
}
xml
<aop:config>
<aop:aspect id="usageTracker" ref="usageTrackingBean">
<!-- 为所有service包下的类添加UsageTracked接口 -->
<aop:declare-parents
types-matching="com.example.service.*+"
implement-interface="com.example.UsageTracked"
default-impl="com.example.DefaultUsageTracked"/>
<!-- 在方法执行前记录使用情况 -->
<aop:before
pointcut="execution(* com.example.service.*.*(..)) and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
</aop:config>
<bean id="usageTrackingBean" class="com.example.UsageTrackingAspect"/>
kotlin
@Service
class UserService {
fun findUser(id: String): User {
// 业务逻辑
return userRepository.findById(id)
}
}
// 使用时可以转换为UsageTracked接口
val userService = applicationContext.getBean("userService", UserService::class.java)
val usageTracked = userService as UsageTracked
println("当前使用次数:${usageTracked.getUseCount()}")
通知执行顺序
当多个通知需要在同一个连接点执行时,可以通过 order
属性控制执行顺序:
xml
<aop:config>
<!-- 高优先级切面(order值越小优先级越高) -->
<aop:aspect id="securityAspect" ref="securityBean" order="1">
<aop:before pointcut="execution(* com.example.service.*.*(..))"
method="checkSecurity"/>
</aop:aspect>
<!-- 低优先级切面 -->
<aop:aspect id="loggingAspect" ref="loggingBean" order="2">
<aop:before pointcut="execution(* com.example.service.*.*(..))"
method="logExecution"/>
</aop:aspect>
</aop:config>
NOTE
在同一个 <aop:aspect>
元素内,通知的执行顺序由它们在 XML 中的声明顺序决定。
Schema-based vs @AspectJ 对比
特性 | Schema-based | @AspectJ |
---|---|---|
配置方式 | XML配置 | 注解配置 |
学习曲线 | 相对平缓 | 需要了解AspectJ语法 |
IDE支持 | XML提示和验证 | 更好的代码提示 |
重构友好性 | 较差 | 更好 |
运行时修改 | 支持 | 不支持 |
切点复用 | 有限制 | 更灵活 |
最佳实践建议
1. 选择合适的通知类型
TIP
始终使用能满足需求的最简单的通知类型。例如,如果只需要在方法执行前做某些操作,使用 Before 通知而不是 Around 通知。
2. 合理组织切点表达式
xml
<!-- 好的做法:使用命名切点提高可读性 -->
<aop:config>
<aop:pointcut id="serviceLayer"
expression="execution(* com.example.service.*.*(..))"/>
<aop:pointcut id="publicMethods"
expression="execution(public * *(..))"/>
<aop:pointcut id="servicePublicMethods"
expression="serviceLayer and publicMethods"/>
</aop:config>
3. 避免过度使用AOP
WARNING
不要为了使用AOP而使用AOP。只有当横切关注点确实跨越多个模块时,才考虑使用AOP。
4. 性能考虑
kotlin
// 在高频调用的方法上使用AOP时要特别注意性能
class PerformanceCriticalAspect {
fun lightweightAdvice(joinPoint: ProceedingJoinPoint): Any? {
// 尽量减少通知中的处理逻辑
val result = joinPoint.proceed()
// 简单的日志记录,避免复杂计算
return result
}
}
总结
Schema-based AOP 为 Spring 应用提供了强大而灵活的横切关注点处理能力。它特别适合以下场景:
✅ 适用场景:
- 需要XML配置的企业环境
- 团队更熟悉XML配置方式
- 需要运行时动态修改AOP配置
- 与其他XML配置集成
❌ 不适用场景:
- 追求简洁代码的现代Spring应用
- 需要复杂切点表达式组合的场景
- 重构频繁的敏捷开发环境
通过合理使用 Schema-based AOP,可以有效地将横切关注点从业务逻辑中分离出来,提高代码的模块化程度和可维护性。记住,AOP 是一个强大的工具,但要谨慎使用,避免过度设计。