前言
软件工程有一个基本原则叫做“关注点分离”(Concern Separation),通俗的理解就是不同的问题交给不同的部分去解决,每部分专注于解决自己的问题。这年头互联网也天天强调要专注嘛!
关注点分离其实也是一种“分治”或者“分类”的思想。人解决复杂问题的能力是有限的,所以为了控制复杂性,我们解决问题时通常都要对问题进行拆解,拆解的同时建立各部分之间的关系,各个击破之后整个问题也迎刃而解了。人类的思考,复杂系统的设计,计算机的算法,都能印证这一思想。额,扯远了,这跟AOP有神马关系?
面向切面编程(Aspect Oriented Programming,AOP)其实就是一种关注点分离的技术,在软件工程领域一度是非常火的研究领域。我们软件开发时经常提一个词叫做“业务逻辑”或者“业务功能”,我们的代码主要就是实现某种特定的业务逻辑。但是我们往往不能专注于业务逻辑,比如我们写业务逻辑代码的同时,还要写事务管理、缓存、日志等等通用化的功能,而且每个业务功能都要和这些业务功能混在一起,痛苦!所以,为了将业务功能的关注点和通用化功能的关注点分而治之,就出现了AOP技术。这些通用化功能的代码实现,对应的就是我们说的切面(Aspect)。从《Spring实战(第4版)》一书中扒了一张图:
从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其封装为独立的模块或者插件。
Spring的基本框架主要包含六大模块:DAO、ORM、AOP、JEE、WEB和CORE。AOP作为一种面向切面编程的思想,是Spring框架中的核心模块之一,在应用中具有非常重要的作用,也是Spring其它组件的基础。
AOP是什么
AOP意为:面向切面编程,通过预编译方式和运行时期动态代理实现程序功能的统一维护的一种技术。AOP作为面向对象编程(Object Oriented Programming,OOP)的一种补充,是软件开发的一个热点,也是Spring框架中的一个重要内容。
AOP是一种蕴含强大力量的相对简单的设计和编程技术,利用她可以对业务功能代码的各个部分进行隔离,明确各模块职责,从而使得业务系统各部分之间的耦合度降低 ,提高程序的可重用性和可扩展性,同时提高了开发的效率。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ,而动态代理则以Spring AOP为代表。
&emsp那么动态代理是如何实现将切面逻辑(advise)织入到目标类方法中去的呢?因为AOP的拦截功能是基于JDK中的动态代理而实现的,所以我们来看下AOP的核心功能的底层实现机制——动态代理的实现原理。AOP的源码中用到了两种动态代理来实现拦截切入功能——JDK动态代理和cglib动态代理。两种方法同时存在,各有优劣。JDK动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提是目标类必须基于统一的接口。如果没有上述前提,jdk动态代理就不能应用。由此可见,JDK动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
Spring AOP
Spring AOP使用纯Java实现,不需要专门的编译过程。它不需要控制类装载器层次,因此适用于J2EE web容器或应用服务器。
Spring目前仅支持使用方法调用作为连接点(在Spring bean上通知方法的执行)。虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。如果你需要把对成员变量的访问和更新也作为通知的连接点,可以考虑其它的语言,如AspectJ。
Spring实现AOP的方法跟其它的框架不同。Spring并不是要提供最完整的AOP实现(尽管Spring AOP有这个能力),相反的,它其实侧重于提供一种AOP实现和Spring IoC容器之间的整合,用于帮助解决在企业级开发中的常见问题。因此,Spring的AOP功能通常都和Spring IoC容器一起使用。切面使用普通的bean定义语法来配置(尽管Spring提供了强大的"自动代理(autoproxying)"功能),与其它AOP实现相比这是一个显著的区别。有些事使用Spring AOP是无法轻松或者高效完成的,比如说通知一个细粒度的对象(例如典型的域对象),此时使用AspectJ是最好的选择。不过经验告诉我们,对于大多数在J2EE应用中适合用AOP来解决的问题,Spring AOP都提供了一个非常友好的解决方案。
Spring IoC容器到底是什么?是一个Map,key是某个bean名称,value是bean。
AOP经典应用场景
AOP用来封装横切关注点,即在横向对各个关注点进行分而治之,具体可以在下面的场景中使用:
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging, tracing, profiling and monitoring 记录、跟踪、优化和校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
AOP就是横向的编程,如业务1和业务2都需要一个共同的操作,与其往每个业务中都添加同样的代码,不如写一遍代码,让两个业务共同使用这段代码。在日常有订单管理、商品管理、资金管理、库存管理等业务,都会需要到类似日志记录、事务控制、权限控制、性能统计、异常处理及事务处理等。AOP把所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码。
横切关注点通知类型
AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为; 那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
使用“横切”技术,AOP把应用程序分为两个部分:核心关注点和横切关注点;处理商业逻辑的主要流程是核心关注点,与之关系不大的功能支撑部分是横切关注点。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”
让我们从一些重要的AOP概念和术语开始了解AOP,这些术语不是Spring特有的。
横切关注点:crosscutting concern,对哪些函数进行增强,增强后怎么处理,这些关注点称之为横切关注点。
切面(Aspect):也称作方面,在实际应用中通常是一个存放关注点实现的普通Java类,其实就是共有功能的实现。简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。之所以能被AOP容器识别成切面,是在配置中指定的。如日志切面、权限切面、事务管理切面等,事务管理是J2EE应用中一个很好的横切关注点例子,她用Spring的 Advisor或拦截器实现。
引入(Introduction): 添加方法或字段到被通知的类。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口。
连接点(Join point):就是程序在运行过程中能够插入切面的地点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。例如,方法调用、异常抛出或字段修改等,但Spring只支持方法级的连接点。
通知(Advice):也称作增强,是切面的具体实现。它除了描述切面要完成的工作,还解决了何时执行这个工作的问题。以目标方法为参照点,根据放置位置的差异,可分为前置通知(Before)、 后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。关于这5中增强类型的介绍,将放在下一个章节。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点表达式来定义的。本文不介绍切入点表达式的语法规范。
目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象,也被称作被通知或被代理对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。
AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(aspect contract),包括通知方法执行等功能。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。 注意:Spring 2.0最新引入的基于模式(schema-based)风格和@AspectJ注解风格的切面声明,对于使用这些风格的用户来说,代理的创建是透明的。
代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。
织入(Weaving):将切面应用到目标对象从而创建一个新的被通知对象的过程,这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。Spring和其他纯Java AOP框架一样,在运行时完成织入。
上述概念组成了基本的AOP技术,大多数AOP工具均实现了这些技术。它们也可以是研究AOP技术的基本术语。
bean增强类型
动态代理的bean,如果里面有拦截方法的话,一定是实现了接口advised()。此时,这个bean就是增强bean。
以上是判断一个bean是否被增强的方法,下面介绍bean增强的五种类型。
-
Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可。
-
AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值。
-
AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象。
-
After:在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式。
-
Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最强大的一种通知类型,像事务、日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。Spring注解声明环绕通知的方式是@Around。
通过切入点匹配连接点的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。 切入点使得通知可以独立对应到面向对象的层次结构中。例如,一个提供声明式事务管理 的环绕通知可以被应用到一组横跨多个对象的方法上(例如服务层的所有业务操作)。
Spring AOP
Spring AOP使用纯Java实现。它不需要专门的编译过程。Spring AOP不需要控制类装载器层次,因此它适用于J2EE web容器或应用服务器。
Spring目前仅支持使用方法调用作为连接点(在Spring bean上通知方法的执行)。虽然可以在不影响到Spring AOP核心API的情况下加入对成员变量拦截器支持,但Spring并没有实现成员变量拦截器。如果你需要把对成员变量的访问和更新也作为通知的连接点,可以考虑其它的语言,如AspectJ。
Spring实现AOP的方法跟其他的框架不同。Spring并不是要提供最完整的AOP实现(尽管Spring AOP有这个能力),相反的,它其实侧重于提供一种AOP实现和Spring IoC容器之间的整合,用于帮助解决在企业级开发中的常见问题。
因此,Spring的AOP功能通常都和Spring IoC容器一起使用。切面使用普通的bean定义语法来配置(尽管Spring提供了强大的"自动代理(autoproxying)"功能):与其他AOP实现相比这是一个显著的区别。有些事使用Spring AOP是无法轻松或者高效完成的,比如说通知一个细粒度的对象(例如典型的域对象):这种时候,使用AspectJ是最好的选择。不过经验告诉我们,对于大多数在J2EE应用中适合用AOP来解决的问题,Spring AOP都提供了一个非常好的解决方案。
Spring AOP的原理机制(动态代理)
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean(目标对象)。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。