`
wukele
  • 浏览: 32651 次
  • 性别: Icon_minigender_1
  • 来自: 泉州
社区版块
存档分类
最新评论

《深入Spring 2:轻量级J2EE开发框架原理与实践》

阅读更多
简介:
http://www.easyjf.com/spring/spring2-aop.htm
  本书首先是一本通过通俗案例讲解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等很多成员给予了很我们很大的帮助,在此深表感谢。



作者邮箱:

蔡世友 caishiyou@sina.com.cn

吴嘉俊 stef_wu@163.com

冯 煜 fengyu8299@126.com

张 钰 zhangyu20030607@hotmail.com







《深入Spring 2:轻量级J2EE开发框架原理与实践》第五章 

面向切面的编程(AOP)及在Spring中的应用

目 录

第五章 面向方面的编程(AOP)及在Spring中的应用... 1

5.1 AOP简介... 1

5.1.1 AOP概念... 1

5.1.2 AOP中的一些相关术语介绍... 3

5.1.3 AOP与OOP关系... 6

5.1.4 AOP联盟简介... 6

5.1.5 AOP相关框架及工具简介... 8

5.1.6 AOP在企业级应用程序中的作用... 8

5.2 AspectJ简介及快速入门... 9

5.2.1 AspectJ介绍... 9

5.2.2 AspectJ的下载及安装... 9

5.2.3 在Eclipse中开发AspectJ程序... 13

5.2.4 AspectJ版的HelloWorld. 15

5.2.5 AspectJ中相关语法... 17

5.2.6 一个简单的回合格斗小游戏示例... 23

5.3 一个简单的Spring AOP示例... 27

5.3.1 定义业务组件... 28

5.3.2 使用基于Schema的配置文件配置Spring AOP. 29

5.3.3 使用Java5注解配置及使用Spring AOP. 31

5.3.4 基于API方式来使用Spring AOP. 32

5.4 Spring中的AOP实现及应用... 34

5.4.1 简介... 34

5.4.2 Spring AOP中对AspectJ的支持... 35

5.4.3 Spring AOP配置方法... 36

5.4.4 切入点(Pointcut) 43

5.4.5 增强(Advice) 47

5.4.6 引介(Introduction) 51

5.4.7 增强器/切面封装(Advisor) 54

5.4.8 ProxyFactoryBean. 57

5.5 示例:模拟Warcraft游戏... 60

5.5.1 示例简介... 60

5.5.2 核心关注点及系统主模块... 61

5.5.3 横切关注点需求引入及实现... 70

5.5.4 使用AspectJ注解支持的AOP实现... 78

5.5.5 使用基于Schema的方式配置Spring AOP. 83

5.6 小结... 87

5.7 思考题... 87







第五章 面向方面的编程(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个业务方法,如下所示:

public class Component {

//业务方法1

public void business1()

{  

    //doSomeThing1

}

//业务方法2

public void business2()

{  

    //doSomeThing2

}

//业务方法3

public void business3()

{  

    //doSomeThing3

}

}

由于需求的变更,需要在每个方法执行前都要进行用户合法性验证,只有合法的用户才能执行业务方法里面的内容,因此,我们在三个方法中的第一行都需要加如一个用户合法性检验的代码。另外,我们还需要在运行方法中的实际业务逻辑前启动一个事务,在业务逻辑代码执行完成后结束一个事务,需要在方法中加入开始事务处理及结束事务处理的代码。最后,我们还需要在每一个方法结束后,把一些信息写入日志系统中。因此需要在每一个方法的最后添加记录日志的代码,这时业务方法变成如下的形式:

public void businessX()

{  

    validateUser();

    beginTransaction();

    //doSomeThing

    endTransaction();

    writeLogInfo();

}

假如我们的系统有成千上万个这样业务方法,都需要执行用户权限验证、事务处理、日志书写或审计等类似的工作,系统中就会充斥着非常多的重复性代码,造成代码书写及维护极其不便。例如,由于需求的变更,我们要取消一部分业务方法中的开启事务或结束事务处理的功能。此时,我们需要手工逐一删除掉这些方法中事务处理语句。

n         问题解决

  我们能不能不用在业务方法中添加哪些重性的代码,而通过某种机制,让权限验证、事务处理、日志记录等功能自动在这些方法指定的位置自动运行呢?为了能更好地解决上面提到的问题,于是引入了AOP(即面向切面)编程思想。

  例如,使用AspectJ中,我们可以定义一个切面,代码如下:

public aspect MyAspect {

void around():call(void Component.business*(..))

{

    validateUser();

    beginTransaction();

    proceed();

    endTransaction();

    writeLogInfo();

}

}

  这样,所有Component组件中返回值为void、名称以business开头的方法都会自动具有了用户验证、事务处理、日志记录等功能。

  假如要进一步使某一个包中的所有名称以business开头、返回值为void的方法都具有上面的功能。则只需要把上面MyAspect中的内容修改一下即可,如下所示:

void around(): 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.2 AspectJ的下载及安装
  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;

public class Hello {

public void sayHello()

{

    System.out.println("Hello AspectJ!");

}

public static void main(String[] args) {

    Hello h=new Hello();

    h.sayHello();

}

}

然后使用使用Eclipse新建一个名为HelloAspect的Aspect切面,如“图5-10”所示。



图5-10 在Eclipse中新建AspectJ切面

在HelloAspect中定义了一个名为somePointcut()的切入点,定义了两个增强,其中一个是在连接点前执行,一个是在连接点后执行。HelloAspect的全部代码如下所示:

package demo;

public aspect 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切面源文件内容:

public aspect AspectDemo {

//切面里面的属性

private int times=0;

//定义一个切入点

pointcut somePointcut():call(* Component.*(..));

//给切入点somePointcut定义一个通知

after():somePointcut(){

    this.times++;

    System.out.println("执行了内中的:"+thisJoinPoint.getSignature().getName());

    this.print();

}

//切面中定义方法

private void 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的公共方法:

 public void 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接口:

  declare parents:Soldier implements java.lang.Comparable;

    public int Soldier.compareTo(Object o) {...}

  (3)、指定继承

  AspectJ可以在切面中用declare parents关键字,给一个现有的类指定一个父类,实现多重继承。格式如下:

  declare parents: TypePattern extends Type;

  如下面的例子,给Soldier指定了一个父类UserInfo,这样Soldier就有了UserInfo类的特性及功能:

  declare parents: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的源码如下:

public class Soldier {

private String name;

private int health=100;

private int damage=10;

private int x=10;

private int y=10;

//攻击其它角色

public boolean attack(Soldier target){

    boolean ret=false;

    if(!target.dodge())//目标是否躲闪成功

    {

       target.setHealth(target.getHealth()-this.damage);

       ret=true;

    }

    move();    //移动一下

    treat();//冶我疗伤

    return ret;

}

public void move()

{

    this.x+=getRandom(5);

    this.y+=getRandom(5);

}

//躲避x及y随机变动,成功率为50%

public boolean dodge()

{

    return getRandom(10)%2==0;

}

//治疗,具有一定成功的机会,可以提高生命值0-20点

public void treat()

{

    if(canTreat())

       this.health+=getRandom(20);

}

public boolean canTreat()

{

    return getRandom(10)/2==0;

}



private int getRandom(int seed)

{

    return RandomUtil.getRandomValue(seed);

}



//getter及setter方法

public int getHealth() {

    return health;

}

public void setHealth(int health) {

    this.health = health;

}

public String getName() {

    return name;

}

public void setName(String name) {

    this.name = name;

}

public int getX() {

    return x;

}

public void setX(int x) {

    this.x = x;

}

public int getY() {

    return y;

}

public void setY(int y) {

    this.y = y;

}

public int getDamage() {

    return damage;

}

public void setDamage(int damage) {

    this.damage = damage;

}

}

Soldier引用了一个随机数生成工具类RandomUtil,用于模拟一定的发生概率,代码如下:

public class RandomUtil {

private static java.util.Random random=new java.util.Random();

public static int getRandomValue(int seed)

{  

    return random.nextInt(seed);

}

}

然后就是使用Soldier的客户端程序MainTest,这里是一个简单的控制台程序,代码如下:

public class MainTest { 

    public static void 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 {

    private static 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()+"得到治疗!");

     }

     boolean around(Soldier s): supperRole(s)

     {

        if("super".equals(s.getName())) return true;

        else return proceed(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;



public interface Component {

     void business1();//商业逻辑方法1

     void business2();//商业逻辑方法2

     void business3();//商业逻辑方法3

}

写一个Component的实现ComponentImpl类,ComponentImpl.java代码如下:

package springroad.demo.chap5.exampleB;

public class ComponentImpl implements Component {

    public void business1() {

       System.out.println("执行业务处理方法1");

    }

    public void business2() {

       System.out.println("执行业务处理方法2");

    }

    public void 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;

public class ComponentClient {

    public static void 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;

public class AspectBean {

    public void validateUser()

    {

       System.out.println("执行用户验证!");

    }

    public void writeLogInfo()

    {

       System.out.println("书写日志信息");

    }

    public void beginTransaction()

    {  

       System.out.println("开始事务");

    }

    public void 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

结束事务

书写日志信息



由此可见,在客户调
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics