Spring AOP原理与实现:代理模式与通知类型详解
Spring AOP概述与核心概念
Spring框架通过面向切面编程(AOP)技术,将与业务逻辑无关但被多个业务模块共同使用的功能代码进行封装,从而提升代码复用性并降低模块间的耦合度。
在Spring AOP中,系统功能被划分为两个主要部分:核心关注点和横切关注点。核心关注点代表主要的业务处理流程,而横切关注点则是被多个核心关注点共享的功能模块,如安全验证、日志记录、事务管理等。横切关注点的典型特征是它们会在核心关注点的多个位置执行,且执行逻辑基本相似。AOP的核心思想是将这两类关注点解耦,使系统结构更加清晰。
两种代理实现机制
Spring框架提供了两种创建代理对象的方式:基于JDK的动态代理和基于CGLib的动态代理。具体采用哪种方式由AopProxyFactory根据AdvisedSupport对象的配置来决定。Spring的默认策略是:如果目标类实现了接口,则使用JDK动态代理;否则,使用CGLib动态代理。
JDK动态代理实现原理
JDK动态代理主要依靠java.lang.reflect包中的Proxy类和InvocationHandler接口。InvocationHandler是一个接口,通过实现不同的类可以定义各种横切逻辑,并利用反射机制调用目标类的方法,将横切逻辑与业务逻辑动态结合。Proxy类则利用InvocationHandler动态创建符合特定接口的实例,生成目标对象的代理。
JDK动态代理的关键实现原理如下:
public class DynamicProxy implements InvocationHandler {
// 目标对象
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置处理
System.out.println("方法执行前处理");
// 调用目标方法
Object result = method.invoke(target, args);
// 后置处理
System.out.println("方法执行后处理");
return result;
}
// 创建代理对象
public static <T> T getProxy(Class<T> clazz, Object target) {
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(),
clazz.getInterfaces(),
new DynamicProxy(target)
);
}
}
CGLib动态代理实现原理
CGLib(Code Generation Library)是一个高性能的代码生成库,能够在运行时扩展Java类和实现Java接口。CGLib通过字节码处理框架ASM来实现其功能,通过转换字节码来生成新的类。
与JDK动态代理的主要区别在于:JDK只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,只能通过CGLib创建动态代理。
五种通知类型详解
Spring AOP支持五种类型的通知,用于在目标方法的不同执行阶段插入横切逻辑:
- 前置通知(Before):在目标方法执行前调用
- 后置通知(AfterReturning):在目标方法正常执行后调用
- 异常通知(AfterThrowing):在目标方法抛出异常时调用
- 最终通知(After):在目标方法执行后调用(无论是否异常)
- 环绕通知(Around):包围目标方法的执行,可以在方法执行前后自定义逻辑
实际应用示例
下面是一个使用Spring AOP实现日志记录的示例:
@Aspect // 声明切面
public class LoggingAspect {
// 定义切入点:拦截com.example.service包下的所有public方法
@Pointcut("execution(public * com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知:方法执行前记录日志
@Before("servicePointcut()")
public void logBefore(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("执行方法: " + className + "." + methodName + "() - 开始执行");
}
// 后置通知:方法正常执行后记录日志
@AfterReturning(pointcut = "servicePointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("执行方法: " + className + "." + methodName + "() - 执行成功,返回值: " + result);
}
// 异常通知:方法抛出异常时记录日志
@AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("执行方法: " + className + "." + methodName + "() - 执行异常: " + ex.getMessage());
}
// 环绕通知:方法执行前后记录日志
@Around("servicePointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - start;
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("执行方法: " + className + "." + methodName + "() - 耗时: " + elapsedTime + "ms");
return result;
}
}
通过上述切面定义,系统会自动在com.example.service包下的所有public方法执行前后插入日志记录逻辑,无需修改业务代码即可实现横切功能。