轻量级Java EE企业应用开发实战
上QQ阅读APP看书,第一时间看更新

6.3 AOP

AOP(Aspect Oriented Programming,面向切面编程)通过提供另一种思考程序结构的方式来补充OOP(Object Oriented Programming,面向对象编程)。OOP模块化的关键单元是类,而在AOP中,模块化的单元是切面。切面可以实现跨多个类型和对象之间的事务管理、日志等方面的模块化。

6.3.1 AOP概述

AOP编程的目标与OOP编程的目标并没有什么不同,都是为了减少重复和专注于业务。相比之下,OOP是婉约派的选择,用继承和组合的方式编制成一套类和对象的体系;而AOP是豪放派的选择,大手一挥,凡某包、某类、某命名方法一并同样处理。也就是说,OOP是“绣花针”,而AOP是“砍柴刀”。

Spring框架的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP,但在Spring应用中经常会使用AOP来简化编程。在Spring框架中使用AOP主要有以下优势:

  • 提供声明式企业服务,特别是作为EJB声明式服务的替代品。重要的是,这种服务是声明式事务管理。
  • 允许用户实现自定义切面。在某些不适合用OOP编程的场景中,采用AOP来补充。
  • 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。

要使用Spring AOP需要添加spring-aop模块。

6.3.2 AOP核心概念

AOP概念并非是Spring AOP所特有的,有些概念同样适用于其他AOP框架,如AspectJ。

  • Aspect(切面):将关注点进行模块化。某些关注点可能会横跨多个对象,如事务管理,它是Java企业级应用中一个关于横切关注点很好的例子。在Spring AOP中,切面可以使用常规类(基于模式的方法)或@Aspect注解的常规类来实现。
  • Join Point(连接点):在程序执行过程中的某个特定的点,如某方法调用时或处理异常时。在Spring AOP中,一个连接点总是代表一个方法的执行。
  • Advice(通知):在切面的某个特定的连接点上执行的动作。通知有各种类型,包括around、before和after等。许多AOP框架(包括Spring)都是以拦截器来实现通知模型的,并维护一个以连接点为中心的拦截器链。
  • Pointcut(切入点):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(如当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心。Spring默认使用AspectJ切入点语法。
  • Introduction(引入):声明额外的方法或某个类型的字段。Spring允许引入新的接口(及一个对应的实现)到任何被通知的对象。例如,可以使用一个引入来使bean实现IsModified接口,以便简化缓存机制。在AspectJ社区,Introduction也被称为Inter-type Declaration(内部类型声明)。
  • Target Object(目标对象):被一个或多个切面所通知的对象。也有人把它称为Advised(被通知)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个Proxied(被代理)对象。
  • AOP Proxy(AOP代理):AOP框架创建的对象用来实现Aspect Contract(切面契约),包括通知方法执行等功能。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
  • Weaving(织入):把切面连接到其他的应用程序类型或对象上,并创建一个Advised(被通知)的对象。这些可以在编译时(如使用AspectJ编译器)、类加载时和运行时完成。

Spring与其他纯Java AOP框架一样,在运行时完成织入。其中有关Advice(通知)的类型主要有以下几种:

  • Before Advice(前置通知):在某连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
  • After Returning Advice(返回后通知):在某连接点正常完成后执行的通知,如果一个方法没有抛出任何异常,就正常返回。
  • After Throwing Advice(抛出异常后通知):在方法抛出异常退出时执行的通知。
  • After(finally)Advice(最后通知):当某连接点退出时执行的通知(不论是正常返回还是异常退出)。
  • Around Advice(环绕通知):包围一个连接点的通知,如方法调用。这是很强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点,或者直接返回它自己的返回值或抛出异常来结束执行。Around Advice是常用的一种通知类型。与AspectJ一样,Spring提供所有类型的通知,推荐使用尽量简单的通知类型来实现需要的功能。例如,如果只是需要用一个方法的返回值来更新缓存,虽然使用环绕通知也能完成同样的事情,但最好使用After Returning通知,而不是使用环绕通知。用合适的通知类型可以使编程模型变得简单,并且能够避免很多潜在的错误。例如,如果不调用Join Point(用于Around Advice)的proceed()方法,就不会有调用的问题。

在Spring 2.0中,所有的通知参数都是静态类型的,因此可以使用合适的类型(如一个方法执行后的返回值类型)作为通知的参数,而不是使用一个对象数组。切入点和连接点匹配的概念是AOP的关键,这使得AOP不同于其他仅仅提供拦截功能的旧技术。切入点使得通知可独立于OO(Object Oriented,面向对象)层次。例如,一个提供声明式事务管理的Around Advice(环绕通知)可以被应用到一组横跨多个对象的方法上(如服务层的所有业务操作)。

6.3.3 Spring AOP

Spring AOP用纯Java实现,它不需要专门的编译过程。Spring AOP不需要控制类装载器层次,因此它适用于Servlet容器或应用服务器。

Spring目前仅支持方法调用作为连接点之用。虽然可以在不影响Spring AOP核心API的情况下加入对成员变量拦截器的支持,但Spring并没有实现成员变量拦截器。如果需要通知对成员变量的访问和更新连接点,那么可以考虑其他语言,如AspectJ。

Spring实现AOP的方法与其他的框架不同。Spring并不是要尝试提供完整的AOP实现(尽管Spring AOP有这个能力),相反,它其实侧重于提供一种AOP实现和Spring IoC容器的整合,用于解决企业级开发中的常见问题。

因此,Spring AOP通常和Spring IoC容器一起使用。Aspect使用普通的bean定义语法,与其他AOP实现相比,这是一个显著的区别。有些是使用Spring AOP无法轻松或高效完成的,如通知一个细粒度的对象。这时,使用AspectJ是很好的选择。对于大多数在企业级Java应用中遇到的问题,Spring AOP都能提供一个非常好的解决方案。

Spring AOP从来没有打算通过提供一种全面的AOP解决方案来取代AspectJ。它们之间的关系应该是互补,而不是竞争。Spring可以无缝地整合Spring AOP、IoC和AspectJ,使所有的AOP应用完全融入基于Spring的应用体系,这样的集成不会影响Spring AOP API或AOP Alliance API。

Spring AOP保留了向下兼容性,这体现了Spring框架的核心原则——非侵袭性,即Spring框架并不强迫在业务或领域模型中引入框架特定的类和接口。

6.3.4 AOP代理

Spring AOP默认使用标准的JDK动态代理来作为AOP的代理,这样任何接口(或接口的set方法)都可以被代理。

Spring AOP也支持使用CGLIB代理,当需要代理类(而不是代理接口)时,CGLIB代理是很有必要的。如果一个业务对象并没有实现一个接口,就会默认使用CGLIB。面向接口编程是一个很好的实践,业务对象通常会实现一个或多个接口。此外,在那些(希望是罕见的)需要通知一个未在接口中声明的方法的情况下,或者需要传递一个代理对象作为一种具体类型的方法的情况下,还可以强制地使用CGLIB。

6.3.5 实战:使用@AspectJ的例子

@AspectJ是用于切面的常规Java类注解。AspectJ项目引入了@AspectJ风格,作为AspectJ 5版本的一部分。Spring使用了与AspectJ 5相同的用于切入点解析和匹配的注解,但AOP运行时仍然是纯粹的Spring AOP,并不依赖于AspectJ编译器。

1.启用@AspectJ

可以通过XML或Java配置来启用@AspectJ支持。无论在任何情况下都要确保AspectJ的aspectjweaver.jar库在应用程序的类路径中(1.6.8版本或以后)。这个库在AspectJ发布的lib目录中或通过Maven的中央库得到。配置如下:

2.创建应用

下面用一个简单有趣的例子来演示Spring AOP的用法。此例是演绎一段“武松打虎”的故事情节——武松(Fighter)在山里等着老虎(Tiger)出现,只要发现老虎出来,就打老虎。源码可以在aop-aspect目录下找到。

aop-aspect项目的pom.xml文件定义如下:

3.定义业务模型

首先定义了老虎类,代码如下:

老虎类只有一个walk()方法,只要老虎出来,就会触发这个方法。

4.定义切面和配置

那么打虎英雄武松要做什么呢?他主要关注老虎的动向,等着老虎出来活动。所以在Fighter类中定义了一个@Pointcut。同时,在该切入点前后都可以执行相关的方法,定义foundBefore()和foundAfter()。

相应的Spring配置为:

5.定义主应用

主应用定义如下:

6.运行应用

运行应用,可以看到控制台最终输出如下内容:

    Fighter wait for tiger...
    Tiger is walking...
    Fighter fight with tiger...

6.3.6 基于XML的AOP

Spring提供了基于XML的AOP支持,并提供了新的aop命名空间。

在Spring配置中,所有的aspect和advisor元素都必须放置在元素中(应用程序上下文配置中可以有多个元素)。一个元素可以包含pointcut、advisor和aspect三个元素(注意这些元素必须按照这个顺序声明)。

1.声明一个aspect

一个aspect就是在Spring应用程序上下文中定义的一个普通的Java对象。状态和行为被捕获到对象的字段和方法中,pointcut和advice被捕获到XML中。

使用元素声明一个aspect,并使用ref属性引用辅助bean。

2.声明一个pointcut

pointcut可以在元素中声明,从而使pointcut定义可以在几个aspect和advice之间共享。

以下声明代表了服务层中任何业务服务都能执行的切入点的定义。

3.声明advice

与@AspectJ风格支持相同的5种类型的advice,它们具有完全相同的语义。

以下是一个示例:

6.3.7 实战:基于XML的AOP的例子

aop-aspect用于演示基于注解的方式进行AOP编程。本节基于aop-aspect示例进行改造,形成一个新的基于XML的AOP实战例子。新的应用源码可以在aop-aspect-xml目录下找到。

1.定义业务模型

之前所定义的老虎类保持不变。老虎类只有一个walk()方法,只要老虎出来,就会触发这个方法。

之前所定义的武松类保持不变,稍作调整,去除注解,变成一个单纯的POJO。

2.定义切面和配置

所有AOP的配置都在相应的Spring的XML配置中。

3.定义主应用

主应用保持不变,代码如下:

4.运行应用

运行应用,可以看到控制台最终输出如下内容:

    Fighter wait for tiger...
    Tiger is walking...
    Fighter fight with tiger...