Skip to content

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 中,可以使用 andornot 关键字替代 &&||!,避免 XML 转义问题。

xml
<!-- 推荐写法 -->
<aop:pointcut id="servicePointcut" 
              expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>

<!-- 避免使用(需要转义) -->
<aop:pointcut id="servicePointcut" 
              expression="execution(* com.xyz.service.*.*(..)) &amp;&amp; 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 是一个强大的工具,但要谨慎使用,避免过度设计。