前面的文章中我们介绍了Spring AOP的简单使用,并从源码的角度学习了其底层的实现原理,有了这些基础之后,本文来讨论一下Spring AOP失效的问题,这个问题可能我们在平时工作中或多或少也会碰到。这个话题应该从同一个对象内的嵌套方法调用拦截失效说起。

1. 问题的现象

  假设我们有如下对象类定义(同一对象内方法嵌套调用的目标对象示例):

public class NestableInvocationDemo {
public void method1(){
method2();
System.out.println("method1 executed!");
} public void method2(){
System.out.println("method2 executed!");
}
}

  这个类定义中需要我们关注的是它的某个方法会调用同一对象上定义的其他方法。这通常是比较常见的,在NestableInvocationDemo类中,method1()方法调用了同一个对象的method2()方法。

  现在,我们要使用Spring AOP拦截该类定义的method1()和method2()方法,比如一个简单的性能检测,我们定义一个Aspect:

@Aspect
public class PerformanceTraceAspect { @Pointcut("execution(public void *.method1())")
public void method1(){} @Pointcut("execution(public void *.method2())")
public void method2(){} @Pointcut("method1() || method2()")
public void compositePointcut(){}; @Around("compositePointcut()")
public Object performanceTrace(ProceedingJoinPoint joinpoint) throws Throwable{
StopWatch watch = new StopWatch();
try{
watch.start();
return joinpoint.proceed();
}finally{
watch.stop();
System.out.println("PT in method[" + joinpoint.getSignature().getName() + "]>>>>" + watch.toString());
}
}
}

配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:aop = "http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <aop:aspectj-autoproxy/>
<bean id = "nestableInvocationDemo" class = "xxx.xxx.NestableInvocationDemo"></bean>
<bean class = "xxx.xxx.PerformanceTraceAspect"></bean>
</beans>

执行如下代码:

public static void main(String[] args) { 

    ClassPathXmlApplicationContext factory = new ClassPathXmlApplicationContext("spring/demo/aop.xml");
NestableInvocationDemo demo = factory.getBean("nestableInvocationDemo", NestableInvocationDemo.class);
demo.method2();
demo.method1(); }

输出如下结果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = ; [] took = %
method2 executed!
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = ; [] took = %

  发现问题没有?当我们从外部直接调用NestableInvocationDemo对象的method2()时,显示拦截成功了,但是当调用method1()时,却只有method1()方法的执行拦截成功,其内部的method2()方法执行却没有被拦截,因为输出日志只有PT in method[method1]的信息。这说明部分AOP失效了,这是什么原因呢,我们接着往下看。

2. 原因的分析

  这种结果的出现,归根结底是由Spring AOP的实现机制造成的。我们知道,Spring AOP采用代理模式实现AOP,具体的横切逻辑会被添加到动态生成的代理对象中,只要调用的是目标对象的代理对象上的方法,通常就可以保证目标对象上的方法可以被拦截。就像NestableInvocationDemo的method2()方法执行一样,当我们调用代理对象上的method2()时,目标对象的method2()就会被成功拦截。

  不过,代理模式的实现机制在处理方法调用的时序方面,会给使用这种机制实现的AOP产品造成一个小小的“缺憾”。我们来看一下代理对象方法与目标对象方法的调用时序:

proxy.method2{
记录方法调用开始时间;
target.method2;
记录方法调用结束时间;
计算消耗的时间并记录到日志;
}

  在代理对象方法中,不管如何添加横切逻辑,也不管添加多少横切逻辑,有一点是确定的。那就是,终归需要调用目标对象上的同一方法来执行最初所定义的方法逻辑。

  如果目标对象中原始方法调用依赖于其他对象,那没问题,我们可以为目标对象注入所依赖对象的代理,并且可以保证相应Joinpoint被拦截并织入横切逻辑。而一旦目标对象中的原始方法调用直接调用自身方法的时候,也就是说,它依赖于自身所定义的其他方法的时候,问题就来了,看下面的图会更清楚。

  在代理对象的method1方法执行经历了层层拦截器之后,最终会将调用转向目标对象上的method1,之后的调用流程全部是走在TargetObject之上,当method1调用method2时,它调用的是TargetObject上的method2,而不是ProxyObject上的method2。要知道,针对method2的横切逻辑,只织入到了ProxyObject上的method2方法中,所以,在method1中所调用的method2没有能够被成功拦截。

3. 解决方案

  知道原因,我们才可以“对症下药”了。

  当目标对象依赖于其他对象时,我们可以通过为目标注入依赖对象的代理对象,来解决相应的拦截问题。那么,当目标对象依赖于自身时,我们也可以尝试将目标对象的代理对象公开给它,只要让目标对象调用自身代理对象上的相应方法,就可以解决内部调用的方法没有被拦截的问题。

  Spring AOP提供了AopContext来公开当前目标对象的代理对象,我们只要在目标对象中使用AopContext.currentProxy()就可以取得当前目标对象所对应的代理对象。现在,我们重构目标对象,让它直接调用它的代理对象的相应方法,如下面代码所示:

public class NestableInvocationDemo {
public void method1(){
((NestableInvocationDemo)AopContext.currentProxy()).method2();
System.out.println("method1 executed!");
} public void method2(){
System.out.println("method2 executed!");
}
}

  要使AopContext.currentProxy()生效,我们在生成目标对象的代理对象时,需要设置expose-proxy为true,具体如下设置:

  在基于配置文件的配置中,可按如下方式配置:

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

  在基于注解地配置中,可按如下方式配置:

@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true)

  现在,我们可以得到想要的拦截结果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = ; [] took = %
method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = ; [] took = %
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = ; [] took = %

  这种方式是可以解决问题,但是不是很优雅,因为我们的目标对象都直接绑定到了Spring AOP的具体API上了。所以,我们考虑能够通过其他方式来解决这个问题,既然我们知道能够通过AopContext.currentProxy()取得当前目标对象对应的代理对象,那完全可以在目标对象中声明对其代理对象的依赖,通过IoC容器来帮助我们注入这个代理对象。

  注入方式可以有多种:

  • 可以在目标对象中声明一个实例变量作为其代理对象的引用,然后由构造方法注入或者setter方法注入将AopContext.currentProxy()取得的Object注入给这个声明的实例变量;
  • 在目标对象中声明一个getter方法,如getThis(),然后通过Spring的IoC容器的方法注入或者方法替换,将这个方法的逻辑替换为return AopContext.currentProxy()。这样,在调用自身方法的时候,直接通过getThis().method2()就可以了;
  • 声明一个Wrapper类,并且让目标对象依赖于这个类。在Wrapper类中直接声明一个getProxy()或者类似的方法,将return AopContext.currentProxy()类似逻辑添加到这个方法中,目标对象只需要getWrapper().getProxy()就可以取得相应的代理对象。Wrapper类分离了目标对象与Spring API的直接耦合。至于让这个Wrapper以Util类出现,还是在目标对象中直接构造,或者依赖注入到目标对象,都可以;
  • 为类似的目标对象声明统一的接口定义,然后通过BeanPostProcessor处理这些接口实现类,将实现类的某个取得当前对象的代理对象的方法逻辑覆盖掉。这个与方法替换所使用的原理一样,只不过可以借助Spring的IoC容器进行批量处理而已。

  实际上,这种情况的出现仅仅是因为Spring AOP采用的是代理机制实现。如果像AspectJ那样,直接将横切逻辑织入目标对象,那么代理对象和目标对象实际上就合为一体了,调用也不会出现这样的问题。

4. 总结

  本文揭示了Spring AOP实现机制导致的一个小小的陷阱,分析了问题产生的原因,并给出了一些解决方案。

  应该说,Spring AOP作为一个轻量的AOP框架,在简单与强大之间取得了很好的平衡。合理地使用Spring AOP,将帮助我们更快更好地完成各种工作,也希望大家在Spring AOP地使用之路上愉快地前行。

Spring AOP学习笔记05:AOP失效的罪因的更多相关文章

  1. 【转】Spring.NET学习笔记——目录

    目录 前言 Spring.NET学习笔记——前言 第一阶段:控制反转与依赖注入IoC&DI Spring.NET学习笔记1——控制反转(基础篇) Level 200 Spring.NET学习笔 ...

  2. Spring.NET学习笔记——目录(原)

    目录 前言 Spring.NET学习笔记——前言 第一阶段:控制反转与依赖注入IoC&DI Spring.NET学习笔记1——控制反转(基础篇) Level 200 Spring.NET学习笔 ...

  3. Spring学习笔记之aop动态代理(3)

    Spring学习笔记之aop动态代理(3) 1.0 静态代理模式的缺点: 1.在该系统中有多少的dao就的写多少的proxy,麻烦 2.如果目标接口有方法的改动,则proxy也需要改动. Person ...

  4. Spring入门IOC和AOP学习笔记

    Spring入门IOC和AOP学习笔记 概述 Spring框架的核心有两个: Spring容器作为超级大工厂,负责管理.创建所有的Java对象,这些Java对象被称为Bean. Spring容器管理容 ...

  5. Spring MVC 学习笔记12 —— SpringMVC+Hibernate开发(1)依赖包搭建

    Spring MVC 学习笔记12 -- SpringMVC+Hibernate开发(1)依赖包搭建 用Hibernate帮助建立SpringMVC与数据库之间的联系,通过配置DAO层,Service ...

  6. Spring Boot 学习笔记(六) 整合 RESTful 参数传递

    Spring Boot 学习笔记 源码地址 Spring Boot 学习笔记(一) hello world Spring Boot 学习笔记(二) 整合 log4j2 Spring Boot 学习笔记 ...

  7. Spring框架学习笔记(1)

    Spring 框架学习笔记(1) 一.简介 Rod Johnson(spring之父) Spring是分层的Java SE/EE应用 full-stack(服务端的全栈)轻量级(跟EJB比)开源框架, ...

  8. Spring MVC 学习笔记一 HelloWorld

    Spring MVC 学习笔记一 HelloWorld Spring MVC 的使用可以按照以下步骤进行(使用Eclipse): 加入JAR包 在web.xml中配置DispatcherServlet ...

  9. SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能

    在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...

随机推荐

  1. Java实现 LeetCode 691 贴纸拼词(DFS+map记录)

    691. 贴纸拼词 我们给出了 N 种不同类型的贴纸.每个贴纸上都有一个小写的英文单词. 你希望从自己的贴纸集合中裁剪单个字母并重新排列它们,从而拼写出给定的目标字符串 target. 如果你愿意的话 ...

  2. Java实现 LeetCode 381 O(1) 时间插入、删除和获取随机元素 - 允许重复

    381. O(1) 时间插入.删除和获取随机元素 - 允许重复 设计一个支持在平均 时间复杂度 O(1) 下, 执行以下操作的数据结构. 注意: 允许出现重复元素. insert(val):向集合中插 ...

  3. Java实现 蓝桥杯VIP 算法训练 校门外的树

    问题描述 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米.我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置:数轴上的每个整数点,即0,1,2,--,L,都种 ...

  4. Java实现 串中取3个不重复字母

    从标准输入读入一个由字母构成的串(不大于30个字符). 从该串中取出3个不重复的字符,求所有的取法. 取出的字符,要求按字母升序排列成一个串. 不同的取法输出顺序可以不考虑. 例如: 输入: abc ...

  5. java实现海盗比酒量

    ** 海盗比酒量** 有一群海盗(不多于20人),在船上比拼酒量.过程如下:打开一瓶酒,所有在场的人平分喝下,有几个人倒下了.再打开一瓶酒平分,又有倒下的,再次重复- 直到开了第4瓶酒,坐着的已经所剩 ...

  6. java实现第四届蓝桥杯振兴中华

    振兴中华 题目描述 小明参加了学校的趣味运动会,其中的一个项目是:跳格子. 地上画着一些格子,每个格子里写一个字,如下所示:(也可参见p1.jpg) 从我做起振 我做起振兴 做起振兴中 起振兴中华 比 ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  8. SpringSceurity(3)---图形验证码功能实现

    SpringSceurity(3)---图形验证码功能实现 有关springSceurity之前有写过两篇文章: 1.SpringSecurity(1)---认证+授权代码实现 2.SpringSec ...

  9. 【Spring注解驱动开发】自定义TypeFilter指定@ComponentScan注解的过滤规则

    写在前面 Spring的强大之处不仅仅是提供了IOC容器,能够通过过滤规则指定排除和只包含哪些组件,它还能够通过自定义TypeFilter来指定过滤规则.如果Spring内置的过滤规则不能够满足我们的 ...

  10. 第二个hibernate Annotation版本的helloworld

    经过第一次的 hibernate  我发现每一个数据库表都对应了一个类,并且每一个类都要新建一个文件进行配置 很麻烦!  于是便出现了Annotation版本的hibernate. 具体如下: 1.同 ...