前面的文章中我们介绍了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. 【Hadoop】hdfs,剖析文件上传

    文件上传原理图 剖析文件写入 1.客户端(client)通过对DistributedFileSystem对象调用create()来新建文件: FSDataOutputStream outputStre ...

  2. Java实现 LeetCode 824 山羊拉丁文(暴力)

    824. 山羊拉丁文 给定一个由空格分割单词的句子 S.每个单词只包含大写或小写字母. 我们要将句子转换为 "Goat Latin"(一种类似于 猪拉丁文 - Pig Latin ...

  3. Java实现 蓝桥杯 基础练习 01字串

    基础练习 01字串 时间限制:1.0s 内存限制:256.0MB 提交此题 锦囊1 锦囊2 问题描述 对于长度为5位的一个01串,每一位都可能是0或1,一共有32种可能.它们的前几个是: 00000 ...

  4. Java实现 蓝桥杯VIP 算法训练 入学考试

    问题描述 辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师.为此,他想拜附近最有威望的医师为师.医师为了判断他的资质,给他出了一个难题.医师把他带到一个到处都是草药的山洞里对他说:" ...

  5. Java实现 洛谷 P1115 最大子段和

    import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scann ...

  6. 关于uniapp获取当前距离屏幕顶部的距离

    onPageScroll(e){ console.log(e); }

  7. 判断IP是否是IPV4

    bool isVaildIp(const char *ip) { int dots = 0; /*字符.的个数*/ int setions = 0; /*ip每一部分总和(0-255)*/ if (N ...

  8. 洛谷 P1115 最大子序和

    **原题链接** ##题目描述   给出一段序列,选出其中连续且非空的一段使得这段和最大.     **解法**:       1.暴力枚举 时间:O(n^3)       2.简单优化 时间:O(n ...

  9. 钻进 Linux 内核看个究竟

    Linux 内核,这个经常听见,却不不知道它具体是干嘛的东西,是不是觉得非常神秘? Linux 内核看不见摸不着,而对于这类东西,我们经常无从下手.本文就以浅显易懂的语言,带你钻进 Linux 内核, ...

  10. Python--字典(三级菜单)

    # -*- coding:utf-8 -*- data = { "腾讯":{ "LOL":{ "上单":["诺手",&q ...