一个Spring AOP的坑,很多人都犯过。

本文介绍了在Spring AOP中遇到的一个问题,即使用注解定义的切面在方法自调用时失效。通过分析发现,由于自调用不涉及代理对象,因此切面无法执行。解决方案是通过AopContext.currentProxy()获取代理对象并进行调用,确保切面生效。同时,文章提醒开发者检查代码中是否存在类似自调用情况,防止切面失效导致的问题。

问题重现

最初我定义了一个注解,希望可以方便统一的对一些数据库操作做缓存。于是就有了以下代码:

首先,定义一个注解:

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface Cacheable {

    /**

     * 策略名称,需要保证唯一

     * @return

     */

    public String keyName();

    /**

     * 超时时长,单位:秒

     * @return

     */

    public int expireTime();

}

然后自定义一个切面,对所有使用了该注解的方法进行切面处理:

@Aspect

@Component

public class StrategyCacheAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);

    @Around("@annotation(com.hollis.cache.StrategyCache)")

    public Object cache(ProceedingJoinPoint pjp) throws Throwable {

        // 先查缓存,如果缓存中有值,直接返回。如果缓存中没有,先执行方法,再将返回值存储到缓存中。

    }

}

然后就可以使用该注解了,使用方法如下:

@Component

public class StrategyService extends BaseStrategyService  {

    public PricingResponse getFactor(Map<String, String> pricingParams) {

        // 做一些参数校验,以及异常捕获相关的事情

        return this.loadFactor(tieredPricingParams);

    }

    @Override

    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)

    private PricingResponse loadFactor(Map<String, String> pricingParams) {

        //代码执行

    }

}

以上,对loadFactor方法增加了切面,为了方便使用,我们还定义了一个getFactor方法,设置为public,方便外部调用。

但是,在调试过程中,我发现我们设置在loadFactor方法上面的切面并没有成功,无法执行切面类。

于是开始排查问题具体是什么。



问题排查

为了排查这个问题,首先是把所有的代码检查一遍,看看切面的代码是不是有问题,有没有可能有手误打错了字之类的。

但是发现都没有。于是就想办法找找问题。

接下来我把loadFactor的访问权限从private改成public,发现没有效果。

然后我尝试着在方法外直接调用loadFactor而不是getFactor。

发现这样做就可以成功的执行到切面里面了。

发现这一现象的时候,我突然恍然大悟,直捶大腿。原来如此,原来如此,就应该是这样的。

我突然就想到了问题的原因。其实原因挺简单的,也是我之前了解到过的原理,但是在问题刚刚发生的时候我并没有想到这里,而是通过debug,发现这个现象之后我才突然想到这个原理。

那么,就来说说为什么会发生这样的问题。



代理的调用方式

我们发现上面的问题关键在于loadFactor方法被调用的方式不同。我们知道,方法的调用通常有以下几种方式:

1、在类内部,通过this进行自调用:

public class SimplePojo implements Pojo {

    public void foo() {

        // this next method invocation is a direct call on the 'this' reference

        this.bar();

    }

    public void bar() {

        // some logic...

    }

}

2、在类外部,通过该类的对象进行调用

public class Main {

    public static void main(String[] args) {

        Pojo pojo = new SimplePojo();

        // this is a direct method call on the 'pojo' reference

        pojo.foo();

    }

}

类关系及调用过程中如下图:

一个Spring AOP的坑!很多人都犯过

如果是静态方法,也可以通过类直接调用。

3、在类外部,通过该类的代理对象进行调用:

public class Main {

    public static void main(String[] args) {

        ProxyFactory factory = new ProxyFactory(new SimplePojo());

        factory.addInterface(Pojo.class);

        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();

        // this is a method call on the proxy!

        pojo.foo();

    }

}

类关系及调用过程中如下图:

一个Spring AOP的坑!很多人都犯过

那么,Spring的AOP其实是第三种调用方式,就是通过代理对象调用,只有这种调用方式,才能够在真正的对象的执行前后,能够让代理对象也执行相关代码,才能起到切面的作用。

而对于使用this的方式调用,这种只是自调用,并不会使用代理对象进行调用,也就无法执行切面类。



问题解决

那么,我们知道了,想要真正的执行代理,那么就需要通过代理对象进行调用而不是使用this调用的方式。

那么,这个问题的解决办法也就是想办法通过代理对象来调用目标方法即可。

这种问题的解决网上有很多种办法,这里介绍一个相对简单的。其他的更多的办法大家可以在网上找到一些案例。搜索关键词"AOP 自调用"即可。

获取代理对象进行调用

我们需要修改一下前面的StrategyService的代码,修改成以下内容:

@Component

public class StrategyService{

    public PricingResponse getFactor(Map<String, String> pricingParams) {

        // 做一些参数校验,以及异常捕获相关的事情

        // 这里不使用this.loadFactor而是使用AopContext.currentProxy()调用,目的是解决AOP代理不支持方法自调用的问题

        if (AopContext.currentProxy() instanceof StrategyService) {

            return ((StrategyService)AopContext.currentProxy()).loadFactor(tieredPricingParams);

        } else {

            // 部分实现没有被代理过,则直接进行自调用即可

            return loadFactor(tieredPricingParams);

        }

    }

    @Override

    @StrategyCache(keyName = "key0001", expireTime = 60 * 60 * 2)

    private PricingResponse loadFactor(Map<String, String> oricingParams) {

        //代码执行

    }

}

即使用AopContext.currentProxy()获取到代理对象,然后通过代理对象调用对应的方法。

还有个地方需要注意,以上方式还需要将Aspect的expose-proxy设置成true。如果是配置文件修改:

 <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>

如果是SpringBoot,则修改应用启动入口类的注解:

@EnableAspectJAutoProxy(exposeProxy = true)

public class Application {

}



总结

以上,我们分析并解决了一个Spring AOP不支持方法自调用的问题。

AOP失败这个问题,其实还是很严重的,因为如果发生非预期的失效,那么直接问题就是没有执行切面方法,更严重的后果可能是诸如事务未生效、日志未打印、缓存未查询等各种问题。

所以,还是建议大家看完此文之后,统查一下自己的代码,是否存在方法自调用的情况。这种情况下,任何切面都是无法生效的!

<think>嗯,用户让我详细整理一下SpringAOP知识点,我需要好好想想怎么组织这些内容。首先,我应该回顾一下AOP的基本概念,比如什么是AOP,为什么用它,然后切入Spring的实现。可能用户是刚开始学习Spring,或者想复习这方面的知识,所以需要结构清晰,覆盖主要知识点。 首先得解释AOP的核心概念,比如切面、连接点、切入点、通知、目标对象、代理这些术语。然后要说明Spring AOP和AspectJ的区别,因为很多可能会混淆这两者。接下来是通知类型,这是使用AOP时必须了解的部分,包括前置、后置、返回后、异常后和环绕通知,每个类型的作用和用法要讲清楚。 然后切入点的表达式很重要,尤其是execution的写法,可能需要举例说明。还有如何定义切面,用注解还是XML配置,这部分现在可能更多用注解,但XML的方式也应该提一下。 接着要考虑JDK动态代理和CGLIB代理的区别,以及如何选择。可能用户在实际开发中会遇到代理方式的问题,比如某些情况下需要CGLIB,这时候需要解释为什么。 另外,一些高级特性比如引入(Introductions)和切面优先级,虽然可能不常用,但作为完整整理还是应该包含。最后,常见的应用场景,比如日志、事务、权限这些,让用户知道AOP的实际用途。 还需要注意Spring AOP的局限性,比如只能拦截方法级别的连接点,不能用于非Spring管理的对象,或者私有方法、自调用的问题。这些是实际开发中容易遇到的,用户可能会需要这些信息。 在组织内容的时候,应该分点列出,结构清晰,每个部分有简短的说明,必要时举例。比如在讲通知类型时,用代码示例来说明不同注解的作用。同时,可能用户对某些概念不太清楚,需要解释得通俗易懂,避免太技术化的术语堆砌。 最后,检查是否有遗漏的重要点,比如AOP的实现原理,或者与其他框架的整合,但可能超出范围。不过用户要求的是SpringAOP,所以主要聚焦在Spring的实现上。总结部分可以强调AOP的优势和适用场景,帮助用户理解何时使用它。</think>好的!以下是 Spring AOP 的核心知识点整理,涵盖概念、实现方式、使用场景及关键细节: --- ### **1. AOP 基本概念** - **AOP(Aspect-Oriented Programming)**:面向切面编程,通过预编译或运行时动态代理,**解耦横切关注点**(如日志、事务、权限等)与核心业务逻辑。 - **核心思想**:将通用功能(如日志)从业务代码中剥离,通过“切面”统一管理。 --- ### **2. AOP 核心术语** | 术语 | 说明 | | --- | --- | | **切面(Aspect)** | 封装横切关注点的模块(如日志模块),包含通知和切入点。 | | **连接点(JoinPoint)** | 程序执行过程中的某个点(如方法调用、异常抛出)。 | | **切入点(Pointcut)** | 定义哪些连接点会被拦截(通过表达式匹配)。 | | **通知(Advice)** | 切面在连接点执行的具体动作(如前置/后置处理)。 | | **目标对象(Target)** | 被代理的原始对象(核心业务逻辑对象)。 | | **代理(Proxy)** | 增强后的对象,由 Spring 动态生成(JDK 或 CGLib 实现)。 | | **织入(Weaving)** | 将切面应用到目标对象的过程(Spring 在运行时完成)。 | --- ### **3. Spring AOP vs AspectJ** | 特性 | Spring AOP | AspectJ | | --- | --- | --- | | **实现方式** | 基于动态代理(运行时) | 基于字节码操作(编译时/类加载时) | | **功能范围** | 仅支持方法级别的连接点 | 支持字段、构造方法等更多连接点 | | **性能** | 轻量级,适合常见场景 | 功能强大,性能更高 | | **依赖** | 无需额外依赖 | 需依赖 AspectJ 编译器或织入器 | --- ### **4. 通知类型(Advice)** | 类型 | 注解 | 执行时机 | 示例 | | --- | --- | --- | --- | | **前置通知** | `@Before` | 目标方法执行前 | 权限校验 | | **后置通知** | `@After` | 目标方法执行后(无论是否异常) | 清理资源 | | **返回通知** | `@AfterReturning` | 目标方法正常返回后 | 记录结果日志 | | **异常通知** | `@AfterThrowing` | 目标方法抛出异常后 | 异常监控 | | **环绕通知** | `@Around` | 包裹目标方法,控制其执行 | 性能统计、事务管理 | ```java @Around("execution(* com.example.service.*.*(..))") public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { // 前置逻辑 Object result = pjp.proceed(); // 执行目标方法 // 后置逻辑 return result; } ``` --- ### **5. 切入点表达式(Pointcut Expression)** - **语法**:`execution([修饰符] 返回类型 包.类.方法(参数) [异常])` - 示例:`execution(* com.example.service.*.*(..))` (拦截 `service` 包下所有类的所有方法) - **通配符**: - `*`:匹配任意字符(除包名分隔符 `.`)。 - `..`:匹配多个子包或任意参数。 - **其他指示符**:`@annotation`(按注解匹配)、`within`(按类匹配)等。 --- ### **6. 定义切面的方式** - **注解方式**(主流): ```java @Aspect @Component public class LogAspect { @Before("execution(* com.example.service.*.*(..))") public void beforeAdvice() { /* ... */ } } ``` - **XML 配置**: ```xml <aop:config> <aop:aspect ref="logAspect"> <aop:before method="beforeAdvice" pointcut="execution(* com.example.service.*.*(..))"/> </aop:aspect> </aop:config> ``` --- ### **7. 代理机制** - **JDK 动态代理**:基于接口,目标类必须实现接口。 - **CGLib 代理**:基于子类,可代理无接口的类(通过生成子类覆盖方法)。 - **默认策略**:若目标类实现了接口,使用 JDK 代理;否则使用 CGLib。 - **强制使用 CGLib**:在配置中设置 `proxyTargetClass=true`。 --- ### **8. 高级特性** - **引入(Introductions)**:为类动态添加新接口(类似 Mixin)。 - **切面优先级**:通过 `@Order` 注解或实现 `Ordered` 接口定义执行顺序。 --- ### **9. 常见应用场景** - 日志记录 - 事务管理(`@Transactional` 基于 AOP 实现) - 权限校验 - 性能监控 - 异常统一处理 - 缓存管理 --- ### **10. 注意事项** 1. **自调用问题**:同类内部方法调用不会触发 AOP(需通过代理对象调用)。 2. **作用域限制**:Spring AOP 只能拦截 Spring 容器管理的 Bean。 3. **性能影响**:动态代理会轻微增加开销,高频调用需评估。 --- ### **总结** Spring AOP 通过动态代理实现解耦横切逻辑,核心在于定义切面(`@Aspect`)、切入点(`@Pointcut`)和通知(Advice)。结合注解可快速集成日志、事务等功能,但需注意其代理机制和局限性。对于复杂需求(如非方法级拦截),可结合 AspectJ 使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值