AOP注解 @EnableAspectJAutoProxy、 @Aspect、 @Pointcut、 @Before、 @After等 详解及详细源码展示

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 会触发以下流程:

  1. 注册 AspectJAutoProxyRegistrar:通过 @Import 注解导入 AspectJAutoProxyRegistrar,该类实现了 ImportBeanDefinitionRegistrar,负责将切面和代理逻辑注册到容器。
  2. 扫描 @Aspect 切面AspectJAutoProxyRegistrar 扫描所有被 @Aspect 标记的类,并解析其中的切点(@Pointcut)和通知(Advice)。
  3. 生成代理对象:为被切面拦截的目标 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. 切点表达式语法(核心)

表达式类型语法示例说明
executionexecution(* com.example.service.*.*(..))匹配指定包、类、方法名的方法(* 通配符,.. 表示任意参数)。
withinwithin(com.example.service.UserService)匹配指定类(或其子类)的所有方法。
annotationannotation(com.example.annotation.Loggable)匹配被指定注解标记的方法。
thisthis(com.example.service.UserService)匹配目标对象是指定类型或其子类的方法(JDK 代理时有效)。
targettarget(com.example.service.UserService)匹配目标对象是指定类型或其子类的方法(CGLIB 代理时有效)。
argsargs(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 生成代理对象,其核心流程如下:

  1. 确定代理类型:根据 proxyTargetClass 属性选择 JDK 动态代理(基于接口)或 CGLIB 代理(基于类)。
  2. 收集通知(Advisors):从切面中提取切点(@Pointcut)和通知(Advice),生成 Advisor(通知+切点的组合)。
  3. 生成代理对象:通过 AopProxy 接口(JDK 或 CGLIB 实现)生成代理对象,并将通知织入目标方法。

2. 通知的执行流程(以 @Around 为例)

当目标方法被调用时,代理对象会触发以下流程:

  1. 调用 @Around 通知的 proceed() 方法
    • proceed() 未被调用,目标方法不会执行。
    • proceed() 被调用,目标方法开始执行。
  2. 目标方法执行前:触发 @Before 通知。
  3. 目标方法执行后(成功):触发 @AfterReturning 通知,然后触发 @After 通知。
  4. 目标方法执行后(异常):触发 @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. @Aroundproceed() 调用

  • @Around 通知必须调用 proceed() 方法,否则目标方法不会执行(可通过 proceedingJoinPoint.proceed(args) 传递参数)。

5. 异常处理

  • @AfterThrowing 通知可捕获目标方法的异常,但需注意异常的传播(若在 @Around 中捕获异常并重新抛出,@AfterThrowing 仍能触发)。

6. 性能优化

  • 避免为高频方法添加过多通知(尤其是 @Around),可能导致性能下降。
  • 对于无状态的切面,可标记为 @Scope("prototype")(但需谨慎,可能增加内存消耗)。

九、总结

Spring AOP 的核心注解通过**切面(@Aspect)、切点(@Pointcut)、通知(@Before/@After 等)**实现了横切关注点的模块化。其底层通过 AnnotationAwareAspectJAutoProxyCreator 生成代理对象,并利用 ProceedingJoinPoint 控制目标方法的执行。理解这些注解的源码(如切点表达式解析、代理生成流程)和执行顺序(前置→目标→后置→返回/异常),有助于开发者编写高效、可维护的 AOP 逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值