前面的文章中我们介绍了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 蓝桥杯 算法训练(VIP) 最大体积

    最大体积 问题描述 每个物品有一定的体积(废话),不同的物品组合,装入背包会战用一定的总体积. 假如每个物品有无限件可用,那么有些体积是永远也装不出来的. 为了尽量装满背包,附中的OIER想要研究一下 ...

  2. Java实现 蓝桥杯 算法提高 判断名次

    算法提高 判断名次 时间限制:1.0s 内存限制:256.0MB 问题描述 某场比赛过后,你想要知道A~E五个人的排名是什么,于是要求他们每个人说了一句话.(经典的开头---_-!)得了第1名的人23 ...

  3. Java实现 蓝桥杯VIP 算法提高 林丹大战李宗伟

    问题描述 我们用0表示林丹,1表示李宗伟. 输入数据中每行会给出一个0或者1,表示对应选手得1分. 当一方得分达到21分时,只要该方与对方分差超过1分,该方即胜出. 你需要输出最后获胜选手的代号. 输 ...

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

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

  5. Redis企业级数据备份与恢复方案

    一.持久化配置 RBD和AOF建议同时打开(Redis4.0之后支持) RDB做冷备,AOF做数据恢复(数据更可靠) RDB采取默认配置即可,AOF推荐采取everysec每秒策略 AOF和RDB还不 ...

  6. (四)rsync未授权访问

    01 漏洞描述 rsync是Linux/Unix下的一个远程数据同步工具,可通过LAN/WAN快速同步多台主机间的文件和目录,默认运行在873端口.由于配置不当,导致任何人可未授权访问rsync,上传 ...

  7. chrome浏览器版本与驱动不匹配问题的解决办法

    1.浏览器与驱动如何匹配才不会报错 使用selenium模块的webdriver打开谷歌浏览器时常遇到这样的错误提示: selenium.common.exceptions.WebDriverExce ...

  8. InstallShield 2015 Limited Edition 打包教程

    InstallShield 2015 Limited Edition 打包教程 右键解决方案,新增项目,选择其他项目类型,安装和部署. InstallShield2015可以免费使用,但需要下载.安装 ...

  9. ServiceStack.Redis 5.8 版本去掉每小时 6000 次访问限制

    ServiceStack.Redis这个官方 Redis 访问组件从4.0版本后开始商业化了,在使用的时候会有很多限制: 1.类型限制 类型限制是20,这个组件自带序列化功能,使得我们可以直接把对象保 ...

  10. Cookie 与 SessionID 的本质

    当用户首次访问服务器的时候,服务器为每个用户单独创建一个 Session 对象,并分配一个新的 SessionID,此时 SessionID 通过 Cookie 保存在用户端. 当用户再次访问服务器的 ...