《深入Spring 2:轻量级J2EE开发框架原理与实践》
(作者:蔡世友 吴嘉俊 冯煜 张钰)
简介:
本书首先是一本通过通俗案例讲解Spring的教程;同时也是一本深入挖掘Spring及相关框架结构、设计原理的书;更是一本探讨J2EE软件开发中的艺术的书。本书还想讲述一条开源框架设计中金科玉律:思想决定一切,万变不离其宗。
本书分成四个部分,第一部分是Spring新手上路,主要讲解轻量级构架中的相关概念、发展过程、所涉及到的相关技术及详细使用方法等;第二部分是一个综合的案例,讲解如何使用Spring及相关技术来构建J2EE应用;第三部分是Spring的原理部分,主要探讨Spring框架的结构,设计原理,Spring项目源码分析等,让我们深入到Spring的核心;本书的第四部分主要探讨开源领域的一些相关话题,让大家对开源有更加深入的认识。
为了能让大家了解Spring、学会Spring、透视Spring的内核、掌握Spring的设计原理、领略Java艺术之精妙,我们为此做了很多工作。我们在EasyJF开源交流社区上开通了一个专用于解决轻量级J2EE开发问题的栏目,并请专人负责解决大家在学习及工作过程中遇到的问题,网址:http://www.easyjf.com/bbs。另外我们还通过EasyJF把本书核心案例作为一个持续的开源项目,会长期根据Spring的变更而更新,SVN地址:http://svn.easyjf.com/repository/easyjf/spring-road/。
当然,由于时间仓促及作者水平有限,本书难免带有一些不成熟的观点,不可避免存在一些问题。为此,我们将通过SVN及交流论坛对本书的补充内容进行不断更新,以确保广大的读者能接触最新、最实用的Spring技术。书中存在的问题及不足之处,还请您多给我们提建议及意见,谢谢!
关于本书的电子版本发布申明:
在出版社及本书全部作者的一致同意下,本书的一些重要章节将在互联网免费公开发行,欢迎各大网站及媒体在保留作者及版权申明的前提下转载,本书电子版本不得用于收费发布及平面发行。
另外,由于本书还处于最后组稿阶段,因此,电子版与最终出版的图书内容会存在一定的差异,我们将会通过EasyJF官网及相关网站上对电子版进行即时更新。
致谢:
在创作本书的过程中,EasyJF开源的williamRyam、天一、瞌睡虫、云淡风轻、与狼共舞、abc、netgod、navImg2等很多成员给予了很我们很大的帮助,在此深表感谢。
作者邮箱:
吴嘉俊 stef_wu@163.com
张 钰 zhangyu20030607@hotmail.com
《深入Spring 2:轻量级J2EE开发框架原理与实践》第五章
面向切面的编程(AOP)及在Spring中的应用
目 录
5.3.2 使用基于Schema的配置文件配置Spring AOP
5.3.3 使用Java5注解配置及使用Spring AOP
5.5.5 使用基于Schema的方式配置Spring AOP
第五章 面向方面的编程(AOP)及在Spring中的应用
AOP全名Aspect-Oriented Programming,中文直译为面向切面(方面)编程,当前已经成为一种比较成熟的编程思想,可以用来很好的解决应用系统中分布于各个模块的交叉关注点问题。在轻量级的J2EE中应用开发中,使用AOP来灵活处理一些具有横切性质的系统级服务,如事务处理、安全检查、缓存、对象池管理等,已经成为一种非常适用的解决方案。
本章首先简单讲解AOP的相关概念以及在Java领域中最为出色的AOP实现AspectJ的应用,然后重点讲解Spring2中AOP的实现及应用,最后通过一个有趣、完整的模拟Warcraft游戏示例来演示Spring2中的AOP的各种用法。
本章的主要是针对刚刚开始接触AOP编程方法、AsepectJ、Spring AOP的读者,另外也针对熟悉Spring2.0以前的AOP但不熟悉Spring2中AOP使用的读者。本章主要从应用的角度分析轻量级应用中的AOP编程以及Spring2中AOP的使用方法,若您对AOP的实现原理、Spring AOP的底层构架原理及AOP高级应用技巧感兴趣,请阅读本书第三部分的《AOP原理及实现》一章中的相关内容。
5.1 AOP简介
AOP全名Aspect-Oriented Programming,中文直译为面向切面 (方面)编程,是近两三年来流行起来的一种编程思想,其弥补了面向对象编程(OOP)中存在的一些不足,让我们可以编写出更加简洁、易维护、复用性更强的程序。本节主要通过一个实例引入AOP中的相关概念,并简单介绍了AOP中的各种相关术语,然后分析了AOP与OOP的关系,介绍了AOP联盟及其发布的API,并对当前一些AOP框架及工具作了简单介绍,最后简单分析了AOP在企业级应用中的作用。
5.1.1 AOP概念
AOP全名Aspect-Oriented Programming,中文直译为面向切面(方面)编程,是近两三年来流行起来的一种编程思想,用来解决OOP编程中无法很好解决问题。作为一种编程思想,其最初是由Gregor Kiczales在施乐的Palo Alto研究中心领导的一个研究小组于1997年提出。
n 问题引入
在传统OOP编程,我们通过分析、抽象出一系列具有一定属性与行为的对象,并通过这些对象之间的协作来形成一个完整的软件功能。由于对象可以继承,因此我们可以把具有相同功能或相同特性的属性抽象到一个层次分明的类结构体系中。随着软件规范的不断扩大,专业化分工越来越系列,以及OOP应用实践的不断增多,随之也暴露出了一些OOP无法很好解决的问题。
假设我们有一个业务组件Component,里面有3个业务方法,如下所示:
publicclass Component {
//业务方法1
publicvoid business1()
{
//doSomeThing1
}
//业务方法2
publicvoid business2()
{
//doSomeThing2
}
//业务方法3
publicvoid business3()
{
//doSomeThing3
}
}
由 于需求的变更,需要在每个方法执行前都要进行用户合法性验证,只有合法的用户才能执行业务方法里面的内容,因此,我们在三个方法中的第一行都需要加如一个 用户合法性检验的代码。另外,我们还需要在运行方法中的实际业务逻辑前启动一个事务,在业务逻辑代码执行完成后结束一个事务,需要在方法中加入开始事务处 理及结束事务处理的代码。最后,我们还需要在每一个方法结束后,把一些信息写入日志系统中。因此需要在每一个方法的最后添加记录日志的代码,这时业务方法 变成如下的形式:
publicvoid businessX()
{
validateUser();
beginTransaction();
//doSomeThing
endTransaction();
writeLogInfo();
}
假 如我们的系统有成千上万个这样业务方法,都需要执行用户权限验证、事务处理、日志书写或审计等类似的工作,系统中就会充斥着非常多的重复性代码,造成代码 书写及维护极其不便。例如,由于需求的变更,我们要取消一部分业务方法中的开启事务或结束事务处理的功能。此时,我们需要手工逐一删除掉这些方法中事务处 理语句。
n 问题解决
我们能不能不用在业务方法中添加哪些重性的代码,而通过某种机制,让权限验证、事务处理、日志记录等功能自动在这些方法指定的位置自动运行呢?为了能更好地解决上面提到的问题,于是引入了AOP(即面向切面)编程思想。
例如,使用AspectJ中,我们可以定义一个切面,代码如下:
publicaspect MyAspect {
voidaround():call(void Component.business*(..))
{
validateUser();
beginTransaction();
proceed();
endTransaction();
writeLogInfo();
}
}
这样,所有Component组件中返回值为void、名称以business开头的方法都会自动具有了用户验证、事务处理、日志记录等功能。
假如要进一步使某一个包中的所有名称以business开头、返回值为void的方法都具有上面的功能。则只需要把上面MyAspect中的内容修改一下即可,如下所示:
voidaround(): call(void springroad.demo.chap5..business*(..))
{
validateUser();
beginTransaction();
proceed();
endTransaction();
writeLogInfo();
}
在这里只需要知道可以使用AOP方式来实现前面的所提需求,要编译这个程序,需要使用到AspectJ编译器,关于AspectJ,我们将会在本章下一节作介绍。
在AOP中,我们把前面示例中分散程序各个部分,解决同样问题的代码片段,称为问题的切面(或方面)。一个切面可以简单的理解为解决跨越多个模块的交叉关注点问题(大多数是一些系统级的或者核心关注点外围的问题)的模块。通过AOP可以使用一种非常简单、灵活的方式,在切面中实现了以前需要在各个核心关注点中穿插的交叉关注的功能,从而使得解决系统中交叉关注点问题的模块更加容易设计、实现及维护。
提供对AOP编程方法支持的平台称为AOP实现或框架,比如AspectJ、JBoos AOP、Spring AOP等。
5.1.2 AOP中的一些相关术语介绍
在AOP编程中,包括很多新名词及概念,如关注点、核心关注点、方面、连接点、通知、切入点、引介等。由于AOP仍处于发展阶段,很多名称及术语没有统一的解释。因此,本书中关于AOP的一些术语均为当前主流的叫法。本章重点介绍的讲解轻量级J2EE中的AOP框架的应用,而关于面向切面的设计及编程方法等一些话题,我们只略作介绍。
n 关注点(Concern)
关注点也就是我们要考察或解决的问题。比如在一个在一个电子商务系统中,订单的处理,用户的验证、用户日志记录等都属于关注点(Core Concerns)。 核心关注点,是只一个系统中的核心功能,也就是一个系统中跟特定业务需求联系最紧密的商业逻辑。在一个电子商务系统中,订单处理、客户管理、库存及物流管 理都是属于系统中的核心关注点。除了核心关注点以外,还有一种关注点,他们分散在每个各个模块中解决同一样的问题,这种跨越多个模块的关注点称为横切关注 点或交叉关注点(Crosscutting Concerns)。在一个电子商业系统中,用户验证、日志管理、事务处理、数据缓存都属于交叉关注点。
在AOP的 编程方法中,主要在于对关注点的提起及抽象。我们可以把一个复杂的系统看作是由多个关注点来有机组合来实现,一个典型的系统可能会包括几个方面的关注点, 如核心业务逻辑、性能、数据存储、日志、授权、安全、线程及错误检查等,另外还有开发过程中的关注点,如易维护、易扩展等。
n 切面(Aspect)
切面是一个抽象的概念,从软件的角度来说是指在应用程序不同模块中的某一个领域或方面。从程序抽象的角度来说,可以对照OOP中的类来理解。OOP中的类(class)是实现世界模板的一个抽象,其包括方法、属性、实现的接口、继承等。而AOP中的切面(aspect)是实现世界领域问题的抽象,他除了包括属性、方法以外,同时切面中还包括切入点Pointcut、增强(advice)等,另外切面中还可以给一个现存的类添加属性、构造函数,指定一个类实现某一个接口、继承某一个类等。比如,在Spring AOP中可以使用下面的配置来定义一个切面:
<aop:aspect id="aspectDemo" ref="aspectBean">
<aop:pointcut id="somePointcut" expression="execution(* Component.*(..)" />
<aop:after-returning pointcut-ref="log" method="" />
</aop:aspect>
n 连接点(Join point)
连接点也就是运用程序执行过程中需要插入切面模块的某一点。连接点主要强调的是一个具体的“点”概念。这个点可以是一个方法、一个属性、构造函数、类静态初始化块,甚至一条语句。比如前面的例子中,连接点就是指具体的某一个方法。
在一般的AOP框架中,一般采用签名的方式来描述一个连接点,有的AOP框架只有很少类型的连接点,如Spring AOP中当前只有方法调用。
n 切入点(Pointcuts)
切入点指一个或多个连接点,可以理解成一个点的集合。切入点的描述比较具体,而且一般会跟连接点上下文环境结合。比如,在前面的例子中,切入点“execution(* Component.*(..)”表示“在Component类中所有以business打头的方法执行过程中”,其包含了3个连接点(business1、business2、business3)的集合。另外,“Component类中的所有方法调用”、“包com.easyjf.service里面所有类中所有方法抛出错误”、“类UserInfo的所有getter或seeter方法执行”,这些都可以作为切入点。另外,在大多数AOP框架实现中,切入点还支持集合运算,可以把多个切入点通过一定的组合,形成一个新的切入点。在AspectJ中,可以使用||、&&、!等操作符来组合得到一个符合特定要求的切入点。如:
pointcut setter(): target(UserInfo) && (call(void set*(String)) || call(void set*(int)));
表示所有UserInfo类中的所有带一个String或int型参数的setter方法。
pointcut transaction():target(service..)&&call(* save*(..))
表示service包中所有以save开头的方法。
n 增强或通知(Advice)
Advice一词不管翻译成建议、通知或者增强,都不能直接反映其内容,因此本书主要使用“增强”这一叫法。当然也可以把其仅看作是一个简单名词的来看待。增强(Advice)里面定义了切面中的实际逻辑(即实现),比如日志的写入的实际代码,或是安全检查的实际代码。换一种说法增强(Advice)是指在定义好的切入点处,所要执行的程序代码。比如,下面的话都是用来描述增强(Advice)的例子:“当到达切入点seeter时,检查该方法的参数是否正确”、“在save方法出现错误这个切入点,执行一段错误处理及记录的操作”。一般情况下,增强(通知)主要有前增强、后增强、环绕增强三种基本类型。
前增强(before advice)-是指在连接点之前,先执行增强中的代码。
后增加(after advice)-是指在连接点执行后,再执行增强中的代码。后增强一般分为连接点正常返回增加及连接点异常返回增强等类型。
环绕增强(around advice)-是一种功能强大的增强,可以自由改变程序的流程,连接点返回值等。在环绕增强中除了可以自由添加需要的横切功能以外,还需要负责主动调用连接点(通过proceed)来执行激活连接点的程序。
n 引介(Introduction)
引介是指给一个现有类添加方法或字段属性,引介还可以在不改变现有类代码的情况下,让现有的Java类实现新的接口,或者为其指定一个父类从而实现多重继承。相对于增强(Advice)可以动态改变程序的功能或流程来说,引介(Introduction)则用来改变一个类的静态结构。比如我们可以让一个现有为实现java.lang.Cloneable接口,从而可以通过clone()方法复制这个类的实例。
n 织入(weaving)
织入是指把解决横切问题的切面模板,与系统中的其它核心模块通过一定策略或规则组合到一起的过程。在java领域,主要包括以下三种织入方式:
1、运行时织入-即在java运行的过程中,使用Java提供代理来实现织入。根据代理产生方式的不同,运行时织入又可以进一步分为J2SE动态代理及动态字节码生成两种方式。由于J2SE动态代理只能代理接口,因此,需要借助于一些动态字节码生成器来实现对类的动态代理。大多数AOP实现都是采用这种运行时织入的方式。
2、类加载器织入-指通过自定义的类加载器,在虚拟机JVM加载字节码的时候进行织入,比如AspectWerkz(已并入AspecJ)及JBoss就使用这种方式。
3、编译器织入-使用专门的编译器来编译包括切面模块在内的整个应用程序,在编译的过程中实现织入,这种织入是功能最强大的。编译器织入的AOP实现一般都是基于语言扩展的方式,即通过对标准java语言进行一些简单的扩展,加入一些专用于处理AOP模块的关键字,定义一套语言规范,通过这套语言规范来开发切面模块,使用自己的编译器来生成java字节码。AspectJ主要就是是使用这种织入方式。
n 拦截器(interceptor)
拦截器是用来实现对连接点进行拦截,从而在连接点前或后加入自定义的切面模块功能。在大多数JAVA的AOP框架实现中,都是使用拦截器来实现字段访问及方法调用的拦截(interception)。所用作用于同一个连接点的多个拦截器组成一个连接器链(interceptor chain),链接上的每个拦截器通常会调用下一个拦截器。Spring AOP及JBoos AOP实现都是采用拦截器来实现的。
n 目标对象(Target object)
指在基于拦截器机制实现的AOP框架中,位于拦截器链上最未端的对象实例。一般情况下,拦截器未端都包含一个目标对象,通常也就是实际业务对象。当然,也可以不使用目标对象,直接把多个切面模块组织到一起,形成一个完整最终应用程序,整个系统完全使用基于AOP编程方法实现,这种情况少见。
n AOP代理(proxy)
Aop代理是指在基于拦截器机制实现的AOP框架中,实际业务对象的代理对象。这个代理对象一般被切面模块引用,AOP的切面逻辑正是插入在代理对象中来执行的。AOP代理的包括J2SE的代理以及其它字节码生成工具生成的代理两种类型。
5.1.3 AOP与OOP关系
在面向对象(OOP)的编程中,我们是通过对现实世界的抽象及模型化上来分析问题,也即把一个大的应用系统分成一个一个的对象,然后把他们有机的组合在一起完成;而在面向切面(AOP)的编程中,分析问题是从关注点的角度出发,把一个软件分成不同的关注点,软件核心业务逻辑一般都比较集中、单一,这种关注点称为核心关注点,而一些关注属于分散在软件的各个部分(主要是软件核心业务逻辑),这种关注点称为横切关注点。核心关注点可以通过传统的OOP方法来实现,而横切关注点则可以通过AOP的方法解决,即把实现相同功能、解决共性问题并分散在系统中各个部分的模块纳入一个切面中来处理。使用AOP编程,除了把一些具有共性的功能放到切面模块中以外,还可以在切面中给已有的类增加新的属性、实现新的接口等。也就是说,不但可以从类的外部动态改变程序的运行流程、给程序增加特定功能,还可以改变其静态结构。
因此,面向对象编程(OOP)解决问题的重点在于对具体领域模型的抽象,而面向切面编程(AOP)解决问题的关键则在于对关注点的抽象。也就是说,系统中对于一些需要分散在多个不相关的模块中解决的共同问题,则交由AOP来解决;AOP能够使用一种更好的方式来解决OOP不能很好解决的横切关注点问题以及相关的设计难题来实现松散耦合。因此,面向方面编程 (AOP) 提供另外一种关于程序结构的思维完善了OOP,是OOP的一种扩展技术,弥补补了OOP的不足。
OOP编程基本流程
1、 归纳分析系统,抽象领域模型;
2、 使用class来封装领域模型,实现类的功能;
3、 把各个相关联的类组装到一起形成一个完整的系统。
AOP编程基本流程
1、归纳分析系统中的关注点,分解切面;
2、按模块化的方式实现各个关注点中的功能,使用传统的编程方法如OOP;
3、按一定的规则分解及组合切面(织入或集成),形成一个完整的系统。
5.1.4 AOP联盟简介
AOP联盟(AOP Alliance)是由Java领域比较知名的一些专家及组织为了推进AOP运用研究,建立一个通用的AOP规范而成立起来的组织。组织中的成员都是在AOP编程思想及技术研究中有着比较突出贡献的专家及学者,其中有AspectWerkz的Jonas Bonér、JAC的Laurent Martelli、Spring的发起人Rod Jonhson等等。
通过AOP联盟的共同研究,可以避免一些重复性工作。AOP联盟提供了一个公共的AOP API,大多数知名的AOP框架或实现(如JBoss AOP、AspectJ、Spring等)都直接或间接对其AOP进行了集成或支持。从而可以供各种AOP开发工具及框架能简单在各个AOP应用环境中应用、移植。
AOP联盟API简介
AOP联盟制订了一套用于规范AOP实现的底层API,通过这些统一的底层API,可以使得各个AOP实现及工具产品之间实现相互移植。这些API主要以标准接口的形式提供,是AOP编程所要解决的横切交叉关注点问题各部件的最高抽象,Spring的AOP框架中也直接以这些API为基础所构建。下面我我们来看看当前AOP联盟发布的AOP相关接口。
AOP联盟的API主要包括四个部分,第一个是aop包,定义了一个表示增强(Advice)的标识接口,各种各样的增强(Advice)都继承或实现了这个接口;aop包中还包括了一个用于描述AOP系统框架错误的运行时异常AspectException。
第二个部分是intercept包,也就是拦截器包,这个包中规范了AOP核心概念中的连接点(join point)及增强(Advice)类型。
第三部及第四部分是instrument及reflect包。这两个包中的API主要包括AOP框架或产品为了实现把横切关注点的模块与核心应用模块组合集成,所需要使用的设施、技术及底层实现规范等。
“图5-1”及“图5-2”是两张关于介绍AOP联盟所发布的连接点(Joinpint)及增强(Advice)的UML结构图,通过这两张图,我们可以更加清晰了解一些AOP框架(如Spring中的AOP框架)的体系结构。
图5-1 AOP联盟定义的连接点(join point)API
图5-2 AOP联盟定义的增强(Advice) API
5.1.5 AOP相关框架及工具简介
一个AOP框架或实现主要有两部分功能,第一部分是通过定义一种机制来实现连接点、切入点及切面定义(描述)及封装实现,可以是一套语言规范或编程规范;另外一个部分就是提供把切面模块的逻辑通过织入(weaving),与系统的其它部分组合到一起,形成一个完整的系统。要使用AOP技术,不需要从最底层开始逐一实现,可以使用一些现存的AOP框架或辅助工具来引入AOP编程方法的支持,下面我们简单介绍Java中的一些AOP框架及工具。
主要的AOP实现及框架
AspectJ: 对java进行了扩展,形成一个功能强大、灵活、实用的AOP语言。AspectJ在java的基础上,加入一些AOP相关的关键字、语法结构形成一门AOP语言,其编译出来的程序是普通的Java字节码,因此,可以运行于任何Java平台,AspectJ被誉为AOP领域的急先锋。
AspectWerkz:一个动态、轻量级、性能表现良好的AOP框架。可能通过使用配置文件、配合其提供的类加载器实现动态织入。该框架的当前已经与AspectJ合并,AspectJ5就 合并后的成果。
JBoss-AOP:JBoos公司开发的基于方法拦截及源码级数据的AOP实现框架,最开始属于JBoos服务器的一部分,可以脱离JBoos单独作为一个AOP框架使用。
Spring-AOP:Spring框架中也提供了一个AOP实现,使用基于代理及拦截器的机制,与Spring IOC容器融入一体的AOP框架。Spring AOP采用运行时织入方式,使得可以在基于Spring框架的应用程序中使用各种声明式系统级服务。
AOP相关的框架或工具
除了上面介绍的几个AOP实现及框架以外,在Java领域,也有很多成熟的AOP相关技术,提供动态代理、拦截器、动态字节码生成及转换工具。下面简单列举一些用得比较多的:
ASM:一个轻量级的字节码生成及转换器。
BCEL:一个实用的字节码转换工具,在JAC中就是通过BCEL来实现方法拦截机制。
CGLIB:一个功能强大的动态代理代码工具,可以根据指定的类动态生成一个子类,并提供了方法拦截的相关机制,并且在大量的流行开源框架(如Hibernate、Spring等)中得到使用。
Javassist: JBoss提供的java字节码转换器,在JBoos的很多项目中使用,包括JBoss AOP。
5.1.6 AOP在企业级应用程序中的作用
在企业级的应用程序中,有很多系统级服务都是横切性质的,比如事务处理、安全、对象池及缓存管理等。在以EJB为代表的重量级J2EE企业级应用中,这些系统级服务由EJB容器(即J2EE应用服务器)提供,Bean的使用者可以通过EJB描述文件及服务器提供商的特定配置文件,声明式的使用这些系统服务。
而对于轻量级的应用中,由于业务组件都是多数是普通的POJO,要使用这种声明式的系统服务,则可以借助于AOP编程方法,借助AOP框架,通过切面模块来封装各种系统级服务模块,结合一些轻量级的容器应用,从而使得普通POJO业务组件也能享受声明式的系统服务。相对于J2EE应用服务器提供的几种固定的系统级服务来说,使用AOP方法可以自由定义、实现自己的系统级服务,因此变得更加灵活。比如,Spring中的声明式事务支持、JBoss中的缓存等都是使用AOP来实现的。
另外,如同前面分析的,在我们的系统中,除了一些系统级服务属于横切关注点问题以外,一些核心关注点外围的需求也会具有横切性质,因此还可以通过在程序中使用AOP来解决些具有横切性质的需求,使得系统设计更加容易、程序代码更加简洁、更加易于维护及扩展。
5.2 AspectJ简介及快速入门
AspectJ是一个基于Java语言扩展的AOP实现,被业界誉为AOP的急先锋,其提供了强大的AOP功能,其他很多AOP实现都借鉴或采纳其中的一些思想。学习使用AspectJ不但让我们可以直接在项目中使用他来解决横切关注点的问题,还可以通过他加深对AOP编程方法的认识及理解,由于Spring2中的AOP与AspectJ进行了很好的集成,因此也为我们学习使用Spring2中的AOP打下基础。
5.2.1 AspectJ介绍
AspectJ是Java语言的一个AOP实现,其主要包括两个部分,第一个部分定义了一套如何表达、定义面向切面(AOP)编程中的相关概念(如连接点、切入点、增强、切面等)的语法规范。通过这套语言规范,我们可以方便地用AOP来解决java语言中存在的交叉关注点问题。AspectJ的另外一个部分是工具部分,包括编译器、调试程序的工具以及为了更方便开发基于AOP应用而设计的开发集成工具等。
AspectJ是最早、功能比较强大的AOP实现之一,是为数不多的比较完整的AOP的实现,在AOP领域基本上充当着行业领头羊的角色,被誉为AOP领域的急先锋。AspectJ的功能比较强大,从连接点、切入点、通知、引介到切面定义都有一套比较完整的机制,很多其它语言的AOP实现,也借鉴或采纳了AspectJ中很多设计。在Java领域,AspectJ中的很多语法结构基本上成了AOP领域的标准,比如:在Spring2.0中,AOP部分就作了比较大的调整,不但引入了对AspectJ的支持,其自己的AOP核心部分的很多使用方法、定义乃至表示等都力求保持与AspectJ一致。因此,要学习使用AOP,我们有必要从AspectJ开始,因为他本身就是java语言AOP解决方案,就算不用Spring,也可以独立地用于我们的Java应用程序中。
AspectJ是Eclipse下面的一个开源项目,当前发布的版本是AspcetJ5。
5.2.2AspectJ的下载及安装
AspectJ的官方网址是:http://www.eclipse.org/aspectj/
要使用AspectJ,首先需要下载并安装AspectJ。直接进入其官网站,点击【downloads】栏目,在下载页面中选择AspectJ的一个版本,一般选择【Latest Stable Release】,然后点击后面aspectj-xxx.jar连接,即可进入下载页面,如“图5-3”所示。
图5-3 AspectJ下载页面
下载得到的是一个形如aspectj-xxx.jar的文件,比如我们以当前比如新的aspectj1.5为例,我们得到一个aspectj-1.5.2a.jar文件。然后进入命令行,输入类似java -jar D:/test/aspectj-1.5.2a.jar的命令即可启动AspectJ安装程序,如“图5-4”所示。
图5-4 启动AspectJ安装程序
然后按照界面的提示,点击相应的按钮,开始按装。安装完成后,会出现类似“图5-5”的界面:
图5-5 AspectJ安装成功提示界面
“图5-5”表示已经成功把aspectj安装到了指定目录,并建议我们把aspectjrt.jar文件添加到我们的classpath中,并把AspectJ的bin目录添加到操作系统的path中,这样以便于我们在任何目录使用AspectJ提供的工具及库文件,点击【finish】按钮完成安装!
安装完成后,切换到aspectj安装目录,可以看到bin、lib、doc三个目录,其中bin目录包含了AspectJ的编译器及相关调试工具等,lib目录是编译AspectJ的程序时所要用到库文件,doc目录是AspectJ的帮助、入门指南等文档及AspectJ应用示例代码目录。通过doc目录的文档及示例代码,我们可以快速学习及掌握AspectJ的用法。
当然,要在命令行很好的使用AspectJ的相关工具,需要设置一些环境变量。首先是把lib里面的aspectjrt.jar加到系统的classpath中,另外还要把aspectj主目录下的bin目录加到系统环境变量path中。分别如“图5-6”及“图5-7”所示:
图5-6 把AspectJ的相关lib添加到classpath
图5-7 把AspectJ主目录下的bin目录添加到系统path中
这样即完成了在Windows操作系统下AspectJ的手工安装。这时重新进入命令窗口,即可使用AspectJ的编译工具ajc命令来代替javac命令编译java源文件了。
5.2.3在Eclipse中开发AspectJ程序
当然,在实际开发中,我们很少使用命令行来编译或调试程序了。一般情况下都是使用功能比较强大的专业Java开发工具及平台。AspectJ除一套完整的语法规范以外,还提供在各种常用java开发工具开发AspectJ程序的插件,包括Eclipse、JBuild、NetBeans、JDeveloper等。在这里,我们简单讲解AspectJ与Eclipse集成应用。
首先需要下载并安装AspectJ的Eclipse插件AJDT(AspectJ Development Tools)。跟安装其它Eclipse插件一样,有两种方法安装AJDT,下面简要介绍。
第一种方法是直接到AJDT的官方网站http://www.eclipse.org/ajdt/上面,根据自己的Eclipse版本,选择下载相应的版本的插件。下载下来的插件是一个形如ajdt_1.4_for_eclipse_3.2.zip的压缩文件,其中包含features及plugins两个目录,把这个压缩文件解压到Eclipse的主目录即可,然后重新进入Eclipse,在Eclipse的【Preferences】面板中,即会看到一个【AspectJ Compiler】的选项,即表示AJDT已经正确安装。
第二种方法是直接使用Eclipse的插件自动更新功能来安装。直接点击Eclipse的【help】->【Software Updates】->【Find and Install...】,即可进入插件自动更新/安装界面,点击界面上的【New Remote Site...】按钮,然后在弹出的对话框中输入插件的名称,即AJDT,在URL一栏输入自动更新URL地址,比如:http://download.eclipse.org/tools/ajdt/32/update,点"OK"按钮,开始插件安装,安装过程中会出现一些对话框,根据情况作相应的选择即可。如“图5-8”所示:
图5-8 使用Eclipse自动更新功能来添加AJDT
插件安装完成后,会要求重启动,启动后即会在Eclipse的【Preferences】面板中,即会看到一个【AspectJ Compiler】的选项。
插件安装完后,即可以直接在Eclipse新建建立AspectJ项目,如“图5-9”,或者把一个已有的项目转为AspectJ项目,使得项目中可以支持AspectJ语法,并具具有可视化的切面信息标识,帮助我们更好的使用AspectJ进行AOP编程。
图5-9 AJDT安装成功后可用Eclipse来建立AspectJ Project
5.2.4 AspectJ版的HelloWorld
下面演示在Eclipse中建立AspectJ版本HelloWorld,首先需要安装AspectJ的Eclipse插件AJDT。然后新建一个AspectJ工程,如“图5-9”,然后新建一个demo.Hello类,内容如下:
package demo;
publicclass Hello {
publicvoid sayHello()
{
System.out.println("Hello AspectJ!");
}
publicstaticvoid main(String[] args) {
Hello h=new Hello();
h.sayHello();
}
}
然后使用使用Eclipse新建一个名为HelloAspect的Aspect切面,如“图5-10”所示。
图5-10 在Eclipse中新建AspectJ切面
在HelloAspect中定义了一个名为somePointcut()的切入点,定义了两个增强,其中一个是在连接点前执行,一个是在连接点后执行。HelloAspect的全部代码如下所示:
package demo;
publicaspect HelloAspect {
pointcut somePointcut():execution(* Hello.sayHello());
before():somePointcut(){
System.out.println("要准备说Hello了...");
}
after():somePointcut(){
System.out.println("Hello已经说完!");
}
}
在Hello上点右键,使用【Run As】->【Java Application】运行Hello,即会看到程序输出结果,如“图5-11”所示。
图5-11 AspectJ版Hello的项目总体图
5.2.5 AspectJ中相关语法
前面说了,AOP主要用来解决软件中交叉关注点的问题,AOP实现要把交叉关注点的切面模块与系统的其他关注点进行组合,即把处理交叉关注点的功能按照一定规则或策略织入到核心关注点的模块中。这里“一定规则”是指什么,怎么来描述?这正是AOP实现的关键所在。在AspectJ中,通过一套完整的语言定义规范,来灵活、清晰地定义、描述这个织入过程中的“一定规则”。而我们程序员使用AspectJ,也就是只需要掌握AspectJ的语言规范,然后按照规则写出适合我们的实际应用程序需求的“织入规则”,最后交给AspectJ的编译器负责按照这些织入规则及策略来把位于切面中处理交叉关注点的模块与其他关注点的模块组合到一起,即可实现灵活、复杂的软件功能。
我们首先来看AspectJ中的关于AOP一些概念的定义及表示方法:
n 切面(Aspect)
在AspectJ中,切面封装了切入点、通知及通知的实现代码,切面中还可以声明改变一个类的继承关系、给一个类添加属性、方法、构造函数,指定一个现有类实现一个接口等等。切面是一个独立的模块单元,跟普通的java类一样,切面中还可以定义自己属性、定义方法。一个切面一般写在一个以aj为扩展名的文件中,切面的定义根据AspectJ切面初始化的方式及生命周期的不同,有如下几种形式:
[modifier] aspect aspectName{ ... }
[modifier] aspect aspectName issingleton() { ... }
[modifier] aspect aspectName perthis(Pointcut) { ... }
[modifier] aspect aspectName pertarget(Pointcut) { ... }
[modifier] aspect aspectName percflow(Pointcut) { ... }
[modifier] aspect aspectName percflowbelow(Pointcut) { ... }
[modifier] privileged aspect aspectName { ... }
在上面的格式中,方括号"[]"中的内容是可省的,一船情况下都不需要,[modifier]可以是abstract、public及final。aspect是表示切面的关键字,aspectName表示切面的名称,aspectName后面的关键字如issingleton等用来标识不同的切面初始化方式及生命周期。
下面是一个AspectJ切面源文件内容:
publicaspect AspectDemo {
//切面里面的属性
privateint times=0;
//定义一个切入点
pointcut somePointcut():call(* Component.*(..));
//给切入点somePointcut定义一个通知
after():somePointcut(){
this.times++;
System.out.println("执行了内中的:"+thisJoinPoint.getSignature().getName());
this.print();
}
//切面中定义方法
privatevoid print()
{
System.out.println(this.times);
}
}
对于OOP编程来说,我们主要是针对类class来编程,把一个类相关的属性、方法、构造子等都封装到了类中。而对于AOP编程来说,主要就是对切面Aspect编程,也就是把切面相关连接点、切入点、通知以及实现、引介等都封装到切面中。通过前面的Helo及上面的示例,我们对AspectJ有了一个初步的印象,接下来我们将对AspectJ中如何实现连接点、切入点、通知、引介等分别作介绍。
n 连接点(Join point)
连接接点是指程序中的某一个点。在AspectJ中,连接点分得非常细致,如一个方法、一个属性、一条语句、对象加载、构造函数等都可以作为连接点。AspecJ中的连接点主要有下面的几种形式:
方法调用(Method Call)-方法被调用的时;
方法执行(Method execution)-方法体的内容执行的时;
构造函数调用(Constructor call)-构造函数被调用时;
构造函数执行(Constructor execution)-构造函数体的内容执行时;
静态初始化部分执行(Static initializer execution)-类中的静态部分内容初始化时;
对象预初始化(Object pre-initialization),主要是指执行构造函数中的this()及super()时;
对象初始化(Object initialization)-在初始化一个类的时候;
属性引用(Field reference)-引用属性值时;
属性设值(Field set)-设置属性值时;
异常执行(Handler execution)-异常执行时;
通知执行(Advice execution)-当一个AOP通知(增强)执行时。
在AspectJ中,连接点的表示使用系统提供的关键字来表达,比如,call来表示方法调用连接点,使用execution来表示方法执行连接点。连接点不会单独存在,需要与一定的上下文结合,而是在原始切入点中包含连接点的表述。
n 切入点(Pointcut)
切入点是用来表示在连接点的何处插入切面模块,也可以称为一组连接点在一定上下文环境中的集合。AspectJ中的切入点主要有两种,一种是最基本的原始切入点,另外一种是由基本切入点组合而成的切入点。原始切入点是对连接点在特定上下文的表述,通过连接点的关键字以及一定的格式来声明。下面简单介绍一些AsepctJ中的原始切入点:
(1)、方法相关的切入点
call(MethodPattern)
execution(MethodPattern)
(2)、属性相关的切入点
get(FieldPattern)
set(FieldPattern)
(3)、对象创建相关的切入点
call(ConstructorPattern)
execution(ConstructorPattern)
initialization(ConstructorPattern)
preinitialization(ConstructorPattern)
(4)、类初始化相关的切入点
staticinitialization(TypePattern)
(5)、异常处理相关的切入点
handler(TypePattern)
(6)、通知(增强)相关的切入点
adviceexecution()
(7)、基于状态的切入点
this(Type or Id)
target(Type or Id)
args(Type or Id or "..", ...)
(8)、控制流程相关的切入点
cflow(Pointcut)
cflowbelow(Pointcut)
(9)、程序内容构相关的切入点
within(TypePattern)
withincode(MethodPattern)
withincode(ConstructorPattern)
(10)、语句相关的切入点
if(BooleanExpression)
在AspectJ中,切入点一般在源代码中通过一条代有签名性质的语句来声明,如下面的例子:
pointcut anyCall() : call(* *.*(..));
pintcut是切入点的声明的关键字,anyCall是我们自己定义的切入点名称,“:”号后面是原始切入点或多个原始切入点的组合。跟其它java语句一样,切入点声明以“;”结束。
在Java5及以及的版本中,也可以在代码中使用注解来标识切入点。如下面的例子:
@Pointcut("call(* *.*(..))")
void anyCall() {}
@Pointcut是切入点的注解标签,参数中的内容为原始切入点或切入点表达式。下面定义的方法anyCall表示切入点的名称,需要用一对大括号“{}”把其括进来,也即一个空的方法体。
切入点的签名及表述遵循固定的语法格式及规范,下面是AspectJ中一些常用切入点签名语法格式:
(1)、与方法相关切入点签名语法
call/execution/withincode(MethodPart)-方法调用/方法执行/在方法体内,MethodPart代表方法签名,格式如下:
[Modifier] Type [ClassType.] methodName(ArgumentType1...N...) [throws ExceptionType]
[]中的内容为可选择的内容,Modifier表示修饰符,如private、public等,type表示返回值类型,ClassType表示类名称,methodName表示方法名称,ArgumentType表示参数类型及顺序,ExceptionType表示异常类型!如下面切入点表示调用UserService类中的所有返回值为void的公开方法:
call("public void UserService.*(..)");
(2)、与构造子相关的签名语法
call/execute/initialization/preinitialization/withincode(ConstructorPart)-表示构造子调用/构造子执行/对象初始化/对象预初始化/在构造子体内。ConstructorPart代表构造子部分,其格式如下:
[Modifier] [ClassType.]new(ArgumentType1...N...) [throws Throwabe]
(3)、与属性相关的签名语法
get/set(FieldPart)-属性引用/属性设值。FieldPart部分的格式如下:
[Modifier] Type [ClassType.] fieldName
(4)、与上下文件相关的签名语法
this(Type|var)-传递当前切入点对象;
target(Type|var)-传递连接点所属的目标对象;
args(Type|var)-传递上下文参数;
另外还有与异常相关的handler(TypePart),与包范围的within(somePackage),与表达式相关的if(Expression),与切入点控制流程相关的cflow/cflowbelow(Poincut),与注解相关的@annotation(Type|Var)等等。
n 组合切入点
在AOP程序中,我们可以直接使用单个原始切入点,有时候需要把几个原始切入点通过一定的组合,形成一个更加适合特定条件的切入点。AspectJ中可以使用集合运算,把原始切入点有机的组合到一起。切入点组合运算主要包括&&“and(与)”、||“or(或)”、!“not(非)”三种。如下所示:
pointcut someCall():(call(void buss*(..))||call(void save*(..))) && within(springroad.demo.service.*);
把三个原始切入点通过&&与||操作符组合起来,形成一个表示在springroad.demo.service包内,名称与buss或save开头,返回值为void的用有方法调用切入点。再看下面的组合切入点:
pointcut supperRole(Soldier s): target(s)&&execution(boolean Soldier.canTreat());
使用&&把两个切入点连接起来,得到一个带有目标对象作为上下文件传送参数的组合切入点。
n 增强(Advice)
增强,也称为通知,是指在切入点里执行的具体程序逻辑,也即切入点中执行什么操作,交叉关注点中需要实现的程序功能。在AspectJ中,也有很多专用于定义通知的关键字,通知的定义如下:
[ strictfp ] AdviceSpec [ throws TypeList ] : Pointcut { Body }
AdviceSpec表示具体的通知类型,具体的切面逻辑写在{}中。AspectJ主要有以下几种通知:
before( Formals )-前置通知,在切入点前执行;
after( Formals ) returning [ ( Formal ) ] -后置通知,在切入点正常返回后执行;
after( Formals ) throwing [ ( Formal ) ] -异常后通知,在切入点出现异常时执行;
after( Formals ) -final后通知,在切入点执后执行;
Type around( Formals )-环绕通知,在切入点任意位置执行,需要手动调用连接点。
我们来看一个例子:
after() returning(boolean value) :doRecord()
{
//执行相关功能
System.out.println("执行切面逻辑!");
}
表示在切入点doRecord所描述的连接点正常执行并返回后,还要执行after通知中的代码,本例输出一个“执行切面逻辑!”。
通知“:”后面的切入点可以是使用pointcut关键字定义的切入点,也可以直接写原始切入点或组合切入点,Formals可以用来代表一些参数定义,看下面的例子:
after(Soldier s)returning(boolean value):target(s)&&call(boolean Soldier.canTreat())
{
if(value)System.out.println(s.getName()+"得到治疗!");
}
定义了一个作用于Soldier类的canTreat方法的通知,并且可以直接在通知程序代码中通过s这一参数,得到连接点所属目标对象。
n 使用thisJoinPoint
在一个Java类中,我们可以在方法中使用this关键字来引用当前对象。同样在AspectJ的通知实现中,使用thisJoinPoint可以得到当前连接点的相关信息,比如对于方法连接点来说,可以得到方法名,方法参数等等。如下面的例子,可以输出当前连接点的相关信息:
after():someCall()
{
System.out.println(thisJoinPoint);
}
n 引介(Introduction)
引介是指不改变现有类的情况下,给现有类增加属性、方法,或让现有类现实指定接口、或继承某个类等,引介改变了类的静态结构。AspectJ中的引介功能比较强大,这里简单介绍其中一些常用功能。
(1)、增加内部成员
通过引介可以很容易在不修改已有类的代码,就给一个现有类增加属性、方法、构造函数等内部成员。
增加方法:
[ Modifiers ] Type OnType . Id(Formals) [ ThrowsClause ] { Body }
abstract [ Modifiers ] Type OnType . Id(Formals) [ ThrowsClause ] ;
如下面的例子给Soldier增加了一个名为exit、返回值为void的公共方法:
publicvoid Soldier.exit(){
System.out.println("退出战场!");
}
增加属性:
[ Modifiers ] Type OnType . Id = Expression;
[ Modifiers ] Type OnType . Id;
如下面的例子给Soldier增加了一个名为nickName的属性:
private String Soldier.nickName;
也可以在定义的时候初始化属性,如:
private String Soldier.nickName="游客";
增加构造子:
[ Modifiers ] OnType . new ( Formals ) [ ThrowsClause ] { Body }
如下面的例子给Soldier增加一个带有参数构造子:
public Soldier.new(String userName)
{
//初始化
}
(2)、实现接口
AspectJ可以在切面中使用declare parents关键字让一个现有的类实现某一个接口,语法如下:
declare parents: TypePattern implements TypeList;
其中TypePattern是指现有的类;当声明了实现接口以后,需要在切面中定义接口的实现逻辑。如下面的例子,我们让Soldier实现一个Comparable接口:
declareparents:Soldier implements java.lang.Comparable;
publicint Soldier.compareTo(Object o) {...}
(3)、指定继承
AspectJ可以在切面中用declare parents关键字,给一个现有的类指定一个父类,实现多重继承。格式如下:
declare parents: TypePattern extends Type;
如下面的例子,给Soldier指定了一个父类UserInfo,这样Soldier就有了UserInfo类的特性及功能:
declareparents:Soldier extends springroad.demo.UserInfo;
另外AspectJ中还有其它一些引入功能,请参考最新的AspectJ文档。
n 织入(weaving)
在写好一个切面模块后,需要把切面模块与系统中的其它模块进行组合,形成一个完整的程序,这一过程称为织入。在AspectJ中,支持两种织入方式,即编译器织入及类加载器织入。编译器织入是指直接使用AspectJ提供的编译器取代普通的javac命令来编译整个应用程序。AspectJ中使用前面安装过程中介绍的ajc命令,ajc的命令使用跟javac命令差不多,只是有一些参数略有差别。ajc可以对所有源代码一起编译(Compile-time weaving),也可以把切面源代码与已经编译好的class文件或jar包一起编译,进行织入(Post-compile weaving)。当然,若我们在开发工作中使用AspectJ提供的插件,插件中就自带AspectJ编译器,不需要使用命令符。
由于AspectWerkz合并入了AspectJ,因此合并后的AspectJ5还支持类加载器,类加载器织入是指使用AspectJ提供的类加载器,取代普通的java类加载器,由类加载在加载class到系统虚拟机中的时候进行织入操作。AspectJ5提供两个命令脚本aj及aj5,可以用来取代普通的java命令,运行需要进行织入的程序,具体的织入参数一般配置在一个名为aop.xml文件中。AspectJ5中负责处理类加载器织入的包是aspectjweaver.jar,在Spring AOP中,也是通过类加载器织入的方式,来达到与AspectJ的完全集成及支持。
5.2.6一个简单的回合格斗小游戏示例
下面,我们使用一个简单的回合格斗的小游戏,来演示AspectJ的应用。这个示例主要设计了一个战士Soldier类,这个类包括发动攻击、治疗、躲避、移动等功能。另外有一个充当客户端的主程序MainTest,里面的功能就是让两个战士回合制互相攻击,直到一个被倒下。
核心类Soldier的源码如下:
publicclass Soldier {
private String name;
privateinthealth=100;
privateintdamage=10;
privateintx=10;
privateinty=10;
//攻击其它角色
publicboolean attack(Soldier target){
boolean ret=false;
if(!target.dodge())//目标是否躲闪成功
{
target.setHealth(target.getHealth()-this.damage);
ret=true;
}
move(); //移动一下
treat();//冶我疗伤
return ret;
}
publicvoid move()
{
this.x+=getRandom(5);
this.y+=getRandom(5);
}
//躲避x及y随机变动,成功率为50%
publicboolean dodge()
{
return getRandom(10)%2==0;
}
//治疗,具有一定成功的机会,可以提高生命值0-20点
publicvoid treat()
{
if(canTreat())
this.health+=getRandom(20);
}
publicboolean canTreat()
{
return getRandom(10)/2==0;
}
privateint getRandom(int seed)
{
return RandomUtil.getRandomValue(seed);
}
//getter及setter方法
publicint getHealth() {
returnhealth;
}
publicvoid setHealth(int health) {
this.health = health;
}
public String getName() {
returnname;
}
publicvoid setName(String name) {
this.name = name;
}
publicint getX() {
returnx;
}
publicvoid setX(int x) {
this.x = x;
}
publicint getY() {
returny;
}
publicvoid setY(int y) {
this.y = y;
}
publicint getDamage() {
returndamage;
}
publicvoid setDamage(int damage) {
this.damage = damage;
}
}
Soldier引用了一个随机数生成工具类RandomUtil,用于模拟一定的发生概率,代码如下:
publicclass RandomUtil {
privatestatic java.util.Random random=new java.util.Random();
publicstaticint getRandomValue(int seed)
{
returnrandom.nextInt(seed);
}
}
然后就是使用Soldier的客户端程序MainTest,这里是一个简单的控制台程序,代码如下:
publicclass MainTest {
publicstaticvoid main(String[] args) {
Soldier p1=new Soldier();
p1.setName("角色1");
Soldier p2=new Soldier();
p2.setName("角色2");
int i=0;
while(p1.getHealth()>0 && p2.getHealth()>0)
{
p2.attack(p1);
p1.attack(p2);
i+=2;
}
System.out.println("战斗次数:"+i);
if(p1.getHealth()>0)System.out.println("角色1战胜!");
else System.out.println("角色2战胜!");
}
}
这三个类组成了一个完成的应用程序,执行MainTest,你会发现经过一会儿的战斗以后,在控制台会输出战斗的结果。
现在由于我们需要观察两个角色的详细战斗情况,也就是attach的方法执行情况,包括何时,对谁发动攻击,攻击结果等,另外还想给Soldier加入一个Boss级角色,Boos角色的疗伤treat的成功率为100%。
由于各种原因,我们不能直接更改Soldier的源代码(毕竟,在其相关的方法中直接添加输出语句,就好比让战士每发动一次攻击都需要自己记录一次战斗情况,这在激烈的即时战斗中肯定是不科学的。) 。为此,我们想到AOP,通过在AOP切面模块中实现观察战斗情况的功能。想从什么角度观察,观察哪些内容,都是由切面模块来定义,对Soldier的核心功能不影响。
除了观察详细战斗情况以外,我们还会对Soldier的一些方法进行进行切入,引入Boss级角色。
设计一个AspectJ的切面RecordGame,来处理战斗详情输出及引入Boss角色的功能,RecordGame.aj的全部内容如下:
public aspect RecordGame {
privatestatic java.text.SimpleDateFormat df=new java.text.SimpleDateFormat("yyyy-MM-dd H:m:s");
pointcut doRecord():execution(boolean Soldier.attack(Soldier));
pointcut supperRole(Soldier s): target(s)&&execution(boolean Soldier.canTreat());
after() returning(boolean value) :doRecord()
{
Soldier s=(Soldier)thisJoinPoint.getTarget();
Soldier t=(Soldier)thisJoinPoint.getArgs()[0];
System.out.println(df.format(new java.util.Date())+":"+s.getName()+" 向 "+t.getName()+" 发动了一次攻击!--结果:"+(value?"成功":"失败"));
System.out.println(s.getName()+"生命值:"+s.getHealth()+";"+t.getName()+"生命值:"+t.getHealth());
}
after(Soldier s)returning(boolean value):target(s)&&call(boolean Soldier.canTreat())
{
if(value)System.out.println(s.getName()+"得到治疗!");
}
booleanaround(Soldier s): supperRole(s)
{
if("super".equals(s.getName())) returntrue;
elsereturnproceed(s);
}
}
在上面的代码中,定义了两个切入点doRecord()及supperRole(Soldier s),doRecord用来切入Soldier的attach方法,supperRole用来切入Soldier的canTreat方法。
第一个后置增强实现用来输出战斗情况,使用AspectJ的thisJointPoint关键字,得到连接点上的目标对象以及方法参数,根据返回值value来判断攻击是否成功,最后输出交战双方的生命值,实现了对战斗情况的详细观察。
第二个后置返回增强用于根据canTreat方法的返回情况,输出是否得到成功治疗的信息。
第三个增强是环绕增强,用于给系统加入Boss角色的判断功能,这里只是通过角色的名称进行判断,若角色名称为super,则将跳过canTreat的其它部分,直接返回true,使得治疗成功率为100%,否则正常执行canTreat方法。
可以使用AspectJ提供的编译器编译4个文件,假如我们的示例存放在包springroad.demo.chap5中,可以命令行执行下面的命令:
ajc springroad/demo/chap5/*.*j*
编译成功后,使用普通java命令运行程序,这时就可以看到程序详细的战斗情况记录了,如“图5-12”所示。
图5-12 回合格斗小游戏运行结果截图
我们还可以把客户端程序MainTest中的某一个角色的名称设置为"super",这样连续运行多次MainTest,会发现super的胜率要大得多。
5.3 一个简单的Spring AOP示例
本节是Spring AOP的入门示例教程,主要是为了演示使用Spring AOP编程的基本步骤及使用方法,建议新手按照相关的步骤进行练习,先对Spring 的AOP有一个感性认识。本节主要提供一个简单例子,演示Spring2.0中AOP的配置及使用方法,并与AspectJ中的的使用进行简单的对比及分析。我们使用了本章开篇提出来的示例,并按一般J2EE应用的开发流程来演示,这里主要是讲解使用方法,因此省略一些与Spring AOP不相关的讲述。
5.3.1定义业务组件
设计系统的核心业务组件。基于针对接口编程的原则,一个好习惯是先使用接口来定义业务组件的功能,下面使用Component来代表业务组件接口。
Component.java代码如下:
package springroad.demo.chap5.exampleB;
publicinterface Component {
void business1();//商业逻辑方法1
void business2();//商业逻辑方法2
void business3();//商业逻辑方法3
}
写一个Component的实现ComponentImpl类,ComponentImpl.java代码如下:
package springroad.demo.chap5.exampleB;
publicclass ComponentImpl implements Component {
publicvoid business1() {
System.out.println("执行业务处理方法1");
}
publicvoid business2() {
System.out.println("执行业务处理方法2");
}
publicvoid business3() {
System.out.println("执行业务处理方法3");
}
}
写一个Spring的配置文件,配置业务Bean。aspect-spring.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="component"
class="springroad.demo.chap5.exampleB.ComponentImpl">
</bean>
</beans>
然后写一个用于在客户端使用业务Bean的ComponentClient类,代码如下:
package springroad.demo.chap5.exampleB;
import org.springframework.context.ApplicationContext;
publicclass ComponentClient {
publicstaticvoid main(String[] args) {
ApplicationContext context=new org.springframework.context.support.ClassPathXmlApplicationContext("springroad/demo/chap5/exampleB/aspect-spring.xml");
Component component=(Component)context.getBean("component");
component.business1();
System.out.println("-----------");
component.business2();
}
}
运行程序,我们可以看到结果输出为:
执行业务处理方法1
-----------
执行业务处理方法2
这个业务Bean只是简单的执行业务方法中代码,现在由于企业级应用的需要,我们需要把业务Bean中的所有business打头所有方法中的业务逻辑前,都要作一次用户检测、启动事务操作,另外在业务逻辑执行完后需要执行结束事务、写入日志的操作。直接修改每一个方法中的代码,添加上面的逻辑,前面已经说过存在不可维护等诸多问题,是不可取的。
由于安全检测、事务处理、日志记录等功能需要穿插分散在各个方法中,具有横切关注点的特性,因此我们想到使用Spring的AOP来实现。
5.3.2使用基于Schema的配置文件配置Spring AOP
定义一个用于处理横切交叉关注点问题的切面模块,Spring AOP使用纯Java的方式来实现AOP的,因此我们使用一个名为AspectBean的类来处理上面所说的问题。
作为示例,AspectBean.java中的内容如下:
package springroad.demo.chap5.exampleB;
publicclass AspectBean {
publicvoid validateUser()
{
System.out.println("执行用户验证!");
}
publicvoid writeLogInfo()
{
System.out.println("书写日志信息");
}
publicvoid beginTransaction()
{
System.out.println("开始事务");
}
publicvoid endTransaction()
{
System.out.println("结束事务");
}
}
(在实现应用中,用户验证、日志记录、事务处理都应该是在上面的方法中调用专门的模块来完成。另外,还要考虑很多问题,比如与连接点上下文相关的目标对象、参数值等。)
有了处理横切交叉问题的切面模块Bean,下面我们就可以在Spring的配置文件中进行Spring AOP相关的配置了。把aspect-spring.xml改成如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:config>
<aop:aspect id="aspectDemo" ref="aspectBean">
<aop:pointcut id="somePointcut"
expression="execution(* springroad.demo.chap5.exampleB.Component.business*(..))" />
<aop:before pointcut-ref="somePointcut"
method="validateUser" />
<aop:before pointcut-ref="somePointcut"
method="beginTransaction" />
<aop:after-returning pointcut-ref="somePointcut"
method="endTransaction" />
<aop:after-returning pointcut-ref="somePointcut"
method="writeLogInfo" />
</aop:aspect>
</aop:config>
<bean id="aspectBean"
class="springroad.demo.chap5.exampleB.AspectBean">
</bean>
<bean id="component"
class="springroad.demo.chap5.exampleB.ComponentImpl">
</bean>
</beans>
上面配置文件中的黑体部分是增加的内容,原来与业务Bean相关的配置不变。
为了能正确运行客户端代码,需要把Spring项目lib目录下aspectj目录中的aspectjweaver.jar文件添加到classpath中。
不需要重新编译客户端代码,直接运行示例程序ComponentClient,会看到如下的内容输出:
执行用户验证!
开始事务
执行业务处理方法1
结束事务
书写日志信息
-----------
执行用户验证!
开始事务
执行业务处理方法2
结束事务
书写日志信息
由此可见,在客户调用业务Bean Component中的business1及business2的方法时,都自动执行了用户验证、事务处理及日志记录等相关操作,满足了我们应用的需求。
5.3.3使用Java5注解配置及使用Spring AOP
Spring2中的 AOP提供了使用AspectJ中定义的Java注解在Bean中配置切入点及通知的方法。这里演示演示这种使用方法,我们写一个包含使用了Java注解来标识切面相关信息的Bean,方法名称及内容跟上面AspectBean的完全一样,AspectAnnotationBean.java中的内容如下所示:
package springroad.demo.chap5.exampleB;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
@Aspect
public class AspectAnnotationBean {
@Pointcut("execution(* springroad.demo.chap5.exampleB.Component.business*(..))")
public void somePointcut()
{
}
@Before("somePointcut()")
public void validateUser()
{
System.out.println("执行用户验证!");
}
@After("somePointcut()")
public void writeLogInfo()
{
System.out.println("书写日志信息");
}
@Before("somePointcut()")
public void beginTransaction()
{
System.out.println("开始事务");
}
@After("somePointcut()")
public void endTransaction()
{
System.out.println("结束事务");
}
}
其中黑体部分的内容是相对于前面示例中AspectBean增加的。现在我们来修改Spring的配置文件,修改后aspect-spring.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:aspectj-autoproxy />
<bean id="aspectBean"
class="springroad.demo.chap5.exampleB.AspectAnnotationBean">
</bean>
<bean id="component"
class="springroad.demo.chap5.exampleB.ComponentImpl">
</bean>
</beans>
对比上一个配置文件,我们看到这个文件比前面一个简单多了,关于aop的配置只有一行,即<aop:aspectj-autoproxy/>。这时运行客户端示例代码,我们会发现,其输出的内容跟前面示例的内容完全一样(当然,这个例子要求你必须是在Jdk1.5及以上才能运行,因为java注解是Jdk1.5才引入的功能!)。比较仔细一点的读者会发现,AspectAnnotationBean中的那些注解标签跟前面配置文件中的大致差不多。
5.3.4基于API方式来使用Spring AOP
当然,在上面的两个示例中,都需要使用到AspectJ的aspectjweaver.jar来帮助解析切入点相关表达式。假如我们不想使用AspectJ,仅仅依靠Spring AOP方面的API,也可以实现类似的功能。(假如你还是使用Spring2.0以前的版本,比如Spring1.2,那么必须使用Spring AOP的API来处理横切交叉关注点的问题)。
写一个用来具体处理连接点的通知Bean,这个Bean实现了MethodBeforeAdvice及AfterReturningAdvice接口。AdviceBean的代码如下:
package springroad.demo.chap5.exampleB;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
public class AdviceBean implements MethodBeforeAdvice, AfterReturningAdvice {
//MethodBeforeAdvice接口要求实现的方法,将在方法内的代码执行之前运行
public void before(Method method, Object[] args, Object target) throws Throwable {
validateUser();
beginTransaction();
}
//AfterReturningAdvice接口要求实现的方法,将在访问执行完后运行
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
endTransaction();
writeLogInfo();
}
public void validateUser()
{
System.out.println("执行用户验证!");
}
public void writeLogInfo()
{
System.out.println("书写日志信息");
}
public void beginTransaction()
{
System.out.println("开始事务");
}
public void endTransaction()
{
System.out.println("结束事务");
}
}
同前面解释的一样,作为演示,我们把切面模块处理的代码直放在了AdviceBean中,实际应用中将会调用具体的处理模块来实现。
有了通知处理的类AdviceBean,还要进一步定义切入点以及切面模块,这里我们直接使用Spring自带的切入点处理实现NameMatchMethodPointcut及默认切面封装(Spring中称为增强器)DefaultPointcutAdvisor类来实现。因此,不需要在书写自己的切入点及切面类,直接使用上面的两个类在配置文件中进行配置即可。
接下就是在Spring配置文件中进行配置,分别配置一个代表直接业务组件的targetBean,一个用来代表通知(Advice)具体实现的adviceBean,一个代表切入点描述的pointcutBean,一个代表切面模块的aspectBean,最后是使用代理Bean来定义的业务组件。配置文件aspect-spring.xml的内容如下:
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="targetBean"
class="springroad.demo.chap5.exampleB.ComponentImpl">
</bean>
<bean id="adviceBean"
class="springroad.demo.chap5.exampleB.AdviceBean">
</bean>
<bean id="pointcutBean"
class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="business*"></property>
</bean>
<bean id="aspectBean"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="pointcut" ref="pointcutBean"></property>
</bean>
<bean id="component"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="targetBean"></property>
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
</beans>
运行客户端程序,你会发现跟前面示例输出的一样。表示我们确实在调用业务组件的业务方法之前及之后都确保调用了安全检查、事务处理、日志记录等模块。
细心的读取会发现,此时客户端程序中用到的component这个Bean被定义为一个ProxyFactoryBean类型,其实,也正是这个代理工厂Bean的作用,才使得客户端用到的业务组件集成了处理横切问题的功能。
5.4 Spring中的AOP实现及应用
Spring是一个实现了AOP的框架,我们可以在应用Spring的系统中直接进行AOP编程,解决应用程序中横切关注点问题;另外Spring框架的其它一些主要模块(如数据访问层、事务处理等)也是建立在Spring AOP的框架的基础上,学会使用Spring AOP,可以为我们进一步学会使用Spring的其它实用功能打下基础。
5.4.1简介
Spring框架中包含一个AOP实现,是Spring 框架的重要组成部分,实现了AOP联盟约定的接口。Spring中的AOP主要使用基于拦截器的方式,实现了对方法调用连接点相关的拦截。,
Spring框架使用纯Java的方式来实现AOP,也就是不需要像AspectJ那样需要自己的专门的编译器或类加载器来实现织入。Spring AOP的织入过程是在运行时由Spring使用Java的代理机制来完成。Spring AOP依赖于Spring的核心IOC容器,并与容器融为一体,因此可以在配置文件中声明应用AOP功能,提供在Spring框架中像EJB中的声明式系统服务一样的功能。
Spring没有像AspectJ那样强大的功能,只支持与方法调用有关的连接点。不支持属性连接点拦截,也不支持构造函数连接点拦截。用Spring主创人员的话说:“这是有意为之,不希望将Spring用于增强所有对象”。当然,在J2EE应用中,AOP拦截到方法级的操作已经足够。
Spring2.0中参考了AspectJ中的很多设计及用法,提供易于理解的AOP配置方式来声明模块的切面、切入点及通知等,同时还提供了Java5注解标签来标识AOP相关信息。因此,我们可以在Spring非常简单的使用AOP功能,来解决软件项目中具有横切性质的问题。当然Spring2.0完全保留了对以前版本的AOP框架兼容,因此,如果你愿意,仍然按以前的方式来使用Spring的AOP功能。
通过使用Spring的AOP功能,我们可以在企业级J2EE应用程序中使用以下功能:
使用声明式系统服务,比如事务服务、安全性服务。
根据实际业务需求的自定义切面,使用AOP的方式解决核心关注点外围的问题。
5.4.2 Spring AOP中对AspectJ的支持
Spring是一个非常灵活的应用框架,因此非常容易与其它框架进行集成,部分或者完全引入其它开源应用框架的功能。Spring2中的AOP部分引入了AspectJ的切入点语法,使得可以使用基于AspectJ的切入点表达式来描述切入点。
Spring2.0当前支持的AspectJ原始切入点表达关键字有,execution(方法执行的连接点,这是Spring中最主要的切入点指定者),within(限定匹配特定类型的连接点),this(连接点本身),target(连接点目标对象),arg(连接点参数)等。另外还有@target、@args、@within 、@annotation等。当然,Spring可能还会在后面的版本中提供更多AspectJ原始切入点的支持。
可以使用AspectJ的切入点表达式来描述切入点,通过的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
有“?”号的部分表示可省略的,modifers-pattern表示修饰符如public、protected等,ret-type-pattern表示方法返回类型,declaring-type-pattern代表特定的类,name-pattern代表方法名称,param-pattern表示参数,throws-pattern表示抛出的异常。在切入点表达式中,可以使用*来代表任意字符,用..来表示任意个参数。
比如前面的示例中,execution(* springroad.demo.chap5.exampleB.Component.business*(..))就是一个基于AspectJ的切入点表达式。匹配springroad.demo.chap5.exampleB.Component类中以business开头,返回值为任意类型的方法。
各个原始切入点表达式的详细使用方法请参考前面的AspectJ一节及其它AspectJ相关资料。
另外,Spring还支持通过使用AspectJ的注解方式来在源码中标识切入点、增强、切面等,就像在AspectJ中使用注解来开发切面一样。Spring2主要支持以下的AspectJ标签:
@Aspect-标识一个切面;
@Pointcut-标识一个切入点;
@Before-标识一个前置增强;
@AfterReturning-标识返回后增强;
@AfterThrowing-标识异常抛出增强;
@After-标识后增强;
@Around-标识环绕增强;
@DeclareParents-标识引介;
如前面的例子中,在普通的POJO中,使用下面的代码标识了一个后置增强,validateUser方法增强的实现代码。
@Before("somePointcut()")
publicvoid validateUser()
{
System.out.println("执行用户验证!");
}
当然,由于Spring的AOP功能比较简单,只实现了针对方法的拦截。假如需要在应用程序中引入更多AOP功能,可以直接在Spring项目中使用AspectJ。由于AspectJ5提供了类加载器织入机制,因此我们可以通过配置类加载器,在基于Spring的应用中使用全部的AspectJ功能。
关于如何配置及使用AspectJ的类加载器织入,请参考本章关于AspectJ一节的介绍及其它AspectJ相关文档。要注意的一点是,尽量在类加载织入的配置参数文件aop.xml中定义自己需要织入的包及类,不要把Spring框架的相关包包含其中。
5.4.3 Spring AOP配置方法
在Spring2提供了比较灵活的AOP配置方式。可以使用基于Schema的方式使用<aop:config>在配置文件配置切入点、增强及切面;也可以直接启用@AspectJ支持功能,直接在POJO中使用Java注解来标识切面相关配置信息;当然,还可以使用Spring2以前的配置方式,即通过在配置文件中分别配置增强(Advice)、切入点(Pointcut)、切面封装/增强器(Advisor)、然后使用代理工厂Bean等配置业务对象的代理,按部就班的进行Spring AOP配置;另外还提供了一些便捷的配置方式,如自动代理。
在Spring2中,我们重点强调使用与AspectJ切入点语言相结合的方式来使用Spring的AOP功能。因此,配置文件将从@AspectJ支持自动代理、基于Shcema的AOP配置标签及基于普通Bean方式三个方面来介绍Spring AOP中的配置。
(1)、使用@AspectJ标签
在AspectJ5中,增加对了Java5注解的完全支持,可以使用Java注解来取代专门的AOP语法,把普通的Java类(POJO)声明为切面模块。Spring2提供了对AspectJ的支持,引用了AspectJ中的一个库来做切入点表达式语言的解析。这里,我们可以像在AspectJ5中进行切面编程一样,直接在Java类中用相关的注解来标识切面模块中的各部分。最后直接在Spring的配置文件中加上一个开启@AspectJ注解支持的标签即可。
要开启使用@AspectJ标签功能,需要在Spring配置文件中使用<aop:aspectj-autoproxy/>来开启在POJO中通过注解来标识切面模块的识别功能。这时要使用一个切面,只需把带有@AspectJ标签的Java类配置成一个普通Bean即可。如下面的例子:
<bean id="someAspect" bean="springroad.demo.LogAspect"/>
对应的java类LogAspect.java源码为:
package springroad.demo;
import org.aspectj.lang.annotation.Aspect;
@Aspect
publicclass LogAspect {
}
进一步完善LogAspect类中功能。首先加入切入点的定义,使用@Pointcut标签来定义,如下面在LogAspect中加入了一个匹配springroad.demo包及下面所有包中,名称为business开头的方法的切入点:
@Pointcut("execution(*business*(..))&&within(springroad.demo..*)")
privatevoid businessMethod(){}
当然,这个切入点可以直接在配置文件中引用。我们可以在切面中进一步定义增强(Advice)实现,同样使用标签。AspectJ支持很多增强类型,但当前Spring只支持其中部分标签,包括@Before、@AfterReturning、@AfterThrowing、@After、@Around等几种。比如,在切面POJO中要定义一个@After增强及实现,可以使用下面的方式:
@After("businessMethod()")
publicvoid writeLog()
{
System.out.println("书写日志信息!");
}
@After标签中的参数也就是前面我们定义的切入点;然后下面的方法即是增强的实现代码!
最后就是在Spring配置文件中使用上面的切面,如下面是使用LogAspect的一个完整的Spring配置文件内容:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:aspectj-autoproxy />
<bean id="aspectBean2" class="springroad.demo.LogAspect"></bean>
<bean id="component"
class="springroad.demo.chap5.exampleB.ComponentImpl">
</bean>
</beans>。
通过这个配置,就会在指定的连接点执行完成后,插入书写日志的代码。注意在<beans>中要加入“xmlns:aop”命名声明,并在“xsi:schemaLocation”中指定aop配置的schema地址。
当然,我们也可以在一个切面类中只包含切点定义信息,在另外一个切面类中包含通知实现信息。比如,下面的代码在MyAppPointcuts类中,我们一共定义了三个切入点。分别为:onSaveSomething()、onDelSomething()、onUpdateSomething()。
package springroad.demo;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
publicclass MyAppPointcuts {
@Pointcut("execution(public * save*(..))")
publicvoid onSaveSomething(){}
@Pointcut("execution(public * del*(..))")
publicvoid onDelSomething(){}
@Pointcut("execution(public * update*(..))")
publicvoid onUpdateSomething(){}
}
接下来,我们可以使用另外一个或多个类来存放处理增强(Advice)的实现逻辑。看下面用来进行对象删除处理的切面封装代码:
package springroad.demo;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
publicclass DelSomethingAdvice {
@Around("MyAppPointcuts.onDelSomething()")
public Object doDelSomething(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("准备执行删除:"+joinPoint.getArgs()[0]);
Object retVal = joinPoint.proceed();
System.out.println("完成了删除操作!");
return retVal;
}
}
注意其中@Around标签中参数值是"MyAppPointcuts.onDelSomething()",这表是我们引用的是MyAppPointcuts中的onDelSomething()这个切入点。
(2)、基于schema(模式)配置Spring AOP
假如我们的JDK底于1.5,或者我们不想在Java类中使用注解来标识切入点、通知实现等相关信息。这时我们可以选择使用完全基于Schema的方式来配置Spring AOP。同样是用普通的Java Bean(POJO)来封装切面模块,只是需要在Spring配置文件中通过AspecJ切入点语言表达式来定义切入点,并配置相关的增强(Advice)实现方法等。
在配置文件中,把所有关于AOP配置的信息统一放在<aop:config>标签内。通过以aop开头的xml命令空间来行进行AOP配置。一个典型的的包含AOP配置信息的Spring配置文件结构如下所示:
<aop:config>
<aop:pointcut id="somePointcut" .../>
<aop:advisor id="someAdvisor" .../>
<aop:aspect id="someAspect" ref="someBean">
<aop:adviceType id="someAdvice" .../>
</aop:aspect>
</aop:config>
<aop:config>中,可以依次定义切入点(Pointcut)、增强器(Advisor)、及切面(Aspect)。在Advisor及Aspect中又进一步指定增强(Advice)的实现。
来看下面的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(public * del*(..))" />
<aop:aspect id="myAspect" ref="aspectBean">
<aop:around pointcut-ref="myPointcut" method="doDelSomething" />
</aop:aspect>
</aop:config>
<bean id="aspectBean" class="springroad.demo.DelSomethingAdvice"></bean>
<bean id="userService"
class="springroad.demo.UserServiceImpl">
</bean>
</beans>
假如使用增强器(advisor)来封装切面模块,这时增强(advice)必须实现AOP联盟Advice接口。下面定义一个环绕通知MethodInterceptor的实现RealDelAdvice,代码如下:
package springroad.demo;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
publicclass RealDelAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("准备执行删除:"+invocation.getArguments()[0]);
Object retVal = invocation.proceed();
System.out.println("完成了删除操作!");
return retVal;
}
}
然后修改Spring配置文件内容,AOP配置部分如下:
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(public * del*(..))" />
<aop:advisor pointcut-ref="myPointcut" advice-ref="delAdvice" />
</aop:config>
<bean id="delAdvice" class="springroad.demo.RealDelAdvice"></bean>
<bean id="userService"
class="springroad.demo.UserServiceImpl">
</bean>
关于基于Schema配置的详细信息,请参考Spring AOP的Schema文件中详细定义,地址:http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
(3)、基于Spring API的配置文件
如果不想使用AspectJ的切入点描述语言,也不想使用Spring2提供的基于Schema的AOP 配置方式,对于Spring2以前的版本来说,还可以直接在Spring配置文件像配置普通Bean一样来进行Spring AOP配置。在默认情况下,配置文件中的内容大致包含如下内容:
1、0个或多个切入点定义Bean,必须实现Pointcut接口;
2、1个或多个通知实现Bean,必须实现Advice接口;
3、0个或多个引介Bean,实现IntroductionInfo接口;
4、一个或多个切面封装Bean,必须实现Advisor接口;
5、一个或多个真实业务Bean;
6、一个或多个代理Bean。
下面来看一个例子:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--定义切入点 -->
<bean id="pointcutBean"
class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="del*"></property>
</bean>
<!-- 定义通知实现-->
<bean id="adviceBean" class="springroad.demo.RealDelAdvice"></bean>
<!--定义一个切面封装-->
<bean id="aspectBean"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="pointcut" ref="pointcutBean"></property>
</bean>
<!--定义一个实际业务Bean-->
<bean id="targetBean" class="springroad.demo.UserServiceImpl">
</bean>
<!--定义一个代理业务Bean-->
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="targetBean"></property>
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
</beans>
当然,上面的配置并非每一步都不可少,适当选择Spring的一些切面封装实现,可以减少切入点配置环节,另外,也可以使用自动代理或代理模板等,从而达到简化Spring AOP配置的目的。比如,通过使用NameMatchMethodPointcutAdvisor代替DefaultPointcutAdvisor,然后在代理Bean中使用内部Bean,上面的配置文件可以简化成如下的形式:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- 定义通知实现-->
<bean id="adviceBean" class="springroad.demo.RealDelAdvice"></bean>
<!--定义一个切面封装-->
<bean id="aspectBean"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="mappedName" value="del*"></property>
</bean>
<!--定义一个代理业务Bean-->
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target"><bean class="springroad.demo.UserServiceImpl">
</bean></property>
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
</beans>
如果使用自动代理,还可以进一步把上面的配置简化成如下的形式:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- 定义通知实现-->
<bean id="adviceBean" class="springroad.demo.RealDelAdvice"></bean>
<!-- 定义一个自动代理 -->
<bean
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<value>*Service</value>
</property>
<property name="interceptorNames">
<list>
<value>adviceBean</value>
</list>
</property>
</bean>
<!--定义一个实际业务Bean-->
<bean id="userService" class="springroad.demo.UserServiceImpl">
</bean>
</beans>
注意在自动代理中我们是直接定义业务Bean,而代理则是由Spring在创建Bean的时候自动加上去的,不需要专门定义,这种方式对于我们的Bean名称一致,需要到处使用插入横切模块的时候使用,比如业务层的事务处理及日志记录等。
尽管我们通过Spring提供的一些PointcutAdvisor实现,或者是使用自动代理,可以使得配置文件得到一定的减化,相比于Spring2中提供的配置方式,基于Spring AOP API的配置还是复杂得多。因此,在Spring2中,我们推荐尽量选择使用前面的两种配置方式,因为一方面是AspectJ的切入点语言功能比较强大,是比较成熟的Java语言扩展,另一方面所写的切面封装模块可以完成不依赖于Spring或AOP联盟的API,而是针对POJO,因此会使得我们的切面模块更加独立、更加灵活,编写及调试都将更加方便。
5.4.4切入点(Pointcut)
在任何一个AOP框架中,一个必要的部分就是如何定义及描述连接点与切入点。在Spring的AOP实现中,由于只实现了针对方法调用的拦截及増强,在此我们只关心方法连接点。Spring中的跟其它AOP框架一样,对于方法连接点主要是观察方法执行前、方法执行正常返回后、方法执行抛出异常时以及方法执行过程中这几个方面。针对这几个方面,连接点的描述也比较简单,在Spring AOP框架内部已经预定义了这些连接点的描述,这一描述是通过与增强(Advice)相关的接口来固定建立关系的,比如MethodBeforeAdvice这个增强就与方法执行前这个连接点关联。因此,我们现在要关心的问题是切入点的描述,由于Spring只关心方法调用连接点,因此切入点的描述重点也就是方法的描述上。方法的描述涉及到几个方面,一个是方法名称的描述(比如要考虑通配符)、方法所带参数、方法所处的类、方法执行的流程等。基于以上的分析,在Spring AOP中,引入了一个Pointcut接口来表示切入点,Poincut接口的内容如下:
publicinterface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
其中ClassFilter用来处理切入点中的类集合,而MethodMatcher是用来处理匹配的方法。ClassFilter完整的定义如下:
public interface ClassFilter {
boolean matches(Class clazz);
}
其中matches方法用来判定某一个类是否在切入点的类集合中。
MethodMatcher完整的定义如下:
public interface MethodMatcher {
boolean matches(Method m, Class targetClass);
boolean isRuntime();
boolean matches(Method m, Class targetClass, Object[] args);
}
matches(Method m, Class targetClass)方法用来判定某一个类中的某一个方法是否匹配。isRuntime来来指定这个切点是否为运行时动态切点。若matches为true且是运行时动态切点,则进一步使用带方法参数的matches来判定方法匹配。
在Spring AOP的核心处理引擎中,就是通过这几个接口来包装描述一个切入点的。当然,这里只是接口,实现应用中我们需要使用特定的实现。好在Spring已经为我们定实现了大多数实现应用中的切入点类型。在我们的应用程序中,直接使用这些Spring预定义的切入点类,设置一定的参数,即可得到一个我们所希望的切入点。
n NameMatchMethodPointcut
全路径:org.springframework.aop.support.NameMatchMethodPointcut
这个是一个通过直接定义方法名称来判断匹配的切入点(Pointcut)实现,这个类使用很简单,可以通过下面几个方法添加匹配的方法。
void setMappedName(String mappedName) -直接指定一个匹配的方法名称。
void setMappedNames(String[] mappedNames) -指定一组(多个)匹配的方法名称。
参数中可以使用“*”来代表0个或多个字符。因此,要在Spring配置文件中配置一个匹配set、get及save开头的方法的切入点,可以像下面这样设置:
<bean id="pointcutBean"
class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames">
<list>
<value>set*</value>
<value>get*</value>
<value>save*</value>
</list>
</property>
</bean>
也可以直接通过mappedName来设置,如下所示:
<bean id="pointcut1"
class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="business*"></property>
</bean>
还可以在程序中,直接使用addMethodName(String name)方法来往对象中手动添加匹配值。如:
NameMatchMethodPointcut pc=new NameMatchMethodPointcut();
pc.addMethodName("set*").addMethodName("get*");
NameMatchMethodPointcut只能解决像什么或是什么该当方法描述与匹配,比如所有像set*的方法。如果没有设置任何值匹配值,默认将匹配所有方法!
n 正则表达式切入点
在前面介绍的NameMatchMethodPointcut中,只能使用通配符“*”来代表0个或多个字符。但在实现应用中,我们的切入点方法名称可能不只这么简单,比如我们需要匹配所有不包含s字符的方法,或者是匹配第一个字符为任意字符、后面一个是e或a、后面是0或多个字符的方法,这时NameMatchMethodPointcut就难以满足这种要求了。不用急,Spring的设计已经考虑到这种情况,因此设计了通过正则表达式来匹配方法名称的切入点实现。跟使用其它的切入点一样,我们直接把相应的切入点处理类拿来使用即可。
正则表达式切入点有两个,一个是JdkRegexpMethodPointcut,这是在直接使用JDK 1.4或更高版本里提供的正则表达式支持功能来进行切入点匹配的;若是JDK比较低的版本,可以使用Jakarta ORO项目(http://jakarta.apache.org/oro/)提供的Perl5兼容的正则表达式处理功能来进行切入点匹配处理,即使用Perl5RegexpMethodPointcut来处理。
正则表达式的功能很多,比如我们可以使用“.”代表任意字符,使用“?”来表示出现0或1次,使用“*”来表示出现0或n次,可以用“[]”来表示一个集合等等,关于正则表达式的使用方法请查阅有关正则表达的资料。总之,使用正则表达式基本上可以满足90%以上的方法名称匹配需求。
切入点:JdkRegexpMethodPointcut
全路径:org.springframework.aop.support.JdkRegexpMethodPointcut
需要在JDK1.4及以上的环境运行,不需要额外的库。
切入点:Perl5RegexpMethodPointcut
全路径:org.springframework.aop.support.JdkRegexpMethodPointcut
需要把jakarta-oro-xx.jar 文件放到classpath上,比如jakarta-oro-2.0.8.jar。
正则表达式切入点关键方法:
void setPattern(String pattern) -设置匹配的正则表达式字符串;
void setPatterns(String[] patterns) -设置一组匹配的正则表达式字符串;
void setExcludedPattern(String excludedPattern) -设置需要排除指定的字符串;
void setExcludedPatterns(String[] excludedPatterns) -设置需要排除的一组字符串。
下面是使用正则表达式切入点的使用示例:
<bean id="pointcutBean"
class="org.springframework.aop.support.Perl5RegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*business.*</value>
<value>.*save.*</value>
</list>
</property>
</bean>
上面的设置将匹配所有包含business及save字符串的方法。假如我们要排除名为business2的方法,则需要设置execludePattern属性值,配置文件如下所示:
<bean id="pointcutBean"
class="org.springframework.aop.support.Perl5RegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*business.*</value>
<value>save*</value>
</list>
</property>
<property name="excludedPattern" value=".*business2"></property>
</bean>
上面的配置以直接换成使用JdkRegexpMethodPointcut来定义,只需要把class中的Perl5RegexpMethodPointcut改成JdkRegexpMethodPointcut即可,其它的都不用变,如下所示:
<bean id="pointcutBean"
class="org.springframework.aop.support.JdkRegexpMethodPointcut
">
<property name="patterns">
<list>
<value>.*business.*</value>
<value>save*</value>
</list>
</property>
<property name="excludedPattern" value=".*business2"></property>
</bean>
n 描述式切入点ExpressionPointcut
在Spring2中,在Pointcut的基础上,引入了一个ExpressionPointcut接口用来通过类似AspectJ中的切入点表达语言来描述切入点(其实当前发布的版本也只提供了一个调用AspectJ的表达式描述语言解析工具来实现表达式描述切入点)。有了ExpressionPointcut,我们可以使用下面更加简单的方式来描述切入点,如execution(* Component.business*(..))表示执行所有Component的业务方法(此处为business打头的方法)。
ExpressionPointcut定义了一个返回描述切入点表达式字符串的方法,getExpression(),完全代码如下
publicinterface ExpressionPointcut extends Pointcut {
String getExpression();
}
Spring2提供了一个ExpressionPointcut的实现,即AspectJExpressionPointcut,提供与AspectJ中切入点描述语法一样表述方式来描述切入点。当然,由于Spring AOP核心引擎仍然是代理实现对方法的拦截,因此,这里只有与方法调用连接点描述相关的切入点表达式才能处理。要使用AspectJExpressionPointcut,需要用到AspectJ中的语法解析处理器,因此需要引入AspectJ的aspectjweaver.jar包。当然Spring AOP在这里只用了这个包的一小部分功能,而且最终仍然使用Spring纯java的方式通过代理在运行时完成织入的,并不依赖于AspectJ的编译器及运行时。
关于AspectJExpressionPointcut的使用很简单,只需要直接使用setExpression方法设置表达式字符串即可!下面的切入点配置表示匹配Component下以business开头的所有方法,不管参数个数!
<bean id="pointcutBean"
class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression"
value="execution(void springroad.demo.chap5.exampleB.Component.business*(..))">
</property>
</bean>
n 切入点集合运算
在Spring AOP中,还支持切入点集合运算,通过集合运算来组合出更多符合实际需要的切入点。下面我来看一个简单的示例:
NameMatchMethodPointcut p1=new NameMatchMethodPointcut();
p1.setMappedName("set*");
JdkRegexpMethodPointcut p2=new JdkRegexpMethodPointcut();
p2.setPattern(".*business.*");
Pointcut p=Pointcuts.union(p1,p2);
在示例中,我们使用Spring提供的Pointcuts类的静态方法union,来把p1及p2两个切入点组合到一起,得到切入点可以用来匹配以set开头或包含business字符串的方法。
5.4.5增强(Advice)
前面我们介绍了切入点,我们知道可以通过多种方式来匹配Spring AOP所要切入的方法。但是具体是在方法的什么位置切入,是方法调用前还是方法调用完成后?切入后做些什么?是改变程序的执行流程还是改变返回值?关于这些问题,就由增强或通知 (Advice)负责处理。
在Spring AOP,增强的类型比较少,都是围绕方法调用连接点展开的,一共有方法调用前增强(MethodBeforeAdvice)、方法调用后增强(AfterReturningAdvice)、环绕增强(MethodInterceptor)、方法调用异常增强(ThrowsAdvice)等几种类型。下面将分别介绍:
n MethodBeforeAdvice
MethodBeforeAdvice的代码如下:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
其中BeforeAdvice是AOP联盟提供的一个标识接口,代码如下:
import org.aopalliance.aop.Advice;
publicinterface BeforeAdvice extends Advice {
}
就像其名称所指的一样,这个增强的位置是在方法之前,因此也称为前置增强。也就是在执行到连接点的时候,首先执行增强中的自定义模块。由于这个增强没有返回值,因此,执行完后下一步将继续执行连接点的方法调用。来看下面的代码:
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
publicclass MethodInvokCount implements MethodBeforeAdvice {
privateintnum=0;
publicvoid before(Method method, Object[] args, Object target)
throws Throwable {
num++;
}
publicint getNum() {
returnnum;
}
}
MethodInvokCount类用来记录连接点方法的调用次数,每当要执行连接点方法时,都会首先调用一次MethodInvokCount中的before方法。
在before方法中,method表示具体连接点,args表示方法所代的参数,target返回该方法所属的目标对象。
如果再执行MethodBeforeAdvice的过程中抛出异常,这将中止拦截器链的进一步执行,异常将沿着拦截器链向上传播。如果异常是非强制检查的(unchecked)或者已经被包含在被调用方法的签名中(译者:即出现在方法声明的throws子句中),它将被直接返回给客户端;否则它将以一个运行时非强制检查(不需要客户端使用try-catch)异常中返回。
n AfterReturningAdvice,返回后增强
方法调用正常返回后增强,也称为后置增强。这是在连接点的方法执行并通过一个语句返回后,执行增强中的自定义模块。AfterReturningAdvice的定义如下:
import java.lang.reflect.Method;
import org.aopalliance.aop.Advice;
publicinterface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
}
在afterReturning方法的几个参数中,returnValue表示连接点的返回值,method为当前方法,args代表方法的参数值,target表示方法所属的实际目标对象。来看下面的代码:
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;
publicclass WriteLogAdvice implements AfterReturningAdvice {
privatefinal Log log=LogFactory.getLog(WriteLogAdvice.class); publicvoid afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
log.info("成功执行了类"+target.getClass()+"的方法"+method.getName()+",返回值为"+returnValue);
}
}
在后置增强的实现afterReturning方法中,调用log的info方法把方法调用的相关情况写入了日志中。在实际应用中,有一些特别重要的业务组件,需要详细记录其每一个方法的执行情况,以便于后期审计,这时可以使用类似的后置增强来实现。
在后置增强的afterReturning方法运行过程中,若中间出现异常,异常将沿着拦截器链返回(或抛出),而不会给客户端继续返回其结果。
n 异常增强ThrowsAdvice(After throwing advice)
Spring中异常增强TrowsAdvice的定义如下:
package org.springframework.aop;
import org.aopalliance.aop.Advice;
publicinterface ThrowsAdvice extends Advice {
}
由定义我们可以看到,ThrowAdvice接口中没有定义任何方法,因此他只是一个标识接口。但是在实际应用中,具体的异常增强实现方法将实现下面的模板方法:
afterThrowing([Method],[args],[target],Throwablesubclass)
在上面的模板方法afterThrowing的几个参数中,中括号的参数是可选的。[Method]用来代表一个方法,[args]用来表示方法调用参数,[target]表示方法所属的实际目标对象,最后一个必选的是异常类型Throwable。
因为一个方法在执行的过程中,可能会抛出各式各样的异常,我们可以在异常增强中分别针对各个异常进行相应的处理。下面例子中设计了一个ErrorLogAdvice异常处理增强,可以用来作为我们系统中对所有异常进行日志记录的增强,可以在这个增强中使用统一的方式集中记录或处理系统中出现的各式各样的异常。
import java.lang.reflect.Method;
import org.springframework.aop.ThrowsAdvice;
import springroad.demo.chap5.exampleB.InValidateUserException;
publicclass ErrorLogAdvice implements ThrowsAdvice {
//当出现Servlet异常时
publicvoid afterThrowing(javax.servlet.ServletException ex)
throws java.lang.Throwable {
log("出现了Servlet异常!");
}
//当用户不合法的时候
publicvoid afterThrowing(Method method, Object[] args,
Object target, InValidateUserException ex)
throws java.lang.Throwable {
log("用户权限不够!" + method.getName());
}
//…
privatevoid log(String s) {
// 把日志信息s写入日志中
}
}
n 环绕增强MethodInterceptor(Around advice)
环绕增强也称为拦截器,是功能比较强大也非常灵活的增强类型,因为他即可改变连接点的程序流程,又可以改变连接点方法的返回值。Spring环绕增强中的相关定义全部使用的AOP联盟规范的接口,我们先来看看MethodInterceptor的完全代码:
publicinterface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
拦截器Interceptor只是一个标识接口,定义如下:
publicinterface Interceptor extends Advice {
}
在上面的定义中,环绕增强需要实现nvoke(MethodInvocation invocation)这个方法,这个方法的参数MethodInvocation中封装了方法调用的相关对象,包括方法名称、方法参数、方法所属的实际目标对象等。另外MethodInvocation中还有一个proceed()方法,用于继续执行连接点本身的调用。
下面来看一个示例:
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
publicclass MethodInterceptorBean implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
validateUser();
beginTransaction();
Object ret=invocation.proceed();
endTransaction();
writeLogInfo();
return ret;
}
publicvoid validateUser()
{
System.out.println("执行用户验证!");
}
publicvoid writeLogInfo()
{
System.out.println("书写日志信息");
}
publicvoid beginTransaction()
{
System.out.println("开始事务");
}
publicvoid endTransaction()
{
System.out.println("结束事务");
}
}
在上面的代码中,我们使用的环绕通知,取代了前面AdviceBean中的MethodBeforeAdvicee及AfterReturningAdvice增强组合所完成的功能。即在连接点方法执行前,先执行用户验证、开启事务操作,然后再通过invocation的proceed()方法继续执行连接点方法的业务逻辑,执行完成后我们进一步执行结束事务及日志记录操作。
上面的示例代码只是演示了在环绕增强中插入自定义的功能,那么,如何通过环绕增强来改变程序流程呢?是不是可以不执行连接点方法直接返回呢?答案是肯定的,你可以通过本章最后的综合示例程序中看到我们使用环绕增强来改变了Hero对象getArmor方法返回值。
当然,在Spring AOP中,一个通知增强模块是否就需要实现上面的接口,从底层原理的角度来说,这是肯定的。然而,Spring 2引入了基于Schema的AOP配置定义,又引入了AspectJ中切面模块相关标签,支持从常规的java类中定义增强实现逻辑。也就是说可以通过配置指定某一个普通类(POJO)的某一个方法用来作为增强的实现逻辑。这样我们就可以更加集中精力编写切面单元模块中的相关业务逻辑,并且可以单独作单元测试,最后再使用配置把其与系统中的其它模块组合到一起来。
5.4.6引介(Introduction)
引介(Introduction)是指在不更改源代码的情况,给一个现有类增加属性、方法,以及让现有类实现其它接口或指定其它父类,从而改变类的静态结构。Spring AOP通过采代理与拦截器的方式来实现的,可以通过拦截器机制使一个实有类实现指定的接口,由于是使用拦截器的机制,因此Spring AOP中引介的底层仍然是增强(Advice)及拦截(Interceptor)。引介不能和任何切入点一起使用,因为它是应用在类级别而不是方法级别。下面是Spring AOP中引介拦截器(IntroductionInterceptor)的定义:
import org.aopalliance.intercept.MethodInterceptor;
publicinterface IntroductionInterceptor extends MethodInterceptor, DynamicIntroductionAdvice {
}
MethodInterceptor是前面讲的环绕增强(Advice),即方法拦截器,DynamicIntroductionAdvice的定义如下:
import org.aopalliance.aop.Advice;
publicinterface DynamicIntroductionAdvice extends Advice {
boolean implementsInterface(Class intf);
}
implementsInterface用来指定实现某个接口。
要实现一个引介拦截器,只需要实现IntroductionInterceptor接口即可,在Spring中,为我们提供了两个IntroductionInterceptor的实现,其中比较常用的是DelegatingIntroductionInterceptor,另外一个是DelegatePerTargetObjectDelegatingIntroductionInterceptor。DelegatingIntroductionInterceptor需要一个准备引入的接口实现的实例作为参数,而DelegatePerTargetObjectDelegatingIntroductionInterceptor需要指定一个接口及实现类作为参数。
如下面的代码可以构造一个引介拦截,使用这个引介拦截的代理对象将会自动拥有UserServiceImpl类所实现的接口及功能。
DelegatingIntroductionInterceptor introduction=new DelegatingIntroductionInterceptor(new UserServiceImpl());
另外Spring还定义了一个用于描述引介信息的IntroductionInfo,内容如下:
publicinterface IntroductionInfo {
Class[] getInterfaces();
}
还有一个封装IntroductionInfo及Advisor的引介增强器(切面),内容如下:
publicinterface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
Spring为提供了IntroductionAdvisor的一个实现DefaultIntroductionAdvisor,在实际应用中可以通过继承该类并实现指定的接口来实现一个引介增强器(切面)。注意:在配置文件中不能在在没有IntroductionAdvisor的情况下使用IntroductionInterceptor。
因为是引介是改变类的静态结构,因此不需要定义切入点Pointcut。在Spring AOP中,引介的使用是通过把一个指定的接口实现添加到代理工厂中,从而使得代理工厂返回的代理对象也实现引介指定的接口。
下面我们举例子说明,首先定义一个简单的业务接口TestService及接口实现TestServiceImpl,也可以直接是类:
publicinterface TestService {
void doSomeThing();
}
publicclass TestServiceImpl implements TestService {
publicvoid doSomeThing() {
System.out.println("执行某样操作!");
}
}
另外有一个UserService接口及其实现UserServiceImpl。我们可以在程序中通过代理使用TestService,如下所示:
ProxyFactory proxy=new ProxyFactory(new TestServiceImpl());
TestService s=(TestService)proxy.getProxy();
s.doSomeThing();
下面,我们在不修改TestServiceImpl的情况下,要让代理工厂返回的代理对象实现UserService接口,这时需要使用引介(Introduction)。
于是创建一个引介拦截,并把拦截添加到代理工厂中,代码如下所示:
DelegatingIntroductionInterceptor introduction=new DelegatingIntroductionInterceptor(new UserServiceImpl());
proxy.addAdvice(introduction);
通过在代理工厂中加入引介拦截,使得代理对象实现UserService接口。于是下面的代码变得合法了:
UserService userService=(UserService)proxy.getProxy();
doDelUser(userService);
下面的语句,输出结果都将为true:
System.out.println(proxy.getProxy() instanceof TestService);
System.out.println(proxy.getProxy() instanceof UserService);
当然,在Spring中,都是通过使用配置文件来配置。于是我们把上面的ProxyFactory换成最常用的ProxyFactoryBean。配置文件api-aop-introduction.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--定义引介切面-->
<bean id="introductionAdvisor"
class="org.springframework.aop.support.DefaultIntroductionAdvisor">
<constructor-arg>
<bean
class="org.springframework.aop.support.DelegatingIntroductionInterceptor">
<constructor-arg>
<bean class="springroad.demo.UserServiceImpl" />
</constructor-arg>
</bean>
</constructor-arg>
</bean>
<bean id="service"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean class="springroad.demo.TestServiceImpl" />
</property>
<property name="proxyTargetClass" value="true" />
<property name="interceptorNames">
<list>
<value>introductionAdvisor</value>
</list>
</property>
</bean>
</beans>
客户端应用示例代码如下:
ApplicationContext context=new org.springframework.context.support.ClassPathXmlApplicationContext("springroad/demo/api-aop-introduction.xml");
TestService s=(TestService)context.getBean("service");
s.doSomeThing();
UserService userService=(UserService)context.getBean("service");
doDelUser(userService);
当然,也可以继承DefaultIntroductionAdvisor来实现一个引介切面,为了定义一个实现UserService接口的引介,可以设计一个UserServiceIntroductionAdvisor,代码如下:
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
publicclass UserServiceIntroductionAdvisor extends DefaultIntroductionAdvisor {
public UserServiceIntroductionAdvisor()
{
super(new DelegatingIntroductionInterceptor(new UserServiceImpl()),UserService.class);
}
}
这时要在配置文件中使用引介,配置文件可以简化成如下的形式:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--定义引介Bean-->
<bean id="introductionAdvisor"
class="springroad.demo.UserServiceIntroductionAdvisor" />
<!-- 配置代理业务Bean -->
<bean id="service"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean class="springroad.demo.TestServiceImpl" />
</property>
<property name="proxyTargetClass" value="true" />
<property name="interceptorNames">
<list>
<value>introductionAdvisor</value>
</list>
</property>
</bean>
</beans>
5.4.7增强器/切面封装(Advisor)
增强器(advisor)可以看作是一个切面的模块化封装,因此也称为切面封装,简称为切面,相当于AspectJ中的Aspect。一个切面封装模块中一般包括一个切入点、一个通知实现。在Spring AOP中, 切面封装主要有大类,一类是用来封装一个切入点与及通知的实现,这种切面封装主要功能是用来改变程序的流程,这类切面里面包含了切入点;而另外一种切面封 装是用来让一个现有的类实现一个接口、甚至指定类继承某一个类、给一个类增加方法等,这种类型的切面封装主要功能是改变了类的静态结构,这类切面封装了引 入Introduction。
Spring AOP最底层的切面封装是Advisor接口,在其下面有两个接口IntroductionAdvisor及PointcutAdvisor。就像他们的名称所定义的一样,IntroductionAdvisor是用来定义引介的切面封装,而PointcutAdvisor是用来定义改变程序流程的切面封装。 Advisor包含了增强(Advice)的定义,其代码如下:
import org.aopalliance.aop.Advice;
publicinterface Advisor {
boolean isPerInstance();
Advice getAdvice();
}
PointcutAdvisor是我们使用得最多的切面封装类型,前面的示例中使用到的AOP功能都是这种改变程序流程、在程序的方法调用中加入自定义流程的切面封装。PointcutAdvisor把切入点与增强封装到了一起,其完全内容如下:
publicinterface PointcutAdvisor extends Advisor {
Pointcut getPointcut();
}
在Spring中,给我们提供预定义的PointcutAdvisor实现,我们直接在应用程序中使用他即可,下面分别看看这些切入点封装实现。
n DefaultPointcutAdvisor-默认的切面封装
全路径:org.springframework.aop.support.DefaultPointcutAdvisor
DefaultPointcutAdvisor类的使用很简单,他有一个advice及pointcut属性,我们可以通过构造子或设值注入方式来配置这个Bean。看下面的构造子注入方式:
<bean id="aspectBean"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<constructor-arg ref="adviceBean"/>
<constructor-arg ref="poingcutBean"/>
</bean>
或者使用设值方法注入,如下所示:
<bean id="aspectBean"
class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="pointcut" ref="pointcutBean"></property>
</bean>
n NameMatchMethodPointcutAdviso方法匹配切入点切面封装
全路径:org.springframework.aop.support.NameMatchMethodPointcutAdvisor
在前面的DefaultPointcutAdvisor中,需要分别设置Advice实现及切入点,才能形成一个完整的切面封装。而NameMatchMethodPointcutAdvisor不需要专门定义一个切入点,直接通过设置方法名称匹配字符串,再指定一个增强实现即可。NameMatchMethodPointcutAdvisor的主要设值方法有:
void setAdvice(org.aopalliance.aop.Advice advice)-设置通知实现;
setClassFilter(ClassFilter classFilter) -设置切入点匹配的类;
void setMappedName(String mappedName) -设置切入点匹配的方法名称,可以使用*通配符;
void setMappedNames(String[] mappedNames) -设置切入点匹配的一组方法名称,可以使用*通配符;
使用NameMatchMethodPointcutAdvisor,前面示例的配置文件可以写成如下的形式:
<bean id="aspectBean"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="mappedName" value="business*"></property>
</bean>
n RegexpMethodPointcutAdvisor-正则表达式切入点切面封装
全路径:org.springframework.aop.support.RegexpMethodPointcutAdvisor
跟NameMatchMethodPointcutAdvisor功能一样,RegexpMethodPointcutAdvisor也是不需要指定专门的切入点,而是通过直接使用正则表达式字符串匹配来指定切入点。RegexpMethodPointcutAdvisor提供以下设值方法:
void setAdvice(org.aopalliance.aop.Advice advice)-设置增强实现;
void setPattern(String pattern) -设置一个正则字符串匹配切入点;
void setPatterns(String[] patterns) -设置一组正则字符串匹配切入点;
void setPerl5(boolean perl5) -强制使用Perl5来处理正则字符串。
下面的配置文件配置了一个RegexpMethodPointcutAdvisor:
<bean id="aspectBean"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="pattern" value=".*Component.business.*"></property>
</bean>
n AspectJExpressionPointcutAdvisor-AspectJ表达式切入点切面封装
这个切面封装同样不需要专门设置切入点,而是直接通过使用AspectJ的切入点表达式来生成切入点,我们只需要给他设置一个切入点表达式,然后设置一个通知实现,即可工作。AspectJExpressionPointcutAdvisor的主要设值方法如下:
void setAdvice(org.aopalliance.aop.Advice advice)-设置增强实现;
void setExpression(String expression) -设置AspectJ切入点描述表达式
void setLocation(String location) -设置位置参数
void setParameterNames(String[] names) -设置表达式中的参数名称
void setParameterTypes(Class[] types)-设置参数类型,与名称对应
下面的配置文件定义了一个AspectJExpressionPointcutAdviso:
<bean id="aspectBean"
class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="expression"
value="execution(void springroad.demo.chap5.exampleB.Component.business*(..))">
</property>
</bean>
5.4.8 ProxyFactoryBean
Spring的AOP横切面切入过程是在运行时进行的,是一个基于拦截机制的AOP框架,因此,其核心为代理的实现。因为只有在代理中才能很好的引入拦截。关于使用代理及拦截机制的AOP具体实现原理,我们将在后面Spring AOP原理一章详细讲解,这里只作简单介绍。
代理的工作机制很简单,就是当我们需要使用某一个类的时候,由Spring通过一定的代理机制,创建一个我们所需类代理对象,代理对象跟实际对象实现相同的接口或者是实际对象类的一个子类。当执行代理对象上的某一个方法时,其交由一个回调对象来处理,而我们可以自定义这个回调对象,从而加入自定义的程序逻辑,即AOP中的增强。因此,在系统中就需要有一个负责根据一定的策略,创建代理对象的代理工厂角色,在Spring的AOP实现中,ProxyFactoryBean正是扮演了这个角色。
ProxyFactoryBean是一个工厂Bean,回顾前面第四章中《Spring IOC》一节关于工厂Bean(即FactoryBean)的介绍,FacotryBean返回的并非是class属性或构造方法创建出来的对象,返回的对象是其通过其getObject()得到。因此,可以使用ProxyFactoryBean来充当任何需要代理的对象的代理工厂。如下面的配置文件,就将返回由target所指定的(在这里是UserServiceImpl)一个代理对象:
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target"><bean class="springroad.demo.UserServiceImpl">
</bean></property>
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
在Spring AOP中,既可以对目标接口进行代理,也可以对目标类创建代理。当要代理一个目标类的时候,Spring自动使用CGLIB(因为JDK的代理机制只能对接口代理)来创建代理,而代理目标为一个或多个接口时,默认将使用JDK的代理来创建代理,当然也可通过设置使用CGLIB来对接口代理。
下面,我们来重点看看ProxyFactoryBean的设值方法:
void setTargetName(String targetName)-设置代理目标对象的bean名称;
void setTarget(Object target)-设备代理目标对象Bean;这个用来设置的是目标对象。在使用代理的时候,我们一般都不会再需要直接使用目标对象,因此,我们可以把目标对象配置成一个内部Bean,这是比较推荐的使用方式。
void setInterceptorNames(String[] interceptorNames)-设置拦截器的名称,在Spring AOP中,这里的拦截器可以是 Advice或Advisor。若是Advice,则所有方法都将拦截,若是Advisor,则将根据其中具体的切入点及通知现实来处理;拦截器的名称可以使用*来代表0或多个任意字符;
void setProxyInterfaces(Class[] proxyInterfaces)-设置代理的目标类接口;可以指定代理一个或多个接口;
void setInterfaces(Class[] interfaces)-设置代理接口,跟proxyInterfaces属性作用一样;
void setProxyTargetClass(boolean proxyTargetClass) -设置是否代理目标类。若为true,则在代理的时候直接使用CGLIB来创建一个目标类的子类;
void setAutodetectInterfaces(boolean autodetectInterfaces) -设置是否自动检测目标类实现的接口,默认为true。因此,在没有设置proxyInterfaces而且没把proxyTargetClass设为true时,Spring将自动检测目标类实现的所有接口,若实现了1个或多个接口,将创建这些这些接口的代理,否则将使用CGLIB来创建代理。
void setBeanClassLoader(ClassLoader classLoader) -设置类加载器;
void setFrozen(boolean frozen)-设置是否固定增强,即在代理工厂被配置之后,是否还允许修改通知;默认值为false,即在代理配置被加载之后,不允许再修改代理的配置。
void setAopProxyFactory(AopProxyFactory apf) -设置是JDK用动态代理、CGLIB还是其它代理策略,缺省实现将根据情况自动选择动态代理(有接口)或者CGLIB。
void setSingleton(boolean singleton) -设置在每次调用代理工厂的getObject时,是否应该返回同一个对象。工厂是否应该返回同一个对象,不论方法getObject()被调用的多频繁。多个FactoryBean实现都提供了这个方法。缺省值是true。如果你希望使用有状态的增强,可以把单例属性的值设置为false来使用原型通知。
public void setOptimize(boolean optimize)-是否使用CGLIB代理优化策略。仅用于CGLIB代理;对于JDK动态代理(缺省代理)无效。除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。
void setExposeProxy(boolean exposeProxy) -决定当前代理是否被保存在一个ThreadLocal中以便被目标对象访问。
在ProxyFactoryBean的众多JavaBean属性中,target或(targetName)是必须指定的interceptorNames也是必须指定,否则就没必要使用代理了!其它的属性可以根据实际情况选择使用。
使用抽象Bean配置来简化代理设置
如果使用Spring API的方式来开发AOP应用,则要求每一个需要插入横切模块的业务Bean都需要使用ProxyFactoryBean来进行配置,也就是要配置多个ProxyFactoryBean,使得配置文件中的内容过于烦琐。此时我们可以借助于Spring提供的抽象Bean配置,来配置一些模板Bean。这样其它的业务Bean的配置直接继承模板Bean配置即可,大致可以分成两个步骤来实现。
第一步、配置一个或多个抽象Bean,用于充当配置模板,如下所示:
<bean id="baseProxyParent"
class="org.springframework.aop.framework.ProxyFactoryBean"
abstract="true">
</bean>
<bean id="businessProxyParent" parent="baseProxyParent"
abstract="true">
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
第二步、通过继承抽象Bean的配置,使用简化的方式配置业务Bean,如下所示,
<bean id="userService" parent="businessProxyParent">
<property name="target">
<bean class="springroad.demo.UserServiceImpl"></bean>
</property>
</bean>
下面是一个使用了抽象(模板)代理配置的完整示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- 基本ProxyFactoryBean配置-->
<bean id="baseProxyParent"
class="org.springframework.aop.framework.ProxyFactoryBean"
abstract="true">
</bean>
<!-- 业务层代理ProxyFactoryBean配置-->
<bean id="businessProxyParent" parent="baseProxyParent"
abstract="true">
<property name="interceptorNames">
<list>
<value>aspectBean</value>
</list>
</property>
</bean>
<!-- 数据访问层代理ProxyFactoryBean配置-->
<bean id="daoProxyParent" parent="baseProxyParent"
abstract="true">
<property name="interceptorNames">
<list>
<value>daoAspectBean</value>
</list>
</property>
</bean>
<!-- 定义通知实现-->
<bean id="adviceBean" class="springroad.demo.RealDelAdvice"></bean>
<!--定义一个切面封装-->
<bean id="aspectBean"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice" ref="adviceBean"></property>
<property name="mappedName" value="del*"></property>
</bean>
<!--定义业务层的Bean,继承了businessProxyParent的配置属性-->
<bean id="userService" parent="businessProxyParent">
<property name="target">
<bean class="springroad.demo.UserServiceImpl"></bean>
</property>
</bean>
<bean id="roleService" parent="businessProxyParent">
<property name="target">
<bean class="springroad.demo.RoleServiceImpl"></bean>
</property>
</bean>
<!--定义数据访问层的Bean-->
<bean id="userDao" parent="daoProxyParent">
<property name="target">
<bean class="springroad.demo.UserDaoImpl"></bean>
</property>
</bean>
<!-- ... -->
</beans>
当然,还可以使用前面介绍的通过自动代理来简化Spring AOP配置。
5.5 示例:模拟Warcraft游戏
通过前面的几节,我们应该对AOP的概念及AOP的一些使用方法有了一定的认识,现在通过一个综合、完整的模拟warcraft游戏示例,来进一步演示Spring中AOP的应用。本示例从系统核心关注点(主模块)的设计开始,到引入横切关注点问题,并通过Spring2提供的几种方式来实现横切关注点的需求,每一个部分都作了比较详细的讲述,可以让我们灵活掌握Spring2中的AOP应用。
5.5.1示例简介
相信大家都玩过warcraft3的吧,这里就借Warcraft中的一些角色,做一个模拟的小游戏。程序中将通过AOP编程方法,来解决一些具有横切性质的问题。示例程序主要是实现一个战斗系统,有战场WarField、战场中的角色(游戏主角,也称为英雄)Hero、及角色所持有的物品或技能(Props)三个部分,道具又进一步分成增加攻击性质的道具系列(AttackProp)以及增加防御(护甲)的道具系列(ArmorProp)。
5.5.2核心关注点及系统主模块
首先来主角英雄Hero进行抽象。一个英雄大致有名称(name)、生命值(health)、攻击力(damage)、防御力(armor)、物品或技能(AuraAndSkill)等属性;另外还有一个用于攻击其它角色的attack方法,该方法返回值表示攻击是否成功;当然,英雄还应该是可复制的,因此还有一个clone方法,Hero接口的内容如下所示:
package springroad.demo.chap5.wow;
import java.util.List;
//英雄的抽象
publicinterface Hero extends java.lang.Cloneable {
boolean attack(Hero target);//攻击别人
int getHealth();//英雄当前生命值
void setHealth(int health);
int getDamage();//伤害值
void setDamage(int damage);
List getAuraAndSkill();//持有物品或技能
void setAuraAndSkill(List auraAndSkill);
int getArmor();//防御力
void setArmor(int armor);
String getName();//名称
void setName(String name);
Hero clone();//克隆一个英雄
}
在本例中,英雄的实现HeroImpl比较简单,代码如下所示:
package springroad.demo.chap5.wow;
import java.util.List;
import java.util.Random;
publicclass HeroImpl implements Hero {
private String name;// 名称
privateinthealth;// 生命值
privateintdamage;// 被攻击
private List auraAndSkill=new java.util.ArrayList();// 拥有物品
privateintarmor;// 护甲
private Random random = new Random();
public HeroImpl() {
}
public HeroImpl(int damage) {
this.damage = damage;
}
publicint getArmor() {
returnarmor;
}
publicvoid setArmor(int armor) {
this.armor = armor;
}
publicboolean attack(Hero target) {
if (this.health < 0)
returnfalse;
returnrandom.nextInt(10) <= 6;
}
publicint getDamage() {
returnthis.damage;
}
publicint getHealth() {
returnthis.health;
}
public List getAuraAndSkill() {
returnauraAndSkill;
}
publicvoid setAuraAndSkill(List auraAndSkill) {
this.auraAndSkill = auraAndSkill;
}
publicvoid setDamage(int damage) {
this.damage = damage;
}
publicvoid setHealth(int health) {
this.health = health;
}
public String getName() {
returnname;
}
publicvoid setName(String name) {
this.name = name;
}
public String toString() {
return"[英雄名称:" + this.name + ",生命值:" + this.health + ",基本防御:"
+ this.armor + "]";
}
//实现简单的浅clone
public Hero clone() {
try{
return (Hero)super.clone();
}
catch(Exception e)
{
returnnull;
}
}
}
我们来注意看attack方法中的内容,其中“this.health < 0”用于判断英雄是否已经死亡,而返回语句“return random.nextInt(10) <= 6”表示有60%以上的攻击成功率。这里没有攻击动作的具体实现,因为作战的规则及策略将由具体的战场来定,英雄只有放到战场中才能开始有意义的战斗。当然,也可以直接在attack方法中实现攻击的动作,并计算命中目标后的伤害值等,可参考本章前面AspectJ一节所用的回合格斗小游戏示例。
英雄持有的物品或技能主要有两种:增加攻击的物品或技能,增加防御的物品或技能。我们分别使用AttackProp及ArmorProp两个接口来表示,AttackProp有一个获得攻击力的方法getAttack,ArmorProp有一个获得防御力的方法getArmor。两个接口的内容如下面的代码所示:
package springroad.demo.chap5.wow;
//攻击道具或技能
publicinterfaceAttackProp {
int getAttack(Hero hero);
}
package springroad.demo.chap5.wow;
//防御道具或技能
publicinterface ArmorProp {
int getArmor(Hero hero);
}
凡是实现了AttackProp或ArmorProp接口的物品或技能,都可以让主角持有。在本例中,我们提供了以下几个AttackProp的实现,分别是重击技能(Bash)、强击光环(TrueshotAura)、野兽卷轴(ScrollOftheBeast);提供了两个两个ArmorProp的实现,分别是防御光环(DevotionAura)、防御卷轴(ScrollOfProtection),你也可以自己往系统中增加各种各样的物品及技能。
技能重击Bash的源码如下所示:
package springroad.demo.chap5.wow;
import java.util.Random;
//重击技能,具有20%的概率,可以增加英雄的攻击力20点
publicclass Bash implements AttackProp {
privatebooleanhaveHit;
private Random random = new Random();
publicint getAttack(Hero hero) {
if (bigHit()) {
haveHit = true;
} else {
haveHit = false;
}
return (haveHit ? 20 : 0);
}
publicboolean bigHit() {
int rat = random.nextInt(10);
return (rat <= 2);
}
public String toString() {
return"重击技能," + (haveHit ? "伤害提高了20点" : "未使出来");
}
}
物品野兽卷轴ScrollOftheBeast的源码如下所示:
package springroad.demo.chap5.wow;
//野兽卷轴,用于增加伤害百分比
publicclassScrollOftheBeastimplements AttackProp {
publicint getAttack(Hero hero) {
return (int)(hero.getDamage()*0.25);
}
public String toString() {
return"物品野兽卷轴,攻击增加了25%";
}
}
技能强击光环TrueshotAura的源码如下所示:
package springroad.demo.chap5.wow;
//强击光环,增加英雄攻击伤害百分比
publicclassTrueshotAuraimplements AttackProp {
publicint getAttack(Hero hero) {
return (int)(hero.getDamage()*0.1);
}
public String toString()
{
return"强击光环技能,攻击力增加了10%";
}
}
技能防御光环DevotionAura的源码如下所示:
package springroad.demo.chap5.wow;
//光环增加,相当于英雄的护甲,也即增加防御力
publicclassDevotionAuraimplements ArmorProp {
publicint getArmor(Hero hero) {
return 1;
}
public String toString()
{
return "专注光环技能,护甲增加了1点!";
}
}
物品防御卷轴ScrollOfProtection的源码如下所示:
package springroad.demo.chap5.wow;
//卷轴增强,增加英雄的防御能力
publicclass ScrollOfProtection implements ArmorProp {
publicint getArmor(Hero hero) {
return 2;
}
public String toString() {
return"物品保护卷轴,护甲增加了2点";
}
}
战场中的战斗一般有对战与混战两种。对战是指有明确敌对的双方,如攻方对守方、侵略者对反侵略者、绿军对蓝军等都属于对战,参与对战的所有角色都将首先归到战斗的某一方(或称为友军),战斗过程中不能主动杀本方的角色(误杀除外)。而混战则没有这些限制,属于群雄并起,能者称霸、强者生存的性质,战场中的角色见到谁都可以主动发动攻击,谁能战斗到最后谁就是胜利者。除了基本的对战与混战以外,战场根据具体的地理位置、战争目的及参与者不同,也会有不同的实现。因此,我们对战场也作了简单抽象,使用WarField来表示战场,该接口的内容如下:
package springroad.demo.chap5.wow;
import java.util.Collection;
//战斗战场
publicinterface WarField {
void join(Hero hero);//角色加入战场
void begin();//战场战斗开始
void attack(Hero source, Hero target);//战场中的source角色攻击target角色
void setHeros(Collection heros);//设置战场中的角色
Collection getHeros();//得到战场中的角色
}
这个例子中,我们只实现了一个基于混战模式的战场WarFieldScuffle。WarFieldScuffle中主要包含一个集合类型的heros属性,用于存放战场中的角色,join方法用来往战场中增加角色,attack是角色战斗的实际逻辑(即攻击动作的处理),begin用来启动战场中的战斗,另外还有一个私有RandomAttack方法用来随机挑选攻击目标。WarFieldScuffle的代码如下所示:
package springroad.demo.chap5.wow;
import java.util.Collection;
import java.util.HashSet;
import java.util.Random;
publicclass WarFieldScuffle implements WarField{
private Collection heros = new HashSet();
private Random random = new Random();
publicvoid join(Hero hero) {
heros.add(hero);
}
publicvoid begin() {
while (heros.size() > 1) {
Hero[] hs = new Hero[heros.size()];
java.util.Iterator it = heros.iterator();
heros.toArray(hs);
// 开始一轮战斗,每人发一次招,攻击的对象随机产生
for (int i = 0; i < hs.length; i++) {
attack(hs[i], hs[RandomAttack(i, hs.length)]);
System.out.println();
}
// 把已挂的角色清理出战场
for (int i = 0; i < hs.length; i++) {
if (hs[i].getHealth() < 0)
heros.remove(hs[i]);
}
}
}
publicvoid attack(Hero source, Hero target) {
if (source.getHealth() < 0 || (!source.attack(target)))
return;
int attack = source.getDamage();
java.util.Iterator it = source.getAuraAndSkill().iterator();
StringBuffer s = new StringBuffer("基本伤害:" + attack + ";英雄持有以下攻击道具:");
while (it.hasNext()) {
Object p = it.next();
if (p instanceof AttackProp) {
attack += ((AttackProp) p).getAttack(source);
s.append(p + ";");
}
}
int damage = (attack - target.getArmor());
target.setHealth(target.getHealth() - damage);
s.append(s).append("[实际攻攻击力:").append(attack).append("]");
s.append("--实际伤害点数:").append(damage);
System.out.println(s.toString());
if (target.getHealth() < 0)
System.out.println("英雄 " + target.getName() + " 挂了");
}
// 计随机选择战场中的一个英雄来攻击
privateint RandomAttack(int current, int max) {
int rat = random.nextInt(max);
if (rat == current)
return RandomAttack(current, max);
else
return rat;
}
publicvoidsetHeros(Collection heros) {
this.heros = heros;
}
public Collection getHeros() {
returnheros;
}
}
以上便是我们整个示例程序的核心部分(也即核心关注点或业务逻辑),把各个部件组合到一起,我们可以得到一个如“图5-13”所示的UML类图:
图5-13 模拟Warcraft游戏主模块的UML类图
下面我们可以写一个简单的客户端程序来启动这个小游戏,代码如下所示:
package springroad.demo.chap5.wow;
publicclass MainTest {
publicstaticvoid main(String[] args) {
WarField war=newWarFieldScuffle();
//定义英雄1-山丘之王
Hero h1=new HeroImpl();
h1.setName("10级山丘之王");
h1.setHealth(500);
h1.setArmor(1);
h1.setDamage(60);
h1.getAuraAndSkill().add(new Bash());
h1.getAuraAndSkill().add(new DevotionAura());
h1.getAuraAndSkill().add(new ScrollOftheBeast());
h1.getAuraAndSkill().add(new TrueshotAura());
//定义英雄2-Priestess of the Moon
Hero h2=new HeroImpl();
h2.setName("Priestess of the Moon");
h2.setHealth(500);
h2.setArmor(1);
h2.setDamage(60);
h2.getAuraAndSkill().add(new DevotionAura());
h2.getAuraAndSkill().add(new ScrollOftheBeast());
h2.getAuraAndSkill().add(new TrueshotAura());
//英雄进入战场并启动战斗
war.join(h1);
war.join(h2);
war.begin();
}
}
当然我们也可以使用Spring来组织装配我们的这个应用程序,实现在配置文件中灵活的角色属性及战场配置。下面是一个配置了三个角色的Spring配置示例aopdemo-1.xml。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--定义英雄MK-->
<bean id="MK" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="2" />
<property name="health" value="500" />
<property name="name" value="Mountain King" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
</list>
</property>
</bean>
<!--定义英雄POM-->
<bean id="POM" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="Priestess of the Moon" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<!--定义英雄10级山丘之王-->
<bean id="superHero" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="10级山丘之王" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.ScrollOftheBeast" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<bean id="WarField" class="springroad.demo.chap5.wow.WarFieldScuffle">
<property name="heros">
<set>
<ref bean="MK" />
<ref bean="POM" />
<ref bean="superHero" />
</set>
</property>
</bean>
</beans>
此时,使用这个程序的客户端代码就变得简单多了,如下所示:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"/springroad/demo/chap5/wow/aopdemo-1.xml");
WarField war = (WarField) context.getBean("WarField");
war.begin();
}
仔细观察这个系统,你会发现,游戏的防御系统功能没有启用,也即不管英雄持有多少个防御物品或技能,都不会影响其防御值。
5.5.3横切关注点需求引入及实现
假如上面的程序主模块已经固定,现在我们需要对系统中加入以下几个功能。
1、 记录所有战场战斗情况,开始时间,最终胜利者等;
2、 记录各个英雄作战详细情况,包括交战发生的时间、交战双方、回合、攻击结果等;
3、另外开通防御系统功能,也即在每一次访问Armor属性的时候,还要加上角色身上的防御物品及技能的值。
4、我们要开通一个超级角色功能,也即满级的英雄都比较厉害。当超级角色使用一些具有一定成功概率的技能(比如Bash)的时候,具有100%的成功率。
针对第一个功能需要,我们发现战场的开始及结束只需要在调用WarField的begin方法前后分别插入战斗开始及战斗结果的代码即可。本例中只有一个战场实现,而实际中的战场是很多的,假如有几十种不同的战场实现,就需要在每一个WarField实现的begin前后插入相同的代码,这将是一个不小的工作量。因此,对每个战场战斗情况的关注可以归为横切性质的关注点,可以使用AOP编程方法来解决这一个问题,即引入一个专用于记录战场战斗情况的横切模块来解决记录并汇报战场战斗情况的信息。由于涉及到战斗开始及最终结束,也即需要在begin方法的前后进行处理,因此,可以使用环绕(around)增强来解决这个问题。
WarAdvice的代码如下:
package springroad.demo.chap5.wow;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
publicclass WarAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
WarField war=(WarField)invocation.getThis();
java.util.Collection heros=war.getHeros();
if (heros == null || heros.size() < 1)
{
System.out.println("战场中没有战士!");
returnnull;
}
java.util.Iterator ih = heros.iterator();
while (ih.hasNext())
System.out.println(ih.next());
System.out.println("战斗开始......");
Object ret=invocation.proceed();//调用连接点
java.util.Iterator it = heros.iterator();
Hero winner = (Hero) it.next();
System.out.println("/n最终胜利者:" + winner.getName());
return ret;
}
}
在WarAdvice这个增强中,invocation.proceed()表示调用连接点上,这之前用于输出站斗开始信息,而之后输出了最终的胜利者,使用invocation.getThis()来得实际WarField对象。
同需求1一样,对于需求2,记录各个英雄作战的详细情况,可以通过在Hero的attack方法调用后加入用于记录战斗情况的代码来实现,也即用后置(after)增强来实现。用于Hero的attack方法后置增强的全部代码如下所示:
package springroad.demo.chap5.wow;
import java.lang.reflect.Method;
//记录英雄之间攻击情况
publicclass GameRecordAdvice implements org.springframework.aop.AfterReturningAdvice {
privateintattackCount=0;
publicvoid afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
Hero s=(Hero)target;
Hero t=(Hero)args[0];
attackCount++;
boolean ret=(Boolean)returnValue;
System.out.println("第"+attackCount+"回合:玩家["+s.getName()+"]向["+t.getName()+"]发动攻击![命中:"+(ret?"成功":"失败")+"]");
if(ret)System.out.println("生命值--["+s.getName()+":"+s.getHealth()+"]"+"-["+t.getName()+":"+t.getHealth()+"]");
}
}
在上面的代码中,使用target可以得到发出攻击的对象,而方法参数数组中的第1个参数(args[0])得到攻击目标,然后再根据连接点(本例中主要针对Hero的attack方法)的返回值,输出相应的战斗记录信息。
对于需求3,我们不能直接改变Hero的armor属性值,因为英雄所持有的物品都是随机的,而且是多变的,armor是一个基本属性值。然而我们可以在其它对象调用Hero的getArmor方法的时候,计算Hero身上防御性质的物品或技能的防御值总合,并与armor属性值相加,然后结果返回给调用者。虽然本例中只有不多的几个地方调用Hero的getArmor方法,但实际中调用Hero的getArmor方法的地方肯定会很多。因此,我们同样可以使用AOP编程方法来把这个需求封装到一个横切模块中。由于需要改变getArmor方法的返回值,因此可以使用一个环绕(around)增强来解决这个问题。getArmor增强的全部代码如下所示:
package springroad.demo.chap5.wow;
import org.aopalliance.intercept.MethodInvocation;
publicclass ArmorAdvice implements org.aopalliance.intercept.MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Hero hero=(Hero)invocation.getThis();
//System.out.println("执行拦截!");
java.util.Iterator it=hero.getAuraAndSkill().iterator();
int armorValue=(Integer)invocation.proceed();
String s=hero.getName()+ "--基本护甲:"+armorValue+";";
while(it.hasNext())
{
Object p=it.next();
if(p instanceof ArmorProp)
{
armorValue+=((ArmorProp)p).getArmor(hero);
s+=p+";";
}
}
System.out.println(s+"[实际防御:"+armorValue+"]");
return armorValue;
}
}
在上面的代码中,先通过调用invocation.proceed()得到连接点(本例是Hero的getArmor方法)的返回值,然后再通过target(Hero)的防御型物品或技能的属性,把防御值加到返回结果中返回。
对于需求4,我们可以在系统中增加一个用于表示超级角色的超级接口,新增加的道具或技能实现能识别这个接口,只要发现使用当前道具的角色属于超级角色,则把出招成功率改为100%即可。超级接口的内容如下:
package springroad.demo.chap5.wow;
publicinterface SuperHero {
}
修改重击技能Bash,使得其可以识别超级接口,并调整出招成功率,修改后的getAttack方法如下:
publicint getAttack(Hero hero) {
if (bigHit() || (hero instanceof SuperHero)) {
haveHit = true;
} else {
haveHit = false;
}
return (haveHit ? 20 : 0);
}
在if语句中可以看到,只要hero属于SuperHero,则haveHit为true,将增加20点攻击。
现在的问题是,我们如何在现有系统中,不改组件的源代码得到一个实现了SuperHero接口的Hero对象? HeroImpl不能直接实现SuperHero接口,否则所有的角色都属于超级角色了。我们想到,在AOP编程方法中,可以通过引介来改变一个类的静态结构,让一个已有的Java类实现另外的接口。因此,这里我们直接使用Spring中的引介,再通过在配置文件把引介配置给相应的超级角色即可。这个示例中,引介的使用很简单,直接通过DefaultIntroductionAdvisor扩展实现,代码如下:
package springroad.demo.chap5.wow;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
publicclass SuperHeroIntroductionAdvisor extends DefaultIntroductionAdvisor {
public SuperHeroIntroductionAdvisor() {
super(new DelegatingIntroductionInterceptor(new SuperHero(){}), SuperHero.class);
}
}
上面所定义的3个增强(Advice)及1个引介器的总体UML结构图如“图5-14”所示,其中接口均为Spring框架或AOP联盟所定义的。
图5-14 模拟Warcraft游戏用到的AOP API类的UML结构图
最后,我们需要在Spring配置文件中组装切入点及切面,配置完整的AOP,并使用ProxyFactoryBean来定义需要拦截的英雄及战场。修改后的完整配置文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<!-- 技能及道具 -->
<bean id="bash" class="springroad.demo.chap5.wow.Bash" />
<bean id="devotionAura"
class="springroad.demo.chap5.wow.DevotionAura" />
<bean id="scrollOfProtection"
class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean id="scrollOftheBeast"
class="springroad.demo.chap5.wow.ScrollOftheBeast" />
<bean id="trueshotAura"
class="springroad.demo.chap5.wow.TrueshotAura" />
<!--定义AOP切面(包括引介器及增强器) -->
<bean id="superHeroIntroduction"
class="springroad.demo.chap5.wow.SuperHeroIntroductionAdvisor" />
<bean id="armorAspect"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="mappedName" value="getArmor" />
<property name="advice">
<bean class="springroad.demo.chap5.wow.ArmorAdvice"></bean>
</property>
</bean>
<bean id="warAspect"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="mappedName" value="begin" />
<property name="advice">
<bean class="springroad.demo.chap5.wow.WarAdvice"></bean>
</property>
</bean>
<bean id="gameRecordAspect"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="mappedName" value="attack" />
<property name="advice">
<bean class="springroad.demo.chap5.wow.GameRecordAdvice"></bean>
</property>
</bean>
<!--代理配置模板-->
<bean id="baseHeroProxy"
class="org.springframework.aop.framework.ProxyFactoryBean"
abstract="true">
<property name="proxyInterfaces"
value="springroad.demo.chap5.wow.Hero" />
<property name="interceptorNames">
<list>
<value>gameRecordAspect</value>
<value>armorAspect</value>
</list>
</property>
</bean>
<!--定义英雄MK-->
<bean id="MK" parent="baseHeroProxy">
<property name="target">
<bean class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="2" />
<property name="health" value="500" />
<property name="name" value="Mountain King" />
<property name="auraAndSkill">
<list>
<ref bean="bash" />
<ref bean="scrollOfProtection" />
</list>
</property>
</bean>
</property>
</bean>
<!--定义英雄POM-->
<bean id="POM" parent="baseHeroProxy">
<property name="target">
<bean class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="Priestess of the Moon" />
<property name="auraAndSkill">
<list>
<ref bean="devotionAura" />
<ref bean="scrollOftheBeast" />
<ref bean="trueshotAura" />
</list>
</property>
</bean>
</property>
</bean>
<!--定义英雄superHero-->
<bean id="superHero" parent="baseHeroProxy">
<property name="target">
<bean class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="10级山丘之王" />
<property name="auraAndSkill">
<list>
<ref bean="bash" />
<ref bean="devotionAura" />
<ref bean="scrollOftheBeast" />
<ref bean="trueshotAura" />
</list>
</property>
</bean>
</property>
<property name="interceptorNames">
<list merge="true">
<value>superHeroIntroduction</value>
</list>
</property>
</bean>
<bean id="WarField"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<bean class="springroad.demo.chap5.wow.WarFieldScuffle">
<property name="heros">
<set>
<ref bean="MK" />
<ref bean="POM" />
<ref bean="superHero" />
</set>
</property>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>warAspect</value>
</list>
</property>
</bean>
</beans>
客户端代码:
package springroad.demo.chap5.wow;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
publicclass MainTest {
publicstaticvoid main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"/springroad/demo/chap5/wow/aopdemo.xml");
WarField war = (WarField) context.getBean("WarField");
war.begin();
}
}
运行这个程序,得到与“图5-15”相似的结果。
图5-15 模拟Warcraft游戏使用Spring AOP后运行结果截图
从运行结果中可以看到,战斗中有了比较详尽的战斗情况(日志)显示;防御系统也启动了,并在每次对战中显示被攻击方的防御情况;另外属于超级角色的“10级山丘之王”的重击技能(Bash)技能具有100%的出招成功率,因此其攻击是稳定的;而同样具有重击技能(Bash)技能的英雄“Priestess of the Moon”的攻击显得就不那么稳定了。
由此可见,我通过引入AOP编程方法,实现了上面所说的4个属于横切关注点的需求。当然,由于使用的是Spring老式的API配置方式,尽管我们通过模板(抽象)Bean配置来简化了一部分配置文件,然而配置文件仍然显得非常繁琐。
5.5.4使用AspectJ注解支持的AOP实现
前面说过,在Spring2中,引入了AspectJ的切入点表达式解析引擎,并提供了对AspectJ的集成机制,配合使用JDK5的注解功能,可以大简化AOP应用程序的开发。下面我们将使用Aspect提供的注解标签,来实现上面提出的横切关注点中的1-3的需求,即实现角色及战场战斗情况的详细记录,并开启防御系统功能。
由于这些横切关注点的问题都比较简单,而且都是针对模拟战斗组件的增强,因此,我们可以把解决三个横切需求的功能封装到一个切面AspectModule中,AspectModule是一个普通的POJO,代码如下所示:
package springroad.demo.chap5.wow;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
@Aspect
publicclass AspectModule {
privateintattackCount=0;
//用于针对Hero的attak方法进行拦截,记录角色相互攻击情况
@AfterReturning(pointcut="execution(* Hero.attack(Hero))",returning="ret")
publicvoid afterHeroAttack(JoinPoint thisJoinPoint,boolean ret)
{
Hero s=(Hero)thisJoinPoint.getTarget();
Hero t=(Hero)thisJoinPoint.getArgs()[0];
attackCount++;
System.out.println("第"+attackCount+"回合:玩家["+s.getName()+"]向["+t.getName()+"]发动攻击![命中:"+(ret?"成功":"失败")+"]");
if(ret)System.out.println("生命值--["+s.getName()+":"+s.getHealth()+"]"+"-["+t.getName()+":"+t.getHealth()+"]");
}
//用于针对WarField的begin方法进行拦截,
@Around("execution(void WarField.begin())")
public Object aroundWarFieldBegin(ProceedingJoinPoint theJoinPoint)throws Throwable
{
WarField war=(WarField)theJoinPoint.getThis();
java.util.Collection heros=war.getHeros();
if (heros == null || heros.size() < 1)
{
System.out.println("战场中没有战士!");
returnnull;
}
java.util.Iterator ih = heros.iterator();
while (ih.hasNext())
System.out.println(ih.next());
System.out.println("战斗开始......");
Object ret=theJoinPoint.proceed();//调用连接点
java.util.Iterator it = heros.iterator();
Hero winner = (Hero) it.next();
System.out.println("/n最终胜利者:" + winner.getName());
return ret;
}
//用于针对WarField的getArmor方法进行拦截,
@Around("execution(public int Hero.getArmor())")
public Object aroundHeroGetDamage(ProceedingJoinPoint theJoinPoint)throws Throwable
{
Hero hero=(Hero)theJoinPoint.getThis();
java.util.Iterator it=hero.getAuraAndSkill().iterator();
int armorValue=(Integer)theJoinPoint.proceed();
String s=hero.getName()+ "--基本护甲:"+armorValue+";";
while(it.hasNext())
{
Object p=it.next();
if(p instanceof ArmorProp)
{
armorValue+=((ArmorProp)p).getArmor(hero);
s+=p+";";
}
}
System.out.println(s+"[实际防御:"+armorValue+"]");
return armorValue;
}
}
通过上面的代码可以看出,AspectModule这个切面模块不用实现任何框架指定的接口,这个POJO中将引入aspectj的注解标签库,在类名前面使用@Aspect标签表示这个类是一个切面。在切面中,我们使用@AfterReturning及@Around标识横切关注点中的逻辑,即增强,标签中的参数pointcut的值即为切入点表示式语言。比如:
@AfterReturning(pointcut="execution(* Hero.attack(Hero))",returning="ret")
publicvoid afterHeroAttack(JoinPoint thisJoinPoint,boolean ret)
{
…
}
这段代码定义了一个切入点,以及一个方法后置增加,具体的切入点含义由pointcut参数的值“execution(* Hero.attack(Hero))”决定,这里表示Hero对象的attack(Hero)方法调用;returning="ret"用来标识返回参数,字符串ret与下面的方法定义中的ret参数对应。
在afterHeroAttack方法中,第一个参数代表当前的连接点,类型为org.aspectj.lang.JoinPoint
,通过这个参数我们可以得到当前连接点的相关信息,如目标对象、方法的参数等,第二个参数ret表示方法调用的返回值。在afterHeroAttack中,通过连接点的getTarget()及getArgs()方法得到当前连接点的目标对象,以及方法的参数,由于参数为数组类型,而且我们也知道Hero的attack方法只带有一个参数,因此可以直接使用(Hero)thisJoinPoint.getArgs()[0]得到attack方法中的参数。如下所示:
Hero s=(Hero)thisJoinPoint.getTarget();
Hero t=(Hero)thisJoinPoint.getArgs()[0];
我们再来看针对getArmor的环绕增强的实现,代码如下:
@Around("execution(public int Hero.getArmor())")
public Object aroundHeroGetDamage(ProceedingJoinPoint theJoinPoint)throws Throwable
{
…
}
通过@Around标签来标识一个环绕增强,标签中的参数即为切入点,"execution(public int Hero.getArmor())"表示Hero的getArmor方法调用。在下面的增强的实现方法中,包含一个类型为org.aspectj.lang.ProceedingJoinPoint的参数,表示环绕增强的连接点,我们可以通过该连接点得到调用的相关信息,如目标对象、参数值、方法名等,更重要的是通过ProceedingJoinPoint的proceed()来启动连接点的执行。在这个示例中,通过语句int armorValue=(Integer)theJoinPoint.proceed()得到Hero对象的armor原始属性值。然后再根据当前Hero所持有防御类物品或技能进一步计算当前对象的附加防御值,最后把原始属性值与附加防御值加在一起返回给调用者。
针对WarField.begin()的拦截比较简单,在切面中通过如下方式定义:
//用于针对WarField的begin方法进行拦截,
@Around("execution(void WarField.begin())")
public Object aroundWarFieldBegin(ProceedingJoinPoint theJoinPoint)throws Throwable
{
…
}
在完成了切面模块的开发以后,我们可以直接在配置文件中使用<aop:aspectj-autoproxy/>标签来开启自动代理,然后在配置文件中声明一个普通的AspectModule Bean即可。配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<!-- 使用@AspectJ自动代理-->
<aop:aspectj-autoproxy/>
<!--使用普通Bean方式定义一个切面模块-->
<bean id="aspectModuel" class="springroad.demo.chap5.wow.AspectModule"></bean>
<!--定义英雄MK-->
<bean id="MK" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="2" />
<property name="health" value="500" />
<property name="name" value="Mountain King" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
</list>
</property>
</bean>
<!--定义英雄POM-->
<bean id="POM" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="Priestess of the Moon" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<!--定义英雄10级山丘之王-->
<bean id="superHero" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="10级山丘之王" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.ScrollOftheBeast" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<bean id="WarField" class="springroad.demo.chap5.wow.WarFieldScuffle">
<property name="heros">
<set>
<ref bean="MK" />
<ref bean="POM" />
<ref bean="superHero" />
</set>
</property>
</bean>
</beans>
这个配置文件比aopdemo.xml这个文件内容少了很多,基本上全部是普通的Bean配置,而且切面封装模块是一个简单的POJO,也不用依赖于Spring框架,使得整个系统的结构更加清晰及简洁。当然,由于需要在AspectJ的切面模块通过用AspectJ的注解标签来定义切面相关信息,所以对AspectJ仍然有一定依赖。
注意:由于Spring底层使用运行时字节码生成机制,运行上面的程序需要在ClassPath上添加asm相关包,如asm-2.2.2.jar、asm-commons-2.2.2.jar等,当然还要添加AspectJ的aspectjweaver.jar。
运行程序可以发现,我们实现了前面所列出的横切关注点1、2、3的需求。也即通过AOP切面达到了对战斗系统的记录,并通过AOP切面启动了防御系统功能。
这里没有实现需求4,即引入超级角色(SuperHero)的功能,主要是因为引介是改变类的静态结构,而本例中需要的引介有点特殊,只是给类的某一个或部分实例添加SuperHero接口,而并不是要让HeroImpl实现SuperHero接口,无法通过直接在横切模块中定义引介实现。
当然,若要在不修改的HeroImpl代码的情况下让HeroImpl类实现SuperHero接口,则可以在切面模块AspectModule中添加如下的代码即可:
@DeclareParents(value="springroad.demo.chap5.wow.HeroImpl",defaultImpl=SuperHeroImpl.class)
private SuperHero superHero;
其中第一行@DeclareParents标签是aspectJ中的标签,可以用来指定一个现有类实现某个接口,或者为现有类指定一个父类。@DeclareParents的value属性值为匹配类的切入点表达式,defaultImpl属性值指定一个目标接口的实现类,这里SuperHeroImpl.class是SuperHero接口的一个空实现,没有任何内容;标签后面的属性是声明是AspectJ注解的一种语法形式使得整个系统的结构更加清晰及简洁。
假如你在上面的横切模块AspectModule中加入了上面的代码,再运行客户端示例程序,你会发现战场中的所有英雄都变成了超级角色,“Mountain King”以及“10级山丘之王”的重击技能的出招成功率都变成了100%,也即都实现了SuperHero接口,这跟我们的需求有一定的差异。
5.5.5使用基于Schema的方式配置Spring AOP
当然,如果不使用@AspectJ注解方式来封装AOP切面模块,可以直接使用基于Schema的配置文件方式,通过Spring提供的spring-aop-2.0.xsd,使用命名空间<aop:xxx>在配置文件中直接配置切面。
同样是基于POJO的方式来封装切面模块,书写横切关注点的业务逻辑。本例中我们通过AspectJModule2这个类来封装模拟游戏中的切面逻辑,直接把上面AspectJModule中的实现代码拷过来,然后删除掉与@AspectJ注解相关的各种信息,得到一个传统的POJO。AspectJModule2的代码如下所示:
package springroad.demo.chap5.wow;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
publicclass AspectModule2 {
privateintattackCount=0;
//用于针对Hero的attak方法进行拦截,记录角色相互攻击情况
publicvoid afterHeroAttack(JoinPoint thisJoinPoint,boolean ret)
{
Hero s=(Hero)thisJoinPoint.getTarget();
Hero t=(Hero)thisJoinPoint.getArgs()[0];
attackCount++;
System.out.println("第"+attackCount+"回合:玩家["+s.getName()+"]向["+t.getName()+"]发动攻击![命中:"+(ret?"成功":"失败")+"]");
if(ret)System.out.println("生命值--["+s.getName()+":"+s.getHealth()+"]"+"-["+t.getName()+":"+t.getHealth()+"]");
}
//用于针对WarField的begin方法进行拦截
public Object aroundWarFieldBegin(ProceedingJoinPoint theJoinPoint)throws Throwable
{
WarField war=(WarField)theJoinPoint.getThis();
java.util.Collection heros=war.getHeros();
if (heros == null || heros.size() < 1)
{
System.out.println("战场中没有战士!");
returnnull;
}
java.util.Iterator ih = heros.iterator();
while (ih.hasNext())
System.out.println(ih.next());
System.out.println("战斗开始......");
Object ret=theJoinPoint.proceed();//调用连接点
java.util.Iterator it = heros.iterator();
Hero winner = (Hero) it.next();
System.out.println("/n最终胜利者:" + winner.getName());
return ret;
}
//用于针对WarField的getArmor方法进行拦截
public Object aroundHeroGetDamage(ProceedingJoinPoint theJoinPoint)throws Throwable
{
Hero hero=(Hero)theJoinPoint.getThis();
java.util.Iterator it=hero.getAuraAndSkill().iterator();
int armorValue=(Integer)theJoinPoint.proceed();
String s=hero.getName()+ "--基本护甲:"+armorValue+";";
while(it.hasNext())
{
Object p=it.next();
if(p instanceof ArmorProp)
{
armorValue+=((ArmorProp)p).getArmor(hero);
s+=p+";";
}
}
System.out.println(s+"[实际防御:"+armorValue+"]");
return armorValue;
}
}
然后使用下面信息来直接在配置文件中定义切面封装,AOP配置信息位于<aop:config>标签中。如下所示:
<aop:aspect ref="aspectModuel">
<aop:after-returning pointcut="execution(boolean springroad.demo.chap5.wow.Hero.attack(..))" returning="ret" method="afterHeroAttack"/>
<aop:around pointcut="execution(void springroad.demo.chap5.wow.WarField.begin())" method="aroundWarFieldBegin"/>
<aop:around pointcut="execution(public int springroad.demo.chap5.wow.Hero.getArmor())" method="aroundHeroGetDamage"/>
</aop:aspect>
其中<aop:aspect>标签用来定义一个切面,ref属性指向一个普通的Spring Bean;<aop:after-returning>及<aop:around>标签用来定义各个增强,即对Hero.attack(Hero)、Hero.getArmor()及WarField.begin方法增强,标签的pointcut属性是基于AspectJ切入点描述表达定义的切入点信息,method属性是增强所对应的方法。
aopdemo-3.xml是完整的配置文件内容,如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:config>
<aop:aspect ref="aspectModuel">
<aop:after-returning pointcut="execution(boolean springroad.demo.chap5.wow.Hero.attack(..))" returning="ret" method="afterHeroAttack"/>
<aop:around pointcut="execution(void springroad.demo.chap5.wow.WarField.begin())" method="aroundWarFieldBegin"/>
<aop:around pointcut="execution(public int springroad.demo.chap5.wow.Hero.getArmor())" method="aroundHeroGetDamage"/>
</aop:aspect>
</aop:config>
<!--使用普通Bean方式定义一个切面模块-->
<bean id="aspectModuel" class="springroad.demo.chap5.wow.AspectModule2"></bean>
<!--定义英雄MK-->
<bean id="MK" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="2" />
<property name="health" value="500" />
<property name="name" value="Mountain King" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
</list>
</property>
</bean>
<!--定义英雄POM-->
<bean id="POM" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="Priestess of the Moon" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<!--定义英雄10级山丘之王-->
<bean id="superHero" class="springroad.demo.chap5.wow.HeroImpl">
<constructor-arg value="60" />
<property name="armor" value="1" />
<property name="health" value="500" />
<property name="name" value="10级山丘之王" />
<property name="auraAndSkill">
<list>
<bean class="springroad.demo.chap5.wow.Bash" />
<bean class="springroad.demo.chap5.wow.DevotionAura" />
<bean class="springroad.demo.chap5.wow.ScrollOfProtection" />
<bean class="springroad.demo.chap5.wow.ScrollOftheBeast" />
<bean class="springroad.demo.chap5.wow.TrueshotAura" />
</list>
</property>
</bean>
<bean id="WarField" class="springroad.demo.chap5.wow.WarFieldScuffle">
<property name="heros">
<set>
<ref bean="MK" />
<ref bean="POM" />
<ref bean="superHero" />
</set>
</property>
</bean>
</beans>
跟基于AspectJ注解实现的配置一样,配置文件减少了很多内容,由于可以在配置文件中灵活定义切入点,定义用于切面的普通Spring Bean,以及指定处理横切关注点实现方法。由此可见,相对于Spring以前的AOP实现来说,借助于AspectJ,Spring2的AOP功能更加灵活、使用更加简单了。
5.6 小结
本章主要讲解了AOP的相关概念,Spring AOP的各种使用方法、实现原理及一些简单技巧等,另外还介绍了Java领域功能强大的AOP实现AspectJ的应用,以便我们更好的掌握和学习使用Spring AOP。
5.7 思考题
对Spring2提供三种AOP使用方式进行对比分析,各自的优缺点是什么?
Java代理及拦截器机制的是如何来实现AOP功能的?
在一个新闻发布系统应用程序中,有哪功能需求属于横切交叉关注点需求?