Appearance
AOP核心概念
面向切面编程(AOP)是一种编程范式,通过将横切关注点(如日志记录、事务管理、安全性等)与核心业务逻辑分离,提高代码的模块化程度。本文将深入介绍AOP的核心概念及术语。
AOP术语
1. 横切关注点(Cross-cutting Concerns)
横切关注点是指那些影响应用程序多个部分的功能,这些功能与业务逻辑无关,但却贯穿整个应用。典型的横切关注点包括:
- 日志记录
- 性能监控
- 事务管理
- 安全控制
- 错误处理
- 缓存
这些功能如果直接融入业务代码中,会导致代码重复、难以维护和业务逻辑不清晰。AOP提供了一种机制,使这些关注点可以被模块化并应用到需要的地方。
2. 切面(Aspect)
切面是AOP中的核心概念,它是横切关注点的模块化单元。切面包含了通知(Advice)和切点(Pointcut)的结合,定义了"什么时候"、"在哪里"执行"什么功能"。
在Spring中,切面可以是任何带有@Aspect
注解的普通类:
java
@Aspect
@Component
public class LoggingAspect {
// 切面实现
}
3. 连接点(Join Point)
连接点是程序执行过程中的特定点,在这些点上可以插入切面的功能。连接点可以是方法调用、方法执行、字段访问、异常处理等。
在Spring AOP中,连接点总是表示方法的执行,即方法执行时的点。
4. 切点(Pointcut)
切点是匹配连接点的表达式,它定义了在哪些连接点上应用通知。切点表达式有助于准确选择我们想要应用横切关注点的连接点。
Spring AOP使用AspectJ的切点表达式语言来定义切点:
java
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
这个切点表达式匹配com.example.service
包中所有类的所有方法,无论返回类型、方法名和参数如何。
5. 通知(Advice)
通知是切面在特定连接点执行的代码。通知定义了切面"做什么"以及"何时做"。Spring AOP提供了五种类型的通知:
前置通知(Before Advice)
在连接点之前执行,但不能阻止连接点的执行(除非抛出异常)。
java
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before executing: " + joinPoint.getSignature().getName());
}
后置通知(After Advice)
在连接点完成后执行,无论方法是正常退出还是抛出异常。类似于finally块。
java
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After executing: " + joinPoint.getSignature().getName());
}
返回通知(After Returning Advice)
在连接点正常完成后执行,如果方法抛出异常则不执行。
java
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
System.out.println("Method returned: " + result);
}
异常通知(After Throwing Advice)
在连接点抛出异常时执行。
java
@AfterThrowing(pointcut = "serviceMethods()", throwing = "error")
public void logError(JoinPoint joinPoint, Throwable error) {
System.out.println("Method threw exception: " + error.getMessage());
}
环绕通知(Around Advice)
环绕通知包围连接点的执行,这是最强大的通知类型,可以在方法调用前后执行自定义行为,甚至可以决定是否执行原方法。
java
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("Method executed in " + (end - start) + "ms");
return result;
} catch (Exception e) {
System.out.println("Method execution failed: " + e.getMessage());
throw e;
}
}
6. 引入(Introduction)
引入允许我们向现有的类添加新方法或属性。这使得我们可以在不修改目标类代码的情况下,让这个类实现一个接口或添加新功能。
java
@Aspect
public class UsageTrackingAspect {
@DeclareParents(value = "com.example.service.*+", defaultImpl = DefaultUsageTracked.class)
public static UsageTracked usageTracked;
@Before("execution(* com.example.service.*.*(..)) && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUsageCount();
}
}
这个例子使所有com.example.service
包中的类都实现了UsageTracked
接口。
7. 织入(Weaving)
织入是将切面应用到目标对象以创建新的代理对象的过程。织入可以在以下几个阶段进行:
- 编译期织入:在编译时增强源代码(需要特殊编译器)
- 类加载期织入:在类加载时增强字节码(需要特殊的类加载器)
- 运行期织入:在运行时使用代理模式创建目标类的代理对象
Spring AOP使用运行期织入,通过动态代理(JDK动态代理或CGLIB)在运行时创建目标对象的代理。
8. 目标对象(Target Object)
目标对象是被一个或多个切面通知的对象,也被称为"通知对象"。由于Spring AOP使用运行时代理,所以目标对象总是一个被代理的对象。
9. AOP代理(AOP Proxy)
AOP代理是AOP框架创建的对象,用来实现切面约定(通知方法执行等)。在Spring AOP中,AOP代理是JDK动态代理或CGLIB代理。
Spring AOP的代理机制
Spring AOP使用动态代理技术来实现切面功能。主要有两种代理机制:
JDK动态代理
当目标对象实现了接口时,Spring AOP使用JDK动态代理。JDK动态代理通过java.lang.reflect.Proxy
创建接口的代理实例,并将方法调用分派给指定的调用处理器(InvocationHandler)。
java
public interface UserService {
User findById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User findById(Long id) {
// 实现
}
}
在这个例子中,Spring会为UserServiceImpl
创建一个实现UserService
接口的JDK动态代理。
CGLIB代理
当目标对象没有实现任何接口时,Spring AOP使用CGLIB来创建子类代理。CGLIB通过继承目标类并重写方法来创建代理。
java
@Service
public class ProductService {
public Product findById(Long id) {
// 实现
}
}
在这个例子中,由于ProductService
没有实现任何接口,Spring会使用CGLIB创建ProductService
的子类代理。
代理机制的区别
特性 | JDK动态代理 | CGLIB代理 |
---|---|---|
前提条件 | 目标类必须实现接口 | 无需实现接口,但类不能是final |
代理原理 | 基于接口,创建接口实现 | 基于继承,创建子类 |
方法限制 | 只能代理接口方法 | 能代理任何非final方法 |
性能 | JDK 8后性能较好 | 在老版本JDK中可能性能更好 |
Spring AOP会根据目标类是否实现接口自动选择使用JDK动态代理还是CGLIB代理。可以通过配置spring.aop.proxy-target-class
属性为true
强制使用CGLIB代理。
切点表达式
Spring AOP使用AspectJ的切点表达式语言来选择连接点。下面是一些常用的切点表达式:
execution表达式
execution表达式是最常用的切点表达式,用于匹配方法执行:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
modifiers-pattern
:方法的访问修饰符(如public、protected)return-type-pattern
:方法的返回类型declaring-type-pattern
:方法所在的类method-name-pattern
:方法名param-pattern
:方法参数throws-pattern
:方法抛出的异常
例如:
java
// 匹配所有public方法
execution(public * *(..))
// 匹配所有以find开头的方法
execution(* find*(..))
// 匹配UserService中的所有方法
execution(* com.example.service.UserService.*(..))
// 匹配service包中所有类的所有方法
execution(* com.example.service.*.*(..))
// 匹配service包及其子包中所有类的所有方法
execution(* com.example.service..*.*(..))
// 匹配接收一个参数且是Long类型的方法
execution(* *(Long))
// 匹配接收两个参数的方法,第一个是String,第二个是任意类型
execution(* *(String, *))
within表达式
within表达式用于匹配指定类型内的所有方法:
java
// 匹配UserService类中的所有方法
within(com.example.service.UserService)
// 匹配service包中所有类的所有方法
within(com.example.service.*)
// 匹配service包及其子包中所有类的所有方法
within(com.example.service..*)
this和target表达式
this
:匹配代理对象是指定类型的实例的连接点target
:匹配目标对象是指定类型的实例的连接点
java
// 匹配代理对象是UserService的所有方法
this(com.example.service.UserService)
// 匹配目标对象是UserService的所有方法
target(com.example.service.UserService)
args表达式
args表达式用于匹配参数类型符合指定模式的方法:
java
// 匹配接收一个Long参数的方法
args(Long)
// 匹配第一个参数是String类型的方法
args(String, ..)
@target表达式
匹配目标对象带有指定注解的类的所有方法:
java
// 匹配带有@Service注解的类中的所有方法
@target(org.springframework.stereotype.Service)
@annotation表达式
匹配带有指定注解的方法:
java
// 匹配带有@Transactional注解的方法
@annotation(org.springframework.transaction.annotation.Transactional)
bean表达式
匹配指定bean的所有方法:
java
// 匹配ID为'userService'的bean的所有方法
bean(userService)
// 匹配所有名称以'Service'结尾的bean的所有方法
bean(*Service)
切点组合
多个切点表达式可以通过&&
、||
和!
组合起来形成更复杂的表达式:
java
// 匹配UserService中的所有public方法
execution(public * com.example.service.UserService.*(..))
// 匹配所有带有@Transactional注解且在service包中的方法
execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)
// 匹配所有带有@Transactional注解但不在UserService中的方法
@annotation(org.springframework.transaction.annotation.Transactional) && !within(com.example.service.UserService)
JoinPoint接口
通知方法可以声明一个类型为JoinPoint
的参数作为第一个参数(环绕通知使用ProceedingJoinPoint
,它是JoinPoint
的子接口)。JoinPoint
接口提供了有关连接点的有用信息:
java
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
// 获取方法签名
String methodName = joinPoint.getSignature().getName();
// 获取目标对象
Object target = joinPoint.getTarget();
// 获取参数
Object[] args = joinPoint.getArgs();
System.out.println("Executing method: " + methodName);
System.out.println("On object: " + target.getClass().getName());
System.out.println("With arguments: " + Arrays.toString(args));
}
ProceedingJoinPoint
还提供了proceed()
方法,用于环绕通知中执行目标方法:
java
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
System.out.println("Before method: " + methodName);
try {
// 执行目标方法
Object result = joinPoint.proceed();
System.out.println("After method: " + methodName);
return result;
} catch (Exception e) {
System.out.println("Method " + methodName + " threw exception: " + e.getMessage());
throw e;
}
}
通知顺序
当多个切面都需要在同一个连接点执行通知时,通知的执行顺序变得很重要。Spring AOP使用以下规则确定通知的执行顺序:
- 具有高优先级的切面的前置通知先于具有低优先级的切面的前置通知执行。
- 具有低优先级的切面的后置、返回或异常通知先于具有高优先级的切面的后置、返回或异常通知执行。
- 当两个切面都定义了环绕通知,优先级高的切面的环绕通知会包围优先级低的切面的环绕通知。
可以通过实现org.springframework.core.Ordered
接口或使用@Order
注解来指定切面的优先级:
java
@Aspect
@Component
@Order(1) // 较高的优先级
public class SecurityAspect {
// 实现
}
@Aspect
@Component
@Order(2) // 较低的优先级
public class LoggingAspect {
// 实现
}
总结
AOP的核心概念包括横切关注点、切面、连接点、切点、通知、引入和织入。了解这些概念有助于正确使用AOP解决横切关注点问题,提高代码的模块化程度。
Spring AOP基于代理实现,主要使用JDK动态代理和CGLIB两种代理机制。它提供了声明式的切面定义方式,结合AspectJ的切点表达式语言,可以精确地选择需要应用通知的连接点。
通过深入理解AOP的核心概念和Spring AOP的实现机制,开发者可以更有效地利用AOP解决实际问题,例如日志记录、事务管理、安全控制等横切关注点。