Spring AOP(面向切面编程)通过注解驱动的方式实现横切关注点(如日志、事务、权限)的模块化,核心依赖 @EnableAspectJAutoProxy
、@Aspect
、@Pointcut
及各类通知注解(@Before
、@After
等)。以下从注解定义、源码解析、执行流程、典型场景展开深度解析,并结合源码展示其底层机制。
一、核心注解概览与作用
注解 | 作用 |
---|---|
@EnableAspectJAutoProxy | 启用 AspectJ 自动代理,告诉 Spring 容器扫描 @Aspect 切面并生成代理对象。 |
@Aspect | 标记一个类为切面(Aspect),包含切点(@Pointcut )和通知(Advice)。 |
@Pointcut | 定义切点(Pointcut),指定哪些方法会被拦截(通过表达式匹配方法签名)。 |
@Before | 前置通知:在目标方法执行前执行。 |
@After | 后置通知:在目标方法执行后执行(无论成功或异常)。 |
@AfterReturning | 返回后通知:在目标方法成功返回后执行(可获取返回值)。 |
@AfterThrowing | 异常后通知:在目标方法抛出异常后执行(可获取异常信息)。 |
@Around | 环绕通知:包裹目标方法执行(可控制是否继续执行目标方法,获取返回值/异常)。 |
二、@EnableAspectJAutoProxy
:启用 AspectJ 自动代理
1. 注解定义与源码解析
@EnableAspectJAutoProxy
位于 org.springframework.context.annotation
包,源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class) // 导入注册器,负责注册切面和代理
public @interface EnableAspectJAutoProxy {
/**
* 是否强制使用 CGLIB 代理(默认 false,优先 JDK 动态代理)
*/
boolean proxyTargetClass() default false;
/**
* 暴露代理对象的引用(默认 false,仅内部使用)
*/
boolean exposeProxy() default false;
}
关键属性:
proxyTargetClass
:若为true
,强制使用 CGLIB 代理(基于类);否则优先使用 JDK 动态代理(基于接口)。exposeProxy
:若为true
,将代理对象暴露到ThreadLocal
(可通过AopContext.currentProxy()
获取)。
2. 核心流程:启用自动代理
Spring 启动时,@EnableAspectJAutoProxy
会触发以下流程:
- 注册
AspectJAutoProxyRegistrar
:通过@Import
注解导入AspectJAutoProxyRegistrar
,该类实现了ImportBeanDefinitionRegistrar
,负责将切面和代理逻辑注册到容器。 - 扫描
@Aspect
切面:AspectJAutoProxyRegistrar
扫描所有被@Aspect
标记的类,并解析其中的切点(@Pointcut
)和通知(Advice)。 - 生成代理对象:为被切面拦截的目标 Bean 生成代理(JDK 或 CGLIB),并将代理对象注册到容器中(替换原 Bean)。
三、@Aspect
:标记切面类
1. 注解定义与源码解析
@Aspect
位于 org.aspectj.lang.annotation
包(需引入 aspectjweaver
依赖),源码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Aspect {
/**
* 切面的名称(默认类名首字母小写)
*/
String value() default "";
}
关键特性:
- 标记一个类为切面,该类需被 Spring 容器管理(通常标注
@Component
)。 - 切面类中可定义多个切点(
@Pointcut
)和通知(Advice),实现横切逻辑的模块化。
2. 切面类的结构示例
@Component // 必须被 Spring 管理
@Aspect // 标记为切面
public class LogAspect {
// 定义切点:拦截所有 @Service 注解的类的方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知:在切点方法执行前打印日志
@Before("servicePointcut()")
public void beforeServiceMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("前置通知:即将执行方法 " + methodName);
}
}
四、@Pointcut
:定义切点(方法匹配规则)
1. 注解定义与源码解析
@Pointcut
位于 org.aspectj.lang.annotation
包,源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Pointcut {
/**
* 切点表达式(必填)
*/
String value();
/**
* 切点的名称(默认方法名)
*/
String name() default "";
/**
* 切点的修饰符(如 public、protected,默认任意)
*/
String modifiers() default "";
/**
* 切点的返回类型(默认任意)
*/
String returning() default "";
/**
* 切点的参数类型(默认任意)
*/
String argNames() default "";
}
核心作用:通过切点表达式定义哪些方法会被拦截。切点表达式支持以下几种匹配方式:
2. 切点表达式语法(核心)
表达式类型 | 语法示例 | 说明 |
---|---|---|
execution | execution(* com.example.service.*.*(..)) | 匹配指定包、类、方法名的方法(* 通配符,.. 表示任意参数)。 |
within | within(com.example.service.UserService) | 匹配指定类(或其子类)的所有方法。 |
annotation | annotation(com.example.annotation.Loggable) | 匹配被指定注解标记的方法。 |
this | this(com.example.service.UserService) | 匹配目标对象是指定类型或其子类的方法(JDK 代理时有效)。 |
target | target(com.example.service.UserService) | 匹配目标对象是指定类型或其子类的方法(CGLIB 代理时有效)。 |
args | args(java.lang.String) | 匹配参数类型为 String 的方法(.. 表示任意数量参数)。 |
@within | @within(com.example.annotation.Transactional) | 匹配类上有指定注解的所有方法(与 within 类似,但仅匹配注解存在的方法)。 |
3. 切点表达式的组合与复用
通过 &&
(与)、||
(或)、!”(非)组合多个切点表达式,或通过
@Pointcut` 方法复用表达式:
@Component
@Aspect
public class LogAspect {
// 复用基础切点:所有 Service 方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 组合切点:Service 方法且被 @Loggable 标记
@Pointcut("servicePointcut() && annotation(com.example.annotation.Loggable)")
public void loggableServicePointcut() {}
// 前置通知:使用组合切点
@Before("loggableServicePointcut()")
public void beforeLoggableService(JoinPoint joinPoint) {
// 仅拦截被 @Loggable 标记的 Service 方法
}
}
五、通知(Advice)注解:定义横切逻辑
通知(Advice)是切面中具体的横切逻辑,根据执行时机的不同分为以下类型:
1. @Before
:前置通知
执行时机:目标方法执行前。
参数:JoinPoint
(包含方法元信息,如方法名、参数)。
示例:
@Before("servicePointcut()")
public void beforeServiceMethod(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("[前置通知] 类:" + className + ",方法:" + methodName + " 即将执行");
}
2. @After
:后置通知
执行时机:目标方法执行后(无论成功或异常)。
参数:JoinPoint
。
示例:
@After("servicePointcut()")
public void afterServiceMethod(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置通知] 类:" + className + ",方法:" + methodName + " 执行完毕");
}
3. @AfterReturning
:返回后通知
执行时机:目标方法成功返回后(未抛出异常)。
参数:JoinPoint
+ 返回值
(通过 returning
属性指定参数名)。
示例:
@AfterReturning(
pointcut = "servicePointcut()",
returning = "result" // 参数名与方法参数名一致
)
public void afterReturningServiceMethod(JoinPoint joinPoint, Object result) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("[返回后通知] 类:" + className + ",方法:" + methodName + " 返回值:" + result);
}
4. @AfterThrowing
:异常后通知
执行时机:目标方法抛出异常后。
参数:JoinPoint
+ 异常对象
(通过 throwing
属性指定参数名)。
示例:
@AfterThrowing(
pointcut = "servicePointcut()",
throwing = "ex" // 参数名与方法参数名一致
)
public void afterThrowingServiceMethod(JoinPoint joinPoint, Exception ex) {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
System.out.println("[异常后通知] 类:" + className + ",方法:" + methodName + " 抛出异常:" + ex.getMessage());
}
5. @Around
:环绕通知
执行时机:包裹目标方法执行(可控制是否继续执行目标方法)。
参数:ProceedingJoinPoint
(继承自 JoinPoint
,新增 proceed()
方法控制目标方法执行)。
示例:
@Around("servicePointcut()")
public Object aroundServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
// 前置逻辑:记录开始时间
long startTime = System.currentTimeMillis();
System.out.println("[环绕前置] 类:" + className + ",方法:" + methodName + " 开始执行");
try {
// 执行目标方法(必须调用 proceed(),否则目标方法不会执行)
Object result = joinPoint.proceed();
// 后置逻辑:记录执行时间
long duration = System.currentTimeMillis() - startTime;
System.out.println("[环绕后置] 类:" + className + ",方法:" + methodName + " 执行完成,耗时:" + duration + "ms");
return result; // 返回目标方法的返回值
} catch (Throwable e) {
// 异常处理:记录异常
System.out.println("[环绕异常] 类:" + className + ",方法:" + methodName + " 抛出异常:" + e.getMessage());
throw e; // 重新抛出异常,确保 @AfterThrowing 能捕获
}
}
六、源码深度:Spring AOP 的代理生成与通知执行
1. 代理生成的核心类
Spring AOP 通过 ProxyFactory
生成代理对象,其核心流程如下:
- 确定代理类型:根据
proxyTargetClass
属性选择 JDK 动态代理(基于接口)或 CGLIB 代理(基于类)。 - 收集通知(Advisors):从切面中提取切点(
@Pointcut
)和通知(Advice),生成Advisor
(通知+切点的组合)。 - 生成代理对象:通过
AopProxy
接口(JDK 或 CGLIB 实现)生成代理对象,并将通知织入目标方法。
2. 通知的执行流程(以 @Around
为例)
当目标方法被调用时,代理对象会触发以下流程:
- 调用
@Around
通知的proceed()
方法:- 若
proceed()
未被调用,目标方法不会执行。 - 若
proceed()
被调用,目标方法开始执行。
- 若
- 目标方法执行前:触发
@Before
通知。 - 目标方法执行后(成功):触发
@AfterReturning
通知,然后触发@After
通知。 - 目标方法执行后(异常):触发
@AfterThrowing
通知,然后触发@After
通知。
3. 关键源码:AnnotationAwareAspectJAutoProxyCreator
Spring 通过 AnnotationAwareAspectJAutoProxyCreator
(继承自 AbstractAutoProxyCreator
)实现切面的自动检测和代理生成。其核心方法 postProcessAfterInitialization
负责为目标 Bean 生成代理:
public class AnnotationAwareAspectJAutoProxyCreator extends AbstractAutoProxyCreator {
@Override
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 检测是否需要代理(是否被切面拦截)
if (shouldSkip(bean, beanName)) {
return bean;
}
// 收集所有切面中的通知(Advisors)
List<Advisor> advisors = findEligibleAdvisors(bean.getClass(), beanName);
if (advisors.isEmpty()) {
return bean;
}
// 生成代理对象
return createProxy(bean.getClass(), beanName, advisors, new SingletonTargetSource(bean));
}
}
七、典型场景与最佳实践
1. 日志记录
通过 @Around
通知记录方法的执行时间、参数和返回值:
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
long start = System.currentTimeMillis();
System.out.println("方法 " + methodName + " 开始执行,参数:" + Arrays.toString(args));
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println("方法 " + methodName + " 执行完成,返回值:" + result + ",耗时:" + duration + "ms");
return result;
} catch (Throwable e) {
long duration = System.currentTimeMillis() - start;
System.out.println("方法 " + methodName + " 执行失败,异常:" + e.getMessage() + ",耗时:" + duration + "ms");
throw e;
}
}
}
2. 事务管理(结合 @Transactional
)
通过 @Around
通知控制事务的提交与回滚(简化版):
@Aspect
@Component
public class TransactionAspect {
@Autowired
private PlatformTransactionManager transactionManager;
@Around("@annotation(com.example.annotation.Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
Object result = joinPoint.proceed();
transactionManager.commit(status);
return result;
} catch (Throwable e) {
transactionManager.rollback(status);
throw e;
}
}
}
3. 权限校验
通过 @Before
通知校验用户权限:
@Aspect
@Component
public class SecurityAspect {
@Autowired
private UserService userService;
@Before("execution(* com.example.controller.AdminController.*(..))")
public void checkAdminPermission(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("Authorization");
User user = userService.getUserByToken(token);
if (user == null || !user.isAdmin()) {
throw new AccessDeniedException("无管理员权限");
}
}
}
八、注意事项与常见问题
1. 切面类的注册
- 切面类必须被 Spring 容器管理(标注
@Component
或通过@Bean
注册),否则@EnableAspectJAutoProxy
无法扫描到。
2. 切点表达式的准确性
- 切点表达式需精确匹配目标方法,避免拦截无关方法(如使用
within(com.example.service.UserService)
而非execution(* com.example.service.*.*(..))
)。
3. 通知的参数传递
@Before
、@After
等通知的参数需与切点表达式中的参数名一致(或通过argNames
显式指定)。
4. @Around
的 proceed()
调用
@Around
通知必须调用proceed()
方法,否则目标方法不会执行(可通过proceedingJoinPoint.proceed(args)
传递参数)。
5. 异常处理
@AfterThrowing
通知可捕获目标方法的异常,但需注意异常的传播(若在@Around
中捕获异常并重新抛出,@AfterThrowing
仍能触发)。
6. 性能优化
- 避免为高频方法添加过多通知(尤其是
@Around
),可能导致性能下降。 - 对于无状态的切面,可标记为
@Scope("prototype")
(但需谨慎,可能增加内存消耗)。
九、总结
Spring AOP 的核心注解通过**切面(@Aspect
)、切点(@Pointcut
)、通知(@Before
/@After
等)**实现了横切关注点的模块化。其底层通过 AnnotationAwareAspectJAutoProxyCreator
生成代理对象,并利用 ProceedingJoinPoint
控制目标方法的执行。理解这些注解的源码(如切点表达式解析、代理生成流程)和执行顺序(前置→目标→后置→返回/异常),有助于开发者编写高效、可维护的 AOP 逻辑。