Appearance
基于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配置之前,我们需要:
- 确保Spring AOP依赖已添加到项目中
- 创建目标类和切面类
添加依赖
在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文件中 | 分散在各个类中 |
与业务代码分离 | 完全分离 | 耦合在一起 |
适用场景 | 第三方类、需要完全分离关注点 | 自己开发的类 |
灵活性 | 不需要修改源代码即可更改配置 | 需要修改源代码才能更改配置 |
可读性 | 配置集中但可能较长 | 就近配置,但需要了解多个类 |
类型安全 | 不是类型安全的 | 类型安全 |
最佳实践
- 选择合适的配置方式:对于自己开发的代码,注解配置可能更简洁;对于第三方库或需要完全分离的场景,XML配置更合适
- 避免过度使用AOP:AOP是一个强大的工具,但过度使用会增加复杂性和调试难度
- 保持切面简单:切面应该只关注单一的横切关注点,避免在一个切面中混合多种功能
- 合理组织XML配置:对于复杂应用,可以将AOP配置分散到多个XML文件中,保持每个文件的简洁和专注
- 使用有意义的ID:为切面、切点和通知使用有意义的ID,以提高可读性
- 利用Schema命名空间:使用Spring提供的Schema命名空间简化配置
总结
基于XML的AOP配置提供了一种声明式的方法来定义切面、切点和通知,与业务代码完全分离。它适用于需要将横切关注点与业务逻辑严格分离的场景,以及对第三方类应用AOP的情况。
通过本文的介绍,我们了解了XML配置的基本结构、各种通知类型的配置方式,以及高级配置选项如引入、切面顺序等。无论是选择XML配置还是注解配置,了解它们的优缺点和适用场景有助于在实际项目中做出合理的决策。