AOP的理解

在传统的编写业务逻辑处理代码时,我们通常会习惯性地做几件事情:日志记录、事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码。当代码编写完成回头再看时,不禁发现,扬扬洒洒上百行代码中,真正用于核心业务逻辑处理才那么几行,如图6-4所示。方法复方法,类复类,就这样子带着无可奈何遗憾地度过了多少个春秋。这倒也罢,倘若到了项目的尾声,突然决定在权限控制上需要进行大的变动时,成千上万个方法又得一一"登门拜访",痛苦"雪上加霜"。

AOP的理解

如果能把图6-4中众多方法中的所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码的话,最起码可以解决两个问题:

Java EE程序员在编写具体的业务逻辑处理方法时,只需关心核心的业务逻辑处理,既提高了工作效率,又使代码变更简洁优雅。

在日后的维护中由于业务逻辑代码与共有代码分开存放,而且共有代码是集中存放的,因此使维护工作变得简单轻松。

面向切面编程AOP技术就是为解决这个问题而诞生的,切面就是横切面,如图6-5所示,代表的是一个普遍存在的共有功能,例如,日志切面、权限切面及事务切面等。

AOP的理解

下面我们以用户管理业务逻辑组件UserService的AOP实现过程(见图6-6)为例,深度剖析一下AOP技术的实现原理。AOP技术是建立在Java语言的反射机制与动态代理机制之上的。业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按Java EE程序员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能同时得以执行。从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。

AOP的理解

现将图6-6中涉及到的一些概念解释如下。

切面(Aspect):其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类,之所以能被AOP容器识别成切面,是在配置中指定的。

通知(Advice):是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。

连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Spring只支持方法级的连接点。

切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能代码等待AOP容器的切入。

代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑功能加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。

织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期,当然不同的发生点有着不同的前提条件。譬如发生在编译期的话,就要求有一个支持这种AOP实现的特殊编译器;发生在类装载期,就要求有一个支持AOP实现的特殊类装载器;只有发生在运行期,则可直接通过Java语言的反射机制与动态代理机制来动态实现。

3.典型的Spring切面

场景:在一场表演中,表演前观众找位置,并关闭手机,表演精彩时观众鼓掌,表演槽糕的时候观众要求退票。现在在这个场景下使用Spring的切面
[java] view plain copy print ?
  1. package com.sin90lzc.test;  
  2. //观众类  
  3. public class Audience {  
  4.    public Audience() {  
  5.    }  
  6.    //表演前找座位  
  7.    public void takeSeats() {  
  8.        System.out.println("The audience is taking their seats.");  
  9.    }  
  10.    //找到座位后关掉手机  
  11.    public void turnOffCellPhones() {  
  12.        System.out.println("The audience is turning off their cellphones");  
  13.    }  
  14.    //表演精彩时鼓掌  
  15.    public void applaud() {  
  16.        System.out.println("CLAP CLAP CLAP CLAP CLAP");  
  17.    }  
  18.    //表演槽糕时要求退票  
  19.    public void demandRefund() {  
  20.        System.out.println("Boo!We want our money back!");  
  21.    }  
  22. }  
package com.sin90lzc.test;
//观众类
public class Audience {
	public Audience() {
	}
	//表演前找座位
	public void takeSeats() {
		System.out.println("The audience is taking their seats.");
	}
	//找到座位后关掉手机
	public void turnOffCellPhones() {
		System.out.println("The audience is turning off their cellphones");
	}
	//表演精彩时鼓掌
	public void applaud() {
		System.out.println("CLAP CLAP CLAP CLAP CLAP");
	}
	//表演槽糕时要求退票
	public void demandRefund() {
		System.out.println("Boo!We want our money back!");
	}
}
由Spring容器管理Audience:
[html] view plain copy print ?
  1. <bean id="audience" class="com.springinaction.springido1.Audience" />  
<bean id="audience" class="com.springinaction.springido1.Audience" />

3.1创建通知

Spring的AOP里有5种时间点的通知,分别由一个接口进行定义:

Spring AOP的5个时间点
通知类型 接口
Before org.springframework.aop.MethodBeforeAdvice
After-returning org.springframework.aop.AfterReturningAdvice
After-throwing org.springframework.aop.ThrowsAdvice
Around org.aopalliance.intercept.MethodInterceptor
Introduction org.springframework.aop.IntroductionInterceptor

除了MethodInterceptor之外,这些接口都属于Spring框架。在定义周围通知时,Spring利用了由AOP Alliance提供的接口,这是一个开源项目,其宗旨在于AOP的简化及标准化。如果需要进一步了解AOP Alliance,可以访问其站点 http://aopalliance.sourceforge.net

现在就可以根据上面的场景建立一个通知(advice)了:
[java] view plain copy print ?
  1. package com.sin90lzc.test;  
  2.  
  3. import java.lang.reflect.Method;  
  4.  
  5. import org.springframework.aop.AfterReturningAdvice;  
  6. import org.springframework.aop.MethodBeforeAdvice;  
  7. import org.springframework.aop.ThrowsAdvice;  
  8.  
  9. public class AudienceAdvice implements MethodBeforeAdvice,  
  10.        AfterReturningAdvice, ThrowsAdvice {  
  11.  
  12.    private Audience audience;  
  13.  
  14.    public void afterReturning(Object arg0, Method arg1, Object[] arg2,  
  15.            Object arg3) throws Throwable {  
  16.        audience.applaud();  
  17.    }  
  18.  
  19.    public void before(Method arg0, Object[] arg1, Object arg2)  
  20.            throws Throwable {  
  21.        audience.takeSeats();  
  22.        audience.turnOffCellPhones();  
  23.    }  
  24.  
  25.    public void afterThrowing(Throwable throwable) {  
  26.  
  27.    }  
  28.  
  29.    public Audience getAudience() {  
  30.        return audience;  
  31.    }  
  32.  
  33.    public void setAudience(Audience audience) {  
  34.        this.audience = audience;  
  35.    }  
  36.  
  37. }  
package com.sin90lzc.test;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;

public class AudienceAdvice implements MethodBeforeAdvice,
		AfterReturningAdvice, ThrowsAdvice {

	private Audience audience;

	public void afterReturning(Object arg0, Method arg1, Object[] arg2,
			Object arg3) throws Throwable {
		audience.applaud();
	}

	public void before(Method arg0, Object[] arg1, Object arg2)
			throws Throwable {
		audience.takeSeats();
		audience.turnOffCellPhones();
	}

	public void afterThrowing(Throwable throwable) {

	}

	public Audience getAudience() {
		return audience;
	}

	public void setAudience(Audience audience) {
		this.audience = audience;
	}

}

这里一个类中实现了3种不同类型的AOP通知。

前通知——MethodBeforeAdvice
在进入切入点(方法调用前)调用。MethodBeforeAdvice要求实现一个Before()方法:
[java] view plain copy print ?
  1. public void before(Method arg0, Object[] arg1, Object arg2)  
  2.            throws Throwable {}  
public void before(Method arg0, Object[] arg1, Object arg2)
			throws Throwable {}
第一个参数代表要使用这个通知的方法。第二个参数是方法被调用时要传递的参数。最后一个参数是方法调用的目标(也就是被调用方法所在的对象)。

返回后通知——AfterReturningAdvice
这个通知需要实现afterReturning()方法:
[java] view plain copy print ?
  1. public void afterReturning(Object arg0, Method arg1, Object[] arg2,  
  2.            Object arg3) throws Throwable {}  
public void afterReturning(Object arg0, Method arg1, Object[] arg2,
			Object arg3) throws Throwable {}
aferReturning方法的参数与MethodBeforeAdvice的before()方法的参数区别不大,只是第一个参数是从被调用方法返回的值。

抛出后通知——ThrowsAdvice
与MethodBeforeAdvice和AfterReturningAdvice不同的是,ThrowsAdvice不需要实现任何方法,它只是一个标记接口,告诉Spring相应的通知要求处理被抛出的异常。

对于ThrowsAdvice的实现可以是一个或多个afterThrowing()方法,其签名具有如下形式:
public void afterThrowing([method],[args],[target],throwable);

周围通知——MethodInterceptor
周围通知相当于前通知、返回后通知、抛出后通知的结合。AudienceAdvice类可以用周围通知来重写。如:
[java] view plain copy print ?
  1. package com.sin90lzc.test;  
  2.  
  3. import org.aopalliance.intercept.MethodInterceptor;  
  4. import org.aopalliance.intercept.MethodInvocation;  
  5.  
  6. public class AudienceAroundAdvice implements MethodInterceptor {  
  7.  
  8.    public Object invoke(MethodInvocation invocation) throws Throwable {  
  9.        try {  
  10.            audience.takeSeats();  
  11.            audience.turnOffCellPhones();  
  12.            Object returnValue = invocation.proceed();  
  13.            audience.applaud();  
  14.            return returnValue;  
  15.        } catch (Exception e) {  
  16.            audience.demandRefund();  
  17.            throw e;  
  18.        }  
  19.    }  
  20.  
  21.    public Audience getAudience() {  
  22.        return audience;  
  23.    }  
  24.  
  25.    public void setAudience(Audience audience) {  
  26.        this.audience = audience;  
  27.    }  
  28.  
  29.    private Audience audience;  
  30. }  
package com.sin90lzc.test;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AudienceAroundAdvice implements MethodInterceptor {

	public Object invoke(MethodInvocation invocation) throws Throwable {
		try {
			audience.takeSeats();
			audience.turnOffCellPhones();
			Object returnValue = invocation.proceed();
			audience.applaud();
			return returnValue;
		} catch (Exception e) {
			audience.demandRefund();
			throw e;
		}
	}

	public Audience getAudience() {
		return audience;
	}

	public void setAudience(Audience audience) {
		this.audience = audience;
	}

	private Audience audience;
}
使用周围通知的好处之一是能以简洁的方式在一个方法里定义前通知和后通知。 利用周围通知还可以检查和修改被通知方法的返回值,让我们可以在把方法的返回值传递给调用者之前对其进行一些后处理。AfterReturningAdvice只能对返回值进行检查,但不能修改它。

3.2 定义切入点和通知者

Advice(通知)已经解决了切面“做什么”和“何时”的问题。接下来就要解决“在哪里”的问题了。也就是切入点的定义。

Spring提供了几种不同类型的切点,其中两种最有用的是正则表达式切点和AspectJ表达式切点。

3.2.1声明正则表达式切点

使用org.springframework.aop.supprot.JdkRegexpMethodPointcut类来定义正则表达式切点:
[html] view plain copy print ?
  1. <bean id="performancePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">  
  2.    <property name="pattern" value=".*perform" />  
  3. </bean>  
<bean id="performancePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
	<property name="pattern" value=".*perform" />
</bean>

接下来需要把切入点与通知关联起来,可以用类org.springframework.aop.support.DefaultPointcutAdvisor把这种关系关联起来。
[html] view plain copy print ?
  1. <bean id="audienceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">  
  2.    <property name="advice" ref="audienceAdvice" />  
  3.    <property name="pointcut" ref="performancePointcut" />  
  4. </bean>  
<bean id="audienceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="pointcut" ref="performancePointcut" />
</bean>

DefaultPointcutAdvisor是个通知者类,它只是把通知关联到切点。

联合切点与通知
RegexpMethodPointcutAdvisor是个特殊的通知者类,可以在一个Bean里定义切点和通知:
[html] view plain copy print ?
  1. <bean id="audienceAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">  
  2.    <property name="advice" ref="audienceAdvice" />  
  3.    <property name="pattern" value=".*perform" />  
  4. </bean>  
<bean id="audienceAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="pattern" value=".*perform" />
</bean>

3.2.2定义AspectJ切点

正则表达式虽然可以作为切点定义语言来使用,但它并不是针对切点而创建的,其主要用途还是文本解析。与之相比,从AspectJ里定义切点的方式就可以看出AspectJ的切点语言是一种真正的切点表达语言。
Spring使用类org.springframework.aop.aspectj.AspectJExpressionPointcut来定义AspectJ切点表达式:
[html] view plain copy print ?
  1. <bean id="performancePointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">  
  2.    <property name="expression" value="execution(* Performer+.perform(..))" />  
  3. </bean>  
<bean id="performancePointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
	<property name="expression" value="execution(* Performer+.perform(..))" />
</bean>

为了把AspectJ表达式切点与通知关联起来,可以使用DefaultPointcutAdvisor,就像正则表达式切点一样。同样的,我们可以利用特殊的通知者,把切点表达式定义为通知者的一个属性。对于AspectJ表达式来说,使用的通知者类是org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor:
[html] view plain copy print ?
  1. <bean id="audienceAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">  
  2.    <property name="advice" ref="audienceAdvice" />  
  3.    <property name="expression" value="execution(* Performer+.perform(..))" />  
  4. </bean>  
<bean id="audienceAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="expression" value="execution(* Performer+.perform(..))" />
</bean>

通知者把通知与切点关联起来,从而完整地定义一个切面。但是,切面在Spring里是以代理方式实现的,所以仍然需要代理目标Bean才能让通知者发挥作用。

3.3 使用ProxyFactoryBean

Spring的ProxyFactoryBean是个工厂Bean,用于生成一个代理,把一个或多个通知者应用到Bean。示例:
[html] view plain copy print ?
  1. <bean id="duke" class="org.springframework.aop.framework.ProxyFactoryBean">  
  2.    <property name="target" ref="dukeTarget" />  
  3.    <property name="interceptorNames">  
  4.        <list>  
  5.            <value>audienceAdvisor</value>  
  6.        </list>  
  7.    </property>  
  8.    <property name="proxyInterfaces">  
  9.        <list>  
  10.            <value>com.springinaction.springido1.Performer</value>  
  11.        </list>  
  12.    </property>  
  13. </bean>  
<bean id="duke" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="dukeTarget" />
	<property name="interceptorNames">
		<list>
			<value>audienceAdvisor</value>
		</list>
	</property>
	<property name="proxyInterfaces">
		<list>
			<value>com.springinaction.springido1.Performer</value>
		</list>
	</property>
</bean>

当然,当有多个Bean都需要代理相同的接口或通知者时,可以抽象ProxyFactoryBean,达到简化配置的目的。

你可能感兴趣的:(AOP的理解)