Spring面向切面编程

来源:互联网 发布:怎么写出淘宝试用报告 编辑:程序博客网 时间:2024/05/22 02:01

1章主要介绍了Spring管理实体对象的应用,通过ApplicationContext容器来了解Spring管理实体对象的原理以及设值注入、构造方法及自动注入等不同的注入方式。本章先介绍为什么需要AOP以及使用AOP的好处,然后采用手动代理的方式介绍什么是代理及代理的必要性,最后结合商场手机进货和收获的案例分别介绍前置通知、后置通知、环绕通知和异常通知。在介绍前置通知的时候,分别采用Spring1.x和Spring2.x的方式进行配置,为后续课程的学习打下铺垫。

 


核心技能部分

第1章 

1.1 Spring AOP简介

1.1.1 为什么需要AOP

通过依赖注入,在编写程序的时候,我们不必关心依赖的组件如何实现,然而在实际开发过程中我们还需要将程序中涉及的公共问题集中解决,如图2.1.1所示。

 

2.1.1 Spring的两个重要模块

看下面的一个应用:

public void doSameBusiness (long lParam,String sParam){// 记录日志log.info("调用 doSameBusiness方法,参数是:"+lParam);// 输入合法性验证if (lParam<=0){throws new IllegalArgumentException("xx应该大于0");}try{ //真正的业务逻辑代码    //事务控制}catch(){tx.rollback();}// 事务控制tx.commit();}

这是一个典型的业务处理方法。日志、参数合法性验证、异常处理、事务控制等都是一个健壮的业务系统所必需的;否则系统出现问题或者有错误的业务操作时没有日志可查;传入的出库参数为负数,出库反而导致库存增加;转账时,打款方的钱已经被扣了,方法却异常退出,而收款方还没有收到,已经进行的交易没有回滚。这样的系统显然是没有人敢用的。

为了保证系统健壮可用,就要在每个业务方法里都反复编写这些代码,如果需要修改,则每个业务方法都需要修改,这样的代码质量很难保障。

我们怎样才能把心用在真正的业务逻辑上呢?这就是AOP要解决的问题。

1.1.1 什么是AOP

AOP 是Aspect-Oriented Programming的简称,意思是面向切面编程AOP是对OOP的补充和完善。比如刚才的问题,程序中所有的业务方法都需要日志记录、参数验证、事务处理,这些公共的处理如果放在每个业务方法里,系统会变的臃肿,而且很难去维护。散布在系统各处的需要在实现业务系统时关注的事情就被成为“切面”,也称为关注点,AOP的思想就是把这些公共部分从业务方法中提取出来,集中处理。

编写代码的时候,“切面”代码并不放在业务方法中,但是程序运行的时候,Spring会拦截到方法的执行,并运行这些“切面”代码。

OOP 引人了封装、继承及多态性等概念来建立对象层次结构,用于模拟公共行为的集合。在OOP思想中,代码的重复可以被提取出来放在父类中被复用。但是方法中的重复代码,OOP却无能为力。例如,在日志信息记录中,日志代码常水平地散布在所有对象层次中,与对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理以及透明的持续性也同样如此。散布在各处且无关的代码称为横切 (Cross-cutting) 代码,这种代码在OOP 的设计中导致了大量重复且不利于各个模块重用。

AOP技术则刚好相反,它代表横向关系,通过利用“横切”技术解剖封装的对象并获得其内部,将影响多个类的公共行为封装至可重用模块并命名为“Aspect”(切面)。简而言之,即 AOP技术将与业务无关却由业务模块共同调用的逻辑进行封装,减少系统的重复代码,降低模块间的耦合度,提高系统的可操作性及可维护性。若将“对象”视为空心的圆柱体,将属性与行为都封装于柱体中,则 AOP技术的作用类似于刀,将空心圆柱体剖开并获得内部的消息,剖开的面即“切面”。

通过使用“横切”技术,AOP将软件系统分为两个部分:核心关注点与横切关注点。业务处理的主要流程即核心关注点,与之关系较小的部分就是横切关注点。横切关注点常发生在核心关注点的多处且各处基本相似,如权限认证、日志、事务处理等。AOP 的作用在于分离系统中的各种关注点,将核心关注点与横切关注点进行分离,其核心思想是将应用程序中的商业逻辑与向其提供支持的通用服务进行分离。

AOP 中包含许多新的概念与术语,说明如下:

(1)切面 (Aspect):切面是系统中抽象出来的的某一个功能模块。

(2)连接点 (Joinpoint):程序执行过程中某个特定的点,如调用某方法或者处理异常时。在SpringAOP中,连接点总是代表某个方法的执行。

(3)通知 (Advice):通知是切面的具体实现,是放置“切面代码”的类。

(4)切入点 (Pointcut):多个连接点组成一个切入点,可以使用切入点表达式来表示。

(5)目标对象 (Target Object):包含一个连接点的对象,即被拦截的对象。

(6)AOP代理 (AOP Proxy):AOP框架产生的对象,是通知和目标对象的结合体。

(7)织入 (Weaving):将切入点和通知结合的过程称为织入。

1.1 AOP代理

在生活中,“代理”一词出现的频率较高,并且现实事物能够形象、直观地反映代理模式的抽象过程及本质。下面我们以购买手机和出售房屋为例,初步分析“代理”的概念。

众所周知,手机或电脑都可以直接从厂家购买,但是这种方式却很少使用。因为一般情况下,顾客都希望在购买时获得额外的充值卡、鼠标等,厂家却很少提供,但是顾客可以在代理商处获得此类额外物件。房屋买卖中同样存在类似情况:房屋待售时,屋主可以通过互联网发布出售信息寻找买家,同咨询者看房、洽谈、交易、过户以实现成交,需要占用卖家大量时间;此外还可以交给中介管理,由其处理琐碎的交易过程。实际上,中介即卖家的代理。

接下来我们看一个需求,某手机商店需要购买手机和销售手机,现要求购买和销售手机时记录日志。

根据需求我们知道,本系统有两个业务操作:购买手机和销售手机。两个业务方法都用着相同的日志操作,因此日志操作为该系统的“切面”代码,我们可以将这些代码从业务方法中分离出来。我们可以定义一个手机业务接口、一个手机业务接口实现和一个日志操作类。手机接口和手机接口的实现如示例2.1所示。

示例2.1

//手机业务接口public interface PhoneBiz {public void buyPhone(int num);//购买手机public void salePhone(int num);//卖手机}//手机业务接口实现public class PhoneBizImpl implements PhoneBiz {public void buyPhone(int num) {System.out.println("手机进货,进货数量 为" + num + "部");}public void salePhone(int num) {System.out.println("销售手机,销售数量为" + num + "部");}}

日志操作类如示例2.2所示。

示例2.2

public class LogUtil {public void log(String type,int num) {System.out.println("日志:"+currentTime()+type+"手机"+num+"部...");}public String currentTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");return sdf.format(new Date());}}

接下来我们编写一个测试类,对系统的业务方法进行测试,测试代码如示例2.3所示。

示例2.3

PhoneBiz phoneBiz=new PhoneBizImpl();//创建目标对象phoneBiz.buyPhone(10);phoneBiz.salePhone(3);

上面的测试结果可以实现手机的进货和销售,但是日志记录却无法实现,原因是目标对象的方法没有日志相关的代码(日志代码从业务方法中提取出去了),那么如何能在程序运行过程中加入“切面”代码(日志)呢?这个时候,代理的作用就体现出来了,我们可以设计一个针对目标对象的代理。手机业务代理类如示例2.4所示。

示例2.4

public class PhoneBizImplProxy implements PhoneBiz {private PhoneBiz phoneBiz=new PhoneBizImpl();// 目标对象private LogUtil logUtil=new LogUtil();// 日志操作对象public void buyPhone(int num) {phoneBiz.buyPhone(num);// 调用目标对象的手机进货方法logUtil.log("购买", num);//日志操作}public void salePhone(int num) {phoneBiz.salePhone(num);// 调用目标对象的手机销售方法logUtil.log("销售", num);//日志操作}//setter & getter}

通过上面的代码我们知道,该代理类中包含两个重要属性:目标对象和“切面对象”。

要想把公共代码提取出来,又在业务方法运行的时候加上公共代码,我们可以调用代理对象的方法。测试代码如示例2.5所示。

示例2.5

PhoneBiz phoneBiz=new PhoneBizImplProxy();//创建代理对象phoneBiz.buyPhone(10);//调用代理对象的进货方法phoneBiz.salePhone(3);//调用代理对象的销售方法

综上,代理通常可以作为目标对象的替代品,且提供更加强大的功能。代理包含目标Bean的所有方法,代理就是目标对象的加强,通过以目标对象为基础来增加属性与方法,提供更加强大的功能。AOP代理是由 AOP框架创建的对象。

如果在Spring项目中使用AOP,则需要在Spring项目中添加Spring Libraries类库。

1.1 Spring AOP的实现

本章继续以2.2节中手机商店的进货和销售业务为例,以日志管理为需求,讲解Spring AOP技术的具体使用方法。

1.1.1 引入AOP命名空间

首先我们需要在Spring应用容器配置文件中引入Spring AOP命名空间,否则无法在Spring容器配置文件中使用AOP相关xml标签。如示例2.6所示,将其中加粗部分代码,添加在你的applicationContext.xml中即可:

示例2.6 

<beans xmlns=http://www.springframework.org/schema/beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.0.xsd"></beans>

1.1.1 目标对象(Target Object)

包含业务关注点(本例是需要日志管理的点)的对象,称为目标对象。在本例中,我们的目标对象是手机业务对象,即s3spring.ch2.biz.impl.PhoneBizImpl类的对象,它实现s3spring.ch2.biz.PhoneBiz接口,我们需要在applicationContext.xml中将其配置成Spring bean,如示例2.7所示:

示例2.7

<bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>

1.1.2 连接点(JoinPoint)

程序执行过程中的某个业务关注点(本例是需要日志管理的点)被称作连接点(JoinPoint)。在Spring AOP中,连接点总是代表某个方法的执行。在本例中PhoneBizImpl类的buyPhone(int)方法和salePhone(int)的执行都是连接点。

1.1.3 切入点(PointCut)

多个连接点组成一个切入点。我们用切入点表达式来描述多个连接点的集合。为了选择方法,我们可以使用下面这些条件来构造切入点表达式:

切入点指示符:如execution,最常用的切入点指示符,表示方法的执行。

布尔运算符:AND(&&)、OR(||)和NOT(!),可以将多个表达式组合成一个新的表达式来缩小选择连接点的范围。

通配符:星号(*),用于匹配任何方法、类名或者参数类型。双点号(..),用于表示0个或者多个参数类型。

方法可见性修饰符:将选择连接点的范围缩小到某种可见性的方法。如public的。

方法返回类型:void、int、String等,也可以使用*,表示所有类型。

类名:指定完整的类名(包括完整的包名称,如java.lang.String),将选择连接点的范围缩小到某个目标类。

方法名称:可以是全名。也可以是*(任何名称)。也可以是部分名称结合通配符,如get*,即所有名称以get开头的方法。

方法声明抛出的异常: throws java.lang.IOException。

使用execution切入点指示符描述连接点范围时,除了方法返回类型、方法名称和方法参数是必须描述的以外, 其它所有的部分都是可选的(方法可见性修饰符、类名、方法声明抛出的异常)。

示例2.8表达式描述了PhoneBizImpl类名字以Phone结尾,参数只有一个且类型为int,返回类型为void,可见性为public的方法的执行,当然也包括了buyPhone(int)方法和salePhone(int)方法。

示例2.8  

execution( public void s3spring.ch2.biz.impl.PhoneBizImpl.*Phone(int) )

如果想描述所有业务类的所有业务方法,可以象示例2.9这样描述:

示例2.9

execution(* s3spring.ch2.biz.impl.*.* (..) )

示例2.9中第一个*表示任意返回类型。第二个*表示任意类名。第三个*表示任意方法名称。..表示0或多个任意类型的方法参数。

示例2.9也可以用within切入点指示符进行简化。within表示某一范围内的连接点,如某包内,某类内。示例2.10表示s3spring.ch2.biz.impl包中所有的连接点:

示例2.10

within( s3spring.ch2.biz.impl.* )

示例2.11表示s3spring.ch2.biz.impl.PhoneBizImpl类中所有的连接点:

示例2.11

within( s3spring.ch2.biz.impl.PhoneBizImpl )

示例2.12表示s3spring.ch2.biz.impl 包及其所有子孙包中所有的连接点:

示例2.12

within( s3spring.ch2.biz.impl.PhoneBizImpl..* )

1.1.4 切面(Aspect)

切面是系统中抽象出来的的某一个系统服务功能模块。在本例中是日志管理模块,它可以保含多个实现日志管理操作的通知。我们用一个POJO (Plain Old Java Ojbect,简单普通的Java对象)类来表示抽象的切面,用方法表示通知,并把切面类配置成一个Spring bean。示例2.13 是日志管理切面类,目前它还没有包含任何通知:

示例2.13

package s3spring.ch2.log;

public class LogAspect {

}

此时,LogAspect仅仅是一个类,还不能代表一个切面,我们接着需要把它配置成一个Spring bean,请看示例:

示例2.14

<bean id="logAspectBean" class="s3spring.ch2.log.LogAspect"></bean>

然后,我们可以利用示例2.6 引入的Spring AOP命名空间将“logAspectBean” bean 配置成一个切面。请看示例2.15:

示例2.15

<aop:config>

     <!-- 定义一个可以被多个切面共享的切入点 -->

<aop:pointcut id="p1" expression="execution( void *Phone(int))"/>

     <!-- 定义一个切面 -->

<aop:aspect id="logAspect" ref="logAspectBean"></aop:aspect>

</aop:config>

如示例2.15所示,切面、切入点等AOP相关内容必须定义在<aop:config>元素内部,且可以配置多个切面、多个切入点。

我们首先使用<aop:pointcut>标签定义了一个id为p1的切入点,这个切入点的表达式描述了日志管理所关注的连接点——所有返回类型为void,名称以Phone结尾,参数只有一个且类型为int的方法。

将切入点直接定义在<aop:config>元素内部,而不是<aop:aspect>标签内部的好处是它可以被多个切面共享。例如该切入点即被日志管理模块关注,又被事务管理模块关注。当然,你也可以将关注点定义在某个<aop:aspect>标签内部,这时该切入点就成为这个切面内部独享的私有切入点了。

最后,我们用<aop:aspect>标签,通过ref属性指定id为 “logAspectBean”的bean为一个切面,并为该切面设置id为“logAspect”。

1.1.5 通知(Advice)

通知(Advice)是在切面的某个特定的连接点上执行的具体操作。按照执行的时机可以分为下面几种:

前置通知(Before):在某连接点之前执行的通知,它可以阻止连接点的执行。例如检查权限,决定是否执行连接点。

后置通知(AfterReturning):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。它可以访问方法返回值。

异常通知(AfterThrowing):在方法抛出异常退出时执行的通知。它可以访问抛出的异常。

最终通知(After ):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。 这是传统spring AOP中没有的新的通知类型。它不能访问返回值或抛出的异常。可以用来执行释放资源,日志记录等操作。

环绕通知(Around):也叫方法拦截器。包围一个连接点的通知。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。 可用于实现性能测试,事物管理等。

1.1.6 前置通知(Before)

前置通知是在目标方法被调用之前织入的通知。即当目标方法被调用且执行之前,先执行的系统服务业务逻辑,如执行日志记录,参数检查,权限限制等。

示例2.16将展示利用Spring的前置通知实现在手机进货和销售手机执行之前记录操作日志。示例2.1中的业务接口PhoneBiz和实现类PhoneBizImpl的代码不需要做任何修改,代理对象由Spring自动产生。我们只需要在示例2.13中创建的LogAspect切面类内部定义一个方法实现目标业务方法执行之前要进行的日志记录操作,如示例2.16所示。

示例2.16 

package s3spring.ch2.log;public class LogAspect {//前置通知public void before() throws Throwable {System.out.println("业务方法开始执行……");}[先添加一个简单的方法,然后再引入JoinPoint]/** * 前置通知 在目标方法执行之前执行日志记录 */public void before(JoinPoint jp) throws Throwable {Object[] args = jp.getArgs();// 目标方法所有参数String methodName = jp.getSignature().getName();//获得目标方法名称if ("buyPhone".equals(methodName)) {System.out.println(currentTime() + "即将执行进货操作,数量为" + args[0]);}if ("salePhone".equals(methodName)) {System.out.println(currentTime() + "即将执行销售操作,数量为" + args[0]);}}/** * 输出当前时间的工具方法 */public String currentTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");return sdf.format(new Date());}}

如示例2.16所示,before方法拥有一个参数叫做jp, 类型为JoinPoint,即连接点。这个参数是可选的。JoinPoint对象提供了如下方法以获得连接点(在spring中一般是方法的执行)的一些有用信息:

l Object[] getArgs():以对象数组的形式返回所有方法参数

Signature getSignature():返回方法签名,通过SignaturegetName()方法可以得到方法名称;getModifiers()方法可以得到方法修饰符。

l String getKind():返回当前连接点的类型,如:“method-execution”,方法执行。

l Object getTarget():返回连接点所在的目标对象。

l Object getThis():返回AOP自动创建的代理对象。

此时before方法虽然实现了一个日志管理前置通知的业务逻辑,但是它还是一个普通的方法,我们需要把这个方法定义成一个前置通知,并和示例2.15中定义的切入点p1关联起来,请看示例2.17中<aop:aspect>标签内的粗体部分代码:

示例2.17

<aop:config>     <!-- 定义一个可以被多个切面共享的切入点 --><aop:pointcut id="p1" expression="execution( void *Phone(int))"/>     <!-- 定义一个切面 --><aop:aspect id="logAspect" ref="logAspectBean">         <!-- 定义一个前置通知 -->         <aop:before method="before" pointcut-ref="p1" /></aop:aspect></aop:config>

<aop:before>标签用于定义一个前置通知。method属性指定实现通知的方法。pointcut-ref属性指定该通知关联的切入点。一个切面(<aop:aspect>)内可以包含多个不同类型的通知。

到这里,我们的前置通知的示例就完成了。示例2.18是完整的配置:

示例2.18

<?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:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.0.xsd"><!-- 目标业务对象  --><bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean><!-- 日志管理切面类  --><bean id="logAspectBean" class="s3spring.ch2.log.LogAspect"></bean><!-- Aop配置  --><aop:config><aop:pointcut id="p1" expression="execution( void *Phone(int) )"/><!-- 配置日志管理切面  --><aop:aspect id="logAspect" ref="logAspectBean">         <!-- 配置日志记录前置通知  --><aop:before method="before" pointcut-ref="p1" /></aop:aspect></aop:config></beans>

接下来我们来测试一下使用前置通知实现的日志管理操作的效果,示例2.19是测试代码:

示例2.19

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");//创建代理对象PhoneBiz pBiz = (PhoneBiz)ac.getBean("phoneBiz");//购买100部手机pBiz.buyPhone(100);//销售88部手机pBiz.salePhone(88);

示例2.19执行输出结果:

2012年07月18日 18:02:45 即将执行进货操作,数量为100

手机进货,进货数量 为100部

2012年07月18日 18:02:45 即将执行销售操作,数量为88

销售手机,销售数量为88部

观察输出结果我们可以看出,在业务方法执行之前,前置通知成功织入连接点,先进行了日志输出。实际上,我们通过Spring应用容器获得的并不是真正的PhoneBizImpl类型的对象,而是Spring自动根据PhoneBizImpl 类实现的PhoneBiz接口创建的代理对象。示例2.20可以证明着一点:

示例2.20

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");//创建代理对象PhoneBiz pBiz = (PhoneBiz)ac.getBean("phoneBiz");//输出bean类型名称System.out.println(pBiz.getClass().getName());//购买100部手机pBiz.buyPhone(100);//销售88部手机pBiz.salePhone(88);

示例2.20粗体部分代码调用了pBiz的getClass()方法返回该对象的类型描述对象(一个Class类型的对象),并进一步调用Class对象的getName方法获取类名称。输出结果是:

$Proxy4

而不是bean对象的类型:

s3spring.ch2.biz.impl.PhoneBizImpl

这足以说明我们获得的并不是一个 PhoneBizImpl 类的对象,而是一个代理对象,而且该代理对象实现了PhoneBiz接口。所有此时你不能使用PhoneBizImpl类型的变量引用代理对象,请看示例2.21:

示例2.21

ApplicationContext ac =

new ClassPathXmlApplicationContext("applicationContext.xml");

//创建代理对象

PhoneBizImpl pBiz = (PhoneBizImpl)ac.getBean("phoneBiz");

//输出bean类型名称

System.out.println(pBiz.getClass().getName());

示例2.21将引用代理对象的变量类型从PhoneBiz改成PhoneBizImpl,执行时将抛出以下异常信息:

java.lang.ClassCastException: $Proxy4 cannot be cast to s3spring.ch2.biz.impl.PhoneBizImpl

示例2.21抛出的异常信息描述的很清楚:$Proxy4不能被转换成PhoneBizImpl。那么如果我们的目标对象没有实现接口怎么办呢?让我们修改PhoneBizImpl 类的定义,使其不实现任何接口,请看示例2.22:

示例2.22

public class PhoneBizImpl

此时再次运行示例2.20,程序能够正常运行,输出代理对象类型信息如下:

s3spring.ch2.biz.impl.PhoneBizImpl$$EnhancerByCGLIB$$ffeaaf7b

实际上,Spring AOP 创建代理分为两种情况:

如果被代理的目标对象实现了至少一个接口,Spring会使用针对接口产生代理的Java SE动态代理(Java直接提供的动态代理API)。目标对象类型实现的所有接口都将被代理,包括业务接口之外的接口。 注意,Java SE 动态代理只能针对接口产生代理,所以目标对象必须至少实现了一个接口。建议优先使用Java SE 动态代理。

对于需要直接代理类而不是代理接口的时候,Spring也可以使用CGLIB(Code Generation Library)代理。如果一个业务对象并没有实现任何接口,Spring就会使用CGLIB在运行时动态生成目标对象的子类对象来作为代理对象。就算目标对象实现了接口,你也可以强制使用CGLIB代理,例如:希望代理目标对象的所有方法,而不只是实现自接口的方法;或当目标对象没有实现有用的业务接口,而只是实现了一些辅助工具接口(如果此时针对接口产生代理,则代理对象仅拥有这些辅助工具接口定义的方法,没有业务方法)。我们只需要将<aop:config>标签的proxy-target-class属性置为"true"即可强制使用CGLIB针对类产生代理,请看示例:

示例2.23

<aop:config proxy-target-class="true">

         ……

</aop:config>

1.1.1 后置通知(AfterReturning)

后置通知是在目标方法调用之后织入通知,即在方法正常退出返回值之后且返回调用地点之前进行织入。

示例2.24是一个后置通知实现,也位于示例2.13的LogAspect切面类中,作用是在业务方法调用结束之后进行日志记录。

示例2.24


public void afterReturning(JoinPoint jp) throws Throwable {String methodName = jp.getSignature().getName();if ("buyPhone".equals(methodName)) {System.out.println(currentTime() + "进货操作执行完毕...");}if ("salePhone".equals(methodName)) {System.out.println(currentTime() + "销售操作执行完毕...");}

在配置文件中<aop:aspect>标签内增加如示例2.25粗体部分配置内容,将后置通知织入切入点p1。

示例2.25

<!-- Aop配置  --><aop:config ><!-- 配置切入点  --><aop:pointcut id="p1" expression="execution( void *Phone(int) )" /><!-- 配置切面  --><aop:aspect id="logAspect" ref="logAspectBean"><!-- 配置前置通知  --><aop:before method="before" pointcut-ref="p1" /><!-- 配置后置通知  --><aop:after-returning method="afterReturning" pointcut-ref="p1"/></aop:aspect></aop:config>

不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

2012年07月20日 15:04:56即将执行进货操作,数量为100

手机进货,进货数量 为100部

2012年07月20日 15:04:56进货操作执行完毕...

 

2012年07月20日 15:04:56即将执行销售操作,数量为88

销售手机,销售数量为88部

2012年07月20日 15:04:56销售操作执行完毕...

1.1.1 异常通知(AfterThrowing)

如果连接点抛出异常,异常通知(throws advice)将在连接点异常退出后被调用。示例2.26是一个自定义业务异常,表示缺货。

示例2.26

package s3spring.ch2.exception;

/** 缺货异常*/

public class OutOfStockException extends Exception{

 

public OutOfStockException(String msg) {

super(msg);

}

}

修改示例2.1,为 PhoneBizImpl 类添加一个num属性代表库存。在salePhone()方法中判断如果销售量高于库存量,则抛出OutOfStockException,请看示例2.27粗体部分代码:

示例2.27

public class PhoneBizImpl  {int num;// 库存public void buyPhone(int num) {System.out.println("手机进货,进货数量 为" + num + "部");     this.num += num;}public void salePhone(int num) throws OutOfStockException {if(this.num<num) throw new OutOfStockException("存货不足,客户需要"+num+"部手机,库存只有"+this.num+"部");         System.out.println("销售手机,销售数量为" + num + "部");}     ……}

LogAspect切面类添加一个实现异常通知的方法,作用是在业务方法异常退出之后进行日志记录。OutOfStockException类型的参数e用于接收目标方法抛出的OutOfStockException类型异常,以便在通知内处理该异常。如果目标方法抛出的是其它异常,比如空指针异常,则示例2.28中异常通知方法不会被执行:

示例2.28

public void afterThrowing(JoinPoint jp,OutOfStockException e) {String methodName = jp.getSignature().getName();System.out.println(currentTime()+methodName+"方法执行,发生缺货异常"+e.getMessage());}

在配置文件中<aop:aspect>标签内增加如示例2.29粗体部分配置内容,将afterThrowing方法作为异常通知织入切入点p1,将throwing属性的值指定为afterThrowing方法OutOfStockException类型参数的名称“e”:

示例2.29

<!-- Aop配置  --><aop:config ><!-- 配置切入点  --><aop:pointcut id="p1" expression="execution( void *Phone(int) )" /><!-- 配置切面  --><aop:aspect id="logAspect" ref="logAspectBean"><!-- 配置前置通知  --><aop:before method="before" pointcut-ref="p1" /><!-- 配置后置通知  --><aop:after-returning method="afterReturning" pointcut-ref="p1"/><!-- 配置异常通知  --><aop:after-throwing method="afterThrowing" pointcut-ref="p1" throwing="e" /></aop:aspect></aop:config>

不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

2012年07月20日 21:19:10即将执行进货操作,数量为100

手机进货,进货数量 为100部

2012年07月20日 21:19:10进货操作执行完毕...

 

2012年07月20日 21:19:10即将执行销售操作,数量为120

2012年07月20日 21:19:10salePhone方法执行,发生缺货异常:货存不足,客户需要120部手机,库存只有100部

观察执行结果你会发现,当目标方法发生缺货异常时异常通知正确执行,但是后置通知没有执行。因为后置通知只在目标方法正常执行结束时执行。如果我们希望能够在目标方法抛出异常之后,像try、catch、finally结构一样,除了是用异常通知处理异常外,还可以通过某种通知去完成类似finally的任务,即无论目标方法是否抛出异常,该通知都一定会执行。这就是我们继续要学习的最终通知。

1.1.1 最终通知(After)

最终通知是无论目标方法异常退出,还是正常退出都一定会执行的通知。在LogAspect切面类中添加如示例2.30所示方法。

示例2.30 最终通知

public void after(JoinPoint jp) throws Throwable {String methodName = jp.getSignature().getName();if ("buyPhone".equals(methodName)) {System.out.println(currentTime() + "进货操作执行完毕,发生异常也要执行的最终通知...");}if ("salePhone".equals(methodName)) {System.out.println(currentTime() + "销售操作执行完毕,发生异常也要执行的最终通知...");}}

在配置文件中<aop:aspect>标签内增加如示例2.31粗体部分配置内容,将after方法作为最终通知织入切入点p1。

示例2.31最终通知配置

<!-- Aop配置  --><aop:config ><!-- 配置切入点  --><aop:pointcut id="p1" expression="execution( void *Phone(int) )" /><!-- 配置切面  --><aop:aspect id="logAspect" ref="logAspectBean"><!-- 配置前置通知  --><aop:before method="before" pointcut-ref="p1" /><!-- 配置后置通知  --><aop:after-returning method="afterReturning" pointcut-ref="p1"/><!-- 配置异常通知  --><aop:after-throwing method="afterThrowing" pointcut-ref="p1" throwing="e" /><!--配置最终通知  --> <aop:after method="after" pointcut-ref="p1"/></aop:aspect></aop:config>

不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

2012年07月25日 15:55:32即将执行进货操作,数量为100

手机进货,进货数量 为100部

2012年07月25日 15:55:32进货操作执行完毕...

2012年07月25日 15:55:32进货操作执行完毕,发生异常也要执行的最终通知...

2012年07月25日 15:55:32即将执行销售操作,数量为120

销售手机,销售数量为120部

2012年07月25日 15:55:32销售操作执行完毕...

2012年07月25日 15:55:32销售操作执行完毕,发生异常也要执行的最终通知...

1.1.1 环绕通知(Around)

环绕 (Around)通知是最强大的通知类型,它能够代替之前所有通知类型,在连接点的前后执行,获取方法入参、返回值,捕捉并处理异常。下面我们就以性能测试为需求,针对业务层编写环绕通知,计算业务方法执行耗费的时间长短。在LogAspect切面类中添加如示例2.32所示性能测试环绕通知方法实现。

示例2.32 


public Object aroundTest(ProceedingJoinPoint pjp) throws Throwable {      String method = pjp.getSignature().getName();   long begin = System.currentTimeMillis();   System.out.println(currentTime()+":"+method+"方法开始执行,计时开始!");   try {    return pjp.proceed();   } finally{    long end = System.currentTimeMillis();      System.out.println(currentTime()+":"+method+"方法执行完毕,耗时"+(end-begin)+ "毫秒");   }}

环绕通知的实现方法必须包含一个连接点入参,但是其类型与其它类型的通知不同,为ProceedingJoinPoint。ProceedingJoinPoint对象代表了通知织入的当前连接点,调用其proceed()方法就会执行目标方法,proceed()方法的返回值就是目标方法的返回值,proceed()方法抛出的异常就是目标方法抛出的异常;如果你不想执行目标方法,只要不执行proceed()方法即可。

在配置文件中<aop:aspect>标签内增加如示例2.33粗体部分配置内容,将aroundTest方法作为环绕通知织入切入点p1。

示例2.33 

<!-- Aop配置  --><aop:config ><!-- 配置切入点  --><aop:pointcut id="p1" expression="execution( void *Phone(int) )" /><!-- 配置切面  --><aop:aspect id="logAspect" ref="logAspectBean"><!-- 配置前置通知  --><aop:before method="before" pointcut-ref="p1" /><!-- 配置后置通知  --><aop:after-returning method="afterReturning" pointcut-ref="p1"/><!-- 配置异常通知  --><aop:after-throwing method="afterThrowing" pointcut-ref="p1" throwing="e" /><!--配置最终通知  --> <aop:after method="after" pointcut-ref="p1"/><!--配置环绕通知  --><aop:around method="aroundTest" pointcut-ref="p1"/></aop:aspect></aop:config>

不修改示例2.19测试代码,测试结果如下所示,粗体部分为后置通知输出的日志信息:

……省略部分日志

2012年07月25日 10:58:41:buyPhone方法开始执行,计时开始!

手机进货,进货数量 为100部

……省略部分日志

2012年07月25日 10:58:41:buyPhone方法执行完毕执行完毕,耗时31毫秒

 

……省略部分日志

2012年07月25日 10:58:41:salePhone方法开始执行,计时开始!

销售手机,销售数量为120部

……省略部分日志

2012年07月25日 10:58:41:salePhone方法执行完毕执行完毕,耗时0毫秒

1.1 Spring AOP 注解方式的实现

我们也可以使用注解来进行AOP编程的配置。两种方式原理和概念不变,仅仅是配置形式不同罢了。下面我们就在2.3小节示例项目的基础之上将xml配置方式改为注解配置方式。

1.1.1 用注解方式重构日志管理切面

首先创建一个新包s3spring.ch2.log.annotation,然后将默认包下的applicationContext.xml和s3spring.ch2.log.LogAspect类复制到该包下。

修改s3spring.ch2.log.annotation.applicationContext.xml中代码为示例2.34所示:

示例2.34

<beans xmlns=http://www.springframework.org/schema/beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.0.xsd"><!-- 启用注解配置  --><aop:aspectj-autoproxy /><!-- 目标业务对象  --><bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean><!-- 日志管理切面  --><bean class="s3spring.ch2.log.annotation.LogAspect"></bean></beans>

注解配置方式和XML配置方式相同的是也需要在Spring应用容器的配置文件applicationContext.xml中引入AOP命名空间,不同的是我们采用注解来代替<aop:config >、<aop:pointcut>、<aop:aspect>、<aop:before>等等这些标签。

另外,我们需要用<aop:aspectj-autoproxy />标签来告诉spring应用容器启用AOP注解配置,Spring应用容器就会在初始化时扫描所有的bean,以找出应用AOP注解配置的切面bean。

id为“phoneBiz”的bean是目标业务对象,其实现类是示例2.1中的s3spring.ch2.biz.impl.PhoneBizImpl,代码和配置无需任何修改。

s3spring.ch2.log.annotation.LogAspect类作为日志管理切面的实现类被配置成bean,在类声明上方添加@Aspect注解即可将它声明为切面。请看示例:

示例2.35 


package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {……}

spring为我们提供了如下几个注解来帮助我们配置不同类型的通知:

l @Before,前置通知

@AfterReturning,后置通知

l @AfterThrowing,异常通知

l @After,最终通知

l @Around,环绕通知

在方法声明上方使用以上注解即可将方法声明为相应类型的通知,再为注解指定切入点表达式即可将通知织入该切入点。示例2.36演示了@Before、@AfterReturning、 @After和@Around注解的使用,请看示例2.36中粗体部分代码:

示例2.36

package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {

 

/** 前置通知 在目标方法执行之前执行日志记录 */

@Before("execution( void *Phone(int))")

public void before(JoinPoint jp) throws Throwable {  ……  }

 

/** 后置通知 在目标方法正常退出时执行日志记录 */

@AfterReturning("execution( void *Phone(int))")

public void afterReturning(JoinPoint jp) throws Throwable { …… }

 

/** 最终通知 无论目标方法正常退出还是异常退出都执行日志记录 */

@After("execution( void *Phone(int))")

    public void after(JoinPoint jp) throws Throwable {…… }

 

/** 环绕通知 */

@Around("execution( void *Phone(int))")

    public void after(JoinPoint jp) throws Throwable {…… }

 

     ……省略异常通知

}

@AfterThrowing注解配置异常通知除了需要指定切入点外还需要根据方法参数名称绑定异常对象,请看示例2.37中粗体部分代码:

示例2.37

package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {

     ……

/** 异常通知 在目标方法抛出参数指定类型异常时执行 */

@AfterThrowing(pointcut="execution( void *Phone(int))",throwing="e")

public void afterThrowing(JoinPoint jp,OutOfStockException e) {

……

}

}

@AfterThrowing注解的pointcut配置项用于指定切入点,Throwing配置项用于指定方法中表示异常对象的参数名称,这样可以将目标方法抛出的异常对象绑定到同类型叫e的方法参数。

1.1.1 切入点重用

在示例2.36小节中所有通知织入的其实都是同一个切入点,但是却多次重复编写相同的切入点表达式。下面让我们用@Pointcut注解结合切入点表达式在LogAspect类中定义一个切入点,并将before通知织入该切入点,请看示例2.38:

示例2.38

package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {

/** 切入点 */

@Pointcut("execution( void *Phone(int))")

public void p1(){}

 

/** 前置通知 在目标方法执行之前执行日志记录 */

@Before("s3spring.ch2.log.annotation.LogAspect.p1()")

public void before(JoinPoint jp) throws Throwable {  ……  }

 

     ……

}

上例中用一个叫做p1的空方法来表示一个切入点,当我们希望将通知织入该切入点时,在注解中用方法签名来代替切入点表达式即可。由于切入点的声明和通知的声明在同一个类中,可以省略包路径和类名,示例可以简写为示例2.39粗体部分代码:

示例2.39

package s3spring.ch2.log.annotation;

@Aspect

public class LogAspect {

/** 切入点 */

@Pointcut("execution( void *Phone(int))")

public void p1(){}

 

/** 前置通知 在目标方法执行之前执行日志记录 */

@Before("p1()")

public void before(JoinPoint jp) throws Throwable {  ……  }

}


1:使用Spring AOP实现商场购物

训练技能点

Ø Spring AOP前置通知

Ø AOP的配置

需求说明

某商场进行电冰箱促销活动,规定每位顾客只能购买一台特价电冰箱。顾客在购买电冰箱之前输出欢迎信息,顾客如果购买多台特价电冰箱,请给出错误提示,顾客成功购买电冰箱之后输出欢送信息。请使用Spring面向切面编程实现该需求的顾客欢迎信息显示。

实现思路

(1) 定义出售电冰箱的接口和接口实现。

(2) 定义前置通知。

(3) 编写配置文件。

关键代码

(1) 定义缺货异常

public class NoThisFrigException extends Exception {

public NoThisFrigException(String msg) {

super(msg);

}

}

(2) 定义顾客只能购买一台特价电冰箱的异常

public class BuyFrigException extends Exception {

public BuyFrigException(String msg)

{

super(msg);

}

}

(3) 定义电冰箱业务接口。

public interface FrigBiz {//出售电冰箱接口

public void buyFrig(String customer,String frig) throws NoThisFrigException;

}

(4) 定义电冰箱接口的实现类。

public class FrigBizImpl implements FrigBiz {

public void buyFrig(String customer, String frig) throws NoThisFrigException {

  if ("美的".equals(frig)) {

throw new NoThisFrigException("对不起,没有" + frig + "的货了");

        }

System.out.println("您好,您已经购买了一台" + frig);

}

}

(5) 定义前置通知,实现欢迎顾客的信息

public class FrigBefore {

public void before(Joinpoint jp) throws Throwable {

// 通过Joinpoint获得目标方法传入的参数customer值

   String customer = (String) jp.getArgs()[0];// 取得第一个参数,客户名称

   // 显示欢迎信息,在buyFrig方法前调用

    System.out.println("欢迎光临!" + customer + "!");

}

}

(6) 编写配置文件,使用Spring2.x方式配置

 <!-- 配置出售电冰箱的实现类 -->

<bean id="frigBiz" class="bean.FrigBizImpl" />

<!-- 配置前置通知 -->

<bean id="frigBefore" class="utils.FrigBefore" />

<!-- 配置代理对象 -->

<bean id="frigBizProxy" <!-- AOP配置 -->

<aop:config>

     <!-- 定义一个可以被多个切面共享的切入点 -->

<aop:pointcut id="p1"

expression="execution( void buyFrig(String,String))"/>

     <!-- 定义一个切面 -->

<aop:aspect id="logAspect" ref="frigBefore">

         <!-- 定义一个前置通知 -->

         <aop:before method="before" pointcut-ref="p1" />

</aop:aspect>

</aop:config>

 

(7) 编写测试代码测试。

public static void main(String[] args) throws NoThisFrigException {

ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");

FrigBiz frigBiz=(FrigBiz)context.getBean("frigBizProxy");

frigBiz.buyFrig("张无忌", "Lg");

}

}

2:完善商场购物实现顾客欢送信息

训练技能点

Ø Spring AOP后置通知

需求说明

实训任务1的基础上完善系统,当电冰箱卖出之后,显示“欢迎下次再来”。

实现思路

(1) 编写后置通知FrigAfter。

(2) 在配置文件中增加对后置通知的配置。

(3) 编写测试代码。

3:使用环绕通知重构商场购物

训练技能点

Ø Spring AOP环绕通知

Ø Spring2.x的AOP配置

需求说明

每位顾客只能购买一台特价电冰箱,已经购买电冰箱的顾客如果重复购买,则给出顾客限购的提示。

实现思路

(1) 定义环绕通知。

(2) 使用Spring2.x方式配置。

(3) 编写测试代码。

关键代码

(1) 定义环绕通知FrigAround。

public class FrigAround {

private Set<String> customers = new HashSet<String>();

public Object around(ProceedingJoinPoint pjp) throws Throwable {

Object[] args = pjp.getArgs();// 目标方法所有参数

String customer = (String)args[0];

String frig = (String) args[1];

 

if (customers.contains(customer)) {

throw new BuyFrigException("对不起,一名顾客只能购买一台特价电冰箱!您已购买一台特价" + frig);

}

 

try {

         return pjp.proceed();

      } finally{

customers.add(customer);

      }

}

}

(2) 新建配置文件aop.xml,配置环绕通知。

<!-- 配置出售电冰箱的实现类 -->

<bean id="frigBiz" class="bean.FrigBizImpl" />

<!-- 配置环绕通知 -->

<bean id="frigAround" class="utils.FrigAround" />

<aop:config>

     <!-- 定义一个可以被多个切面共享的切入点 -->

<aop:pointcut id="p1"

expression="execution( void buyFrig(String,String))"/>

     <!-- 定义一个切面 -->

<aop:aspect id="frigAspect" ref="frigAround">

         <!-- 定义一个后置通知 -->

         <aop:around method="around" pointcut-ref="p1" />

</aop:aspect>

</aop:config>

4:为商场购物添加库存验证

训练技能点

Ø Spring AOP异常通知

需求说明

升级商场购物,如果电冰箱库存不足,给出订货提示。

实现思路

(1) 编写异常通知FrigThrows。

(2) aop.xml中增加异常通知的配置。

(3) 编写测试代码。

关键代码

(1) 编写异常通知。

public class FrigThrows{

public void afterThrowing(NoThisFrigException e) throws Throwable {

System.out.println("通知仓库,赶紧订货!");

}

}

(2) aop.xml中增加FrigThrows的配置。

<bean id="frigBiz" class="bean.FrigBizImpl" />

<bean id="frigThrows" class="utils.FrigThrows" />

<aop:config>

     <!-- 定义一个可以被多个切面共享的切入点 -->

<aop:pointcut id="p1"

expression="execution( void buyFrig(String,String))"/>

     <!-- 定义一个切面 -->

<aop:aspect id="frigAspect" ref="frigThrows">

         <!-- 定义一个异常通知 -->

         <aop:after-throwing method="afterThrowing" pointcut-ref="p1" />

</aop:aspect>

</aop:config>

 

5:应用注解配置方式将1~4题重新配置

训练技能点

Ø Spring AOP注解配置


巩固练习

一.选择题

1. Spring通知不包括 ()。

    A. 前置通知

    B. 后置通知

    C. 环绕通知

    D. 设置通知

2. 以下对AOP 的说法中,错误的是 ()。

    A. AOP将散落在系统中的“方面”代码集中实现

    B. AOP有助于提高系统的可维护性

    C. AOP可以取代OOP

    D. AOP能大大简化程序的代码

3. 下列关于Spring AOP的说法错误的是 ()。

    A. 可支持前置通知、后置通知、环绕通知

    B. Spring AOP采用拦截方法调用的方式实现,可以在调用方法前、调用后、抛出异常时拦截

    C. Spring AOP采用代理的方式实现

    D. 采用Spring2.x方式配置的时候,不会产生AOP代理

4. 在 Spring框架中,面向方面编程 (AOP)的目标在于 ()。

    A. 编写程序时无须关注其依赖组件的实现

    B. 封装JDBC访问数据库的代码,简化数据访问层的重复性代码

    C. 将程序中涉及的公共问题集中解决

    D. 可以通过Web服务调用

5.下面关于 Spring AOP 错误的是 ()。

A. 任何一个通知的实现发那个发都可以以JoinPoint或ProceedingJoinPoint为

参数

B. 只有环绕通知可以使用ProceedingJoinPoint为入参,其它类型通知只能使用

JoinPoint为入参

C. JoinPoint和ProceedingJoinPoint都有proceed()方法,用于执行目标发方

D. 仅ProceedingJoinPoint有proceed()方法

6. 以下那一个注脚不是用于定义通知的? ()。

    A. @After

    B. @Before

    C. @Aspect

    D. @AfterThorwing

 

二.操作题

1.在第一章操作题第1题的基础上实现以下功能。

现举行活动,升级指环的话,可以免费将任意指环升级为“紫色梦幻”指环,新的装备名称为“紫色梦幻+原指环名”,而且将在原有的基础上再加6点攻击,6点防御。

提示:使用前置通知,判断要升级的装备类型是否为指环,如果是则按照要求修改传入参数的名称以及攻击增效和防御增效的属性。

2.在电子商务网站购物时,需要对生成订单的过程进行日志记录,以便于查询交易过程。该系统有一个生成订单的业务,请使用AOP方式实现在交易方法调用时记录客户信息及生成订单的时间。

3.在银行管理系统中,转账是很重要的操作,如张三向李四的账户转账1000元,需要经过的步骤是:

1)修改张三账户信息,从账户中扣除1000元。

2)修改李四账户信息,在李四账户中增加1000元钱。

3)向交易记录表中增加一条记录。

以上三步操作必须运行在同一事务,而且任何一步出现异常,事务必须回滚。请使用SpringAOP实现事务控制。

提示:

BankBiz中添加一个转账方法,该方法实现以上三步操作,然后编写一个前置通知、后置通知和异常通知。在前置通知中开启事务,在后置通知中提交事务,在异常通知中进行事务回滚。

开启事务的方法:

HibernateSessionFactory.getSession().beginTransaction();

提交事务的方法:

HibernateSessionFactory.getSession().beginTransaction().commit();

事务回滚的方法:

HibernateSessionFactory.getSession().beginTransaction().rollback();

4.升级“会员账户管理系统”,使用Spring的面向切面编程为会员状态的更改、会员充值、会员信息的删除增加日志,日志要求:

1)会员状态的更改,需要记录被更改状态的会员的编号和更改时间。

2)会员充值的时候,如果充值失败,记录失败原因及充值操作时间。如果充值成功,记录被充值会员的编号,充值时间和充值金额。

3)会员信息的删除,需要记录被删除的会员编号、删除时间。

4)日志信息在控制台输出。