Skip to content

基于XML的AOP配置

Spring AOP提供了两种配置方式:基于XML的配置和基于注解的配置。本文将介绍如何使用XML配置方式实现AOP功能。

XML配置的基本结构

Spring AOP的XML配置主要使用<aop:config>元素及其子元素来定义切面、切点和通知。基本的XML配置结构如下:

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
                          http://www.springframework.org/schema/beans/spring-beans.xsd
                          http://www.springframework.org/schema/aop
                          http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 目标bean定义 -->
    <bean id="userService" class="com.example.service.UserServiceImpl"/>
    
    <!-- 切面bean定义 -->
    <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
    
    <!-- AOP配置 -->
    <aop:config>
        <!-- 切面定义 -->
        <aop:aspect ref="loggingAspect">
            <!-- 切点定义 -->
            <aop:pointcut id="serviceMethod" 
                         expression="execution(* com.example.service.*.*(..))"/>
            
            <!-- 通知定义 -->
            <aop:before pointcut-ref="serviceMethod" method="beforeAdvice"/>
            <aop:after pointcut-ref="serviceMethod" method="afterAdvice"/>
            <!-- 其他通知类型... -->
        </aop:aspect>
    </aop:config>
</beans>

准备工作

在开始XML配置之前,我们需要:

  1. 确保Spring AOP依赖已添加到项目中
  2. 创建目标类和切面类

添加依赖

在Maven项目中,确保添加以下依赖:

xml
<dependencies>
    <!-- Spring Core & Context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- AspectJ runtime, required for AOP -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.9.1</version>
    </dependency>
    
    <!-- AspectJ weaver -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.9.1</version>
    </dependency>
</dependencies>

创建目标类

创建一个简单的服务接口和实现类,作为AOP的目标:

java
// UserService.java
package com.example.service;

public interface UserService {
    void addUser(String username);
    void updateUser(String username);
    String getUser(String userId);
}

// UserServiceImpl.java
package com.example.service;

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("Adding user: " + username);
        // 业务逻辑...
    }
    
    @Override
    public void updateUser(String username) {
        System.out.println("Updating user: " + username);
        // 业务逻辑...
    }
    
    @Override
    public String getUser(String userId) {
        System.out.println("Getting user with ID: " + userId);
        // 业务逻辑...
        return "User:" + userId;
    }
}

创建切面类

创建一个切面类,包含各种通知方法:

java
// LoggingAspect.java
package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class LoggingAspect {
    // 前置通知
    public void beforeAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Before method: " + methodName);
    }
    
    // 后置通知
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("After method: " + methodName);
    }
    
    // 返回通知
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " returned: " + result);
    }
    
    // 异常通知
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Method " + methodName + " threw exception: " + ex.getMessage());
    }
    
    // 环绕通知
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String methodName = proceedingJoinPoint.getSignature().getName();
        System.out.println("Around before method: " + methodName);
        
        try {
            Object result = proceedingJoinPoint.proceed();
            System.out.println("Around after method: " + methodName);
            return result;
        } catch (Exception e) {
            System.out.println("Around after throwing: " + e.getMessage());
            throw e;
        }
    }
}

XML配置详解

下面详细介绍XML配置的各个部分:

1. 切点定义

切点定义使用<aop:pointcut>元素,可以定义在<aop:config>级别(可被多个切面共享)或<aop:aspect>级别(仅适用于当前切面):

xml
<!-- 全局切点定义 -->
<aop:config>
    <aop:pointcut id="allServiceMethods" 
                 expression="execution(* com.example.service.*.*(..))"/>
    
    <aop:aspect ref="loggingAspect">
        <!-- 使用全局切点 -->
        <aop:before pointcut-ref="allServiceMethods" method="beforeAdvice"/>
        
        <!-- 切面级别切点定义 -->
        <aop:pointcut id="userServiceMethods" 
                     expression="execution(* com.example.service.UserService.*(..))"/>
        <aop:after pointcut-ref="userServiceMethods" method="afterAdvice"/>
    </aop:aspect>
</aop:config>

2. 通知类型

Spring AOP支持五种通知类型,对应于XML配置中的不同元素:

前置通知(Before Advice)

在连接点之前执行,但不能阻止连接点的执行(除非抛出异常):

xml
<aop:before pointcut-ref="serviceMethod" method="beforeAdvice"/>

或者内联定义切点:

xml
<aop:before pointcut="execution(* com.example.service.*.*(..))" method="beforeAdvice"/>

后置通知(After Advice)

在连接点完成后执行,无论方法是正常退出还是抛出异常:

xml
<aop:after pointcut-ref="serviceMethod" method="afterAdvice"/>

返回通知(After Returning Advice)

在连接点正常完成后执行,如果方法抛出异常则不执行:

xml
<aop:after-returning pointcut-ref="serviceMethod" 
                     method="afterReturningAdvice" 
                     returning="result"/>

returning属性指定通知方法中接收返回值的参数名。

异常通知(After Throwing Advice)

在连接点抛出异常时执行:

xml
<aop:after-throwing pointcut-ref="serviceMethod" 
                   method="afterThrowingAdvice" 
                   throwing="ex"/>

throwing属性指定通知方法中接收异常的参数名。

环绕通知(Around Advice)

环绕通知包围连接点的执行,可以在方法调用前后执行自定义行为:

xml
<aop:around pointcut-ref="serviceMethod" method="aroundAdvice"/>

3. 完整的XML配置示例

以下是一个完整的XML配置示例,包含所有类型的通知:

xml
<?xml version="1.0" encoding="UTF-8"?>
<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
                          http://www.springframework.org/schema/beans/spring-beans.xsd
                          http://www.springframework.org/schema/aop
                          http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 目标bean定义 -->
    <bean id="userService" class="com.example.service.UserServiceImpl"/>
    
    <!-- 切面bean定义 -->
    <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
    
    <!-- AOP配置 -->
    <aop:config>
        <!-- 全局切点定义 -->
        <aop:pointcut id="allServiceMethods" 
                     expression="execution(* com.example.service.*.*(..))"/>
        
        <!-- 切面定义 -->
        <aop:aspect id="loggingAspect" ref="loggingAspect">
            <!-- 前置通知 -->
            <aop:before pointcut-ref="allServiceMethods" method="beforeAdvice"/>
            
            <!-- 后置通知 -->
            <aop:after pointcut-ref="allServiceMethods" method="afterAdvice"/>
            
            <!-- 返回通知 -->
            <aop:after-returning pointcut-ref="allServiceMethods" 
                               method="afterReturningAdvice" 
                               returning="result"/>
            
            <!-- 异常通知 -->
            <aop:after-throwing pointcut-ref="allServiceMethods" 
                              method="afterThrowingAdvice" 
                              throwing="ex"/>
            
            <!-- 环绕通知 -->
            <aop:around pointcut-ref="allServiceMethods" method="aroundAdvice"/>
        </aop:aspect>
    </aop:config>
</beans>

高级XML配置

1. 引入(Introduction)

引入允许向现有的类添加新方法或属性,使其实现额外的接口:

首先,定义一个接口和默认实现:

java
// Monitorable.java
package com.example.monitoring;

public interface Monitorable {
    void startMonitoring();
    void stopMonitoring();
}

// DefaultMonitorable.java
package com.example.monitoring;

public class DefaultMonitorable implements Monitorable {
    @Override
    public void startMonitoring() {
        System.out.println("Starting monitoring...");
    }
    
    @Override
    public void stopMonitoring() {
        System.out.println("Stopping monitoring...");
    }
}

然后,在XML中配置引入:

xml
<aop:config>
    <aop:aspect ref="monitoringAspect">
        <aop:declare-parents
            types-matching="com.example.service.*Service+"
            implement-interface="com.example.monitoring.Monitorable"
            default-impl="com.example.monitoring.DefaultMonitorable"/>
    </aop:aspect>
</aop:config>

这样,所有com.example.service包中名称以Service结尾的类都会实现Monitorable接口。

2. 切面顺序(Aspect Ordering)

当多个切面都需要在同一个连接点执行通知时,可以使用order属性指定切面的优先级:

xml
<aop:config>
    <aop:aspect id="securityAspect" ref="securityAspect" order="1">
        <!-- 安全相关的通知 -->
    </aop:aspect>
    
    <aop:aspect id="loggingAspect" ref="loggingAspect" order="2">
        <!-- 日志相关的通知 -->
    </aop:aspect>
</aop:config>

数值越小,优先级越高。在上面的例子中,securityAspect的前置通知会在loggingAspect的前置通知之前执行,而loggingAspect的后置通知会在securityAspect的后置通知之前执行。

3. 通知参数

可以将连接点的参数传递给通知方法:

xml
<aop:config>
    <aop:aspect ref="securityAspect">
        <aop:pointcut id="userMethods" 
                     expression="execution(* com.example.service.UserService.*(String)) and args(username)"/>
        
        <aop:before pointcut-ref="userMethods" method="checkAccess"/>
    </aop:aspect>
</aop:config>

对应的切面类:

java
public class SecurityAspect {
    public void checkAccess(String username) {
        System.out.println("Checking access for user: " + username);
        // 权限检查逻辑...
    }
}

4. 代理模式

Spring AOP默认会根据目标类是否实现接口自动选择使用JDK动态代理还是CGLIB代理。可以通过proxy-target-class属性强制使用CGLIB代理:

xml
<aop:config proxy-target-class="true">
    <!-- AOP配置 -->
</aop:config>

或者在全局范围内配置:

xml
<aop:aspectj-autoproxy proxy-target-class="true"/>

5. Advisor支持

除了使用<aop:aspect>定义切面外,Spring还支持使用<aop:advisor>元素配置更底层的advisors:

xml
<bean id="transactionManager" 
     class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionAdvice" 
     class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="update*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

<aop:config>
    <aop:pointcut id="serviceMethods" 
                 expression="execution(* com.example.service.*.*(..))"/>
    
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="serviceMethods"/>
</aop:config>

这通常用于配置事务管理等底层服务。

使用XML配置的AOP

1. 加载XML配置

使用ClassPathXmlApplicationContext加载XML配置:

java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.example.service.UserService;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = 
            new ClassPathXmlApplicationContext("aop-config.xml");
        
        UserService userService = context.getBean("userService", UserService.class);
        
        // 测试AOP功能
        userService.addUser("john");
        System.out.println("----------");
        
        userService.updateUser("john");
        System.out.println("----------");
        
        String user = userService.getUser("123");
        System.out.println("Returned: " + user);
        System.out.println("----------");
        
        try {
            // 触发异常通知
            userService.getUser(null);
        } catch (Exception e) {
            System.out.println("Caught exception: " + e.getMessage());
        }
    }
}

2. 测试引入功能

测试之前配置的引入功能:

java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.example.service.UserService;
import com.example.monitoring.Monitorable;

public class IntroductionTest {
    public static void main(String[] args) {
        ApplicationContext context = 
            new ClassPathXmlApplicationContext("aop-config.xml");
        
        UserService userService = context.getBean("userService", UserService.class);
        
        // 将userService转型为Monitorable接口
        Monitorable monitorable = (Monitorable) userService;
        
        // 使用引入的方法
        monitorable.startMonitoring();
        userService.addUser("john");
        monitorable.stopMonitoring();
    }
}

XML配置与注解配置对比

特性XML配置注解配置
配置位置集中在XML文件中分散在各个类中
与业务代码分离完全分离耦合在一起
适用场景第三方类、需要完全分离关注点自己开发的类
灵活性不需要修改源代码即可更改配置需要修改源代码才能更改配置
可读性配置集中但可能较长就近配置,但需要了解多个类
类型安全不是类型安全的类型安全

最佳实践

  1. 选择合适的配置方式:对于自己开发的代码,注解配置可能更简洁;对于第三方库或需要完全分离的场景,XML配置更合适
  2. 避免过度使用AOP:AOP是一个强大的工具,但过度使用会增加复杂性和调试难度
  3. 保持切面简单:切面应该只关注单一的横切关注点,避免在一个切面中混合多种功能
  4. 合理组织XML配置:对于复杂应用,可以将AOP配置分散到多个XML文件中,保持每个文件的简洁和专注
  5. 使用有意义的ID:为切面、切点和通知使用有意义的ID,以提高可读性
  6. 利用Schema命名空间:使用Spring提供的Schema命名空间简化配置

总结

基于XML的AOP配置提供了一种声明式的方法来定义切面、切点和通知,与业务代码完全分离。它适用于需要将横切关注点与业务逻辑严格分离的场景,以及对第三方类应用AOP的情况。

通过本文的介绍,我们了解了XML配置的基本结构、各种通知类型的配置方式,以及高级配置选项如引入、切面顺序等。无论是选择XML配置还是注解配置,了解它们的优缺点和适用场景有助于在实际项目中做出合理的决策。