《Spring In action》学习笔记——AOP(面向切面编程)

1.AOP术语

通知(Advice):
切面的功能被称为Advice(通知),它定义了切面是什么及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应用在某个方法被调用之前?之后?之前和之后?或是只在方法抛出一个异常时?

连接点(Joinpoint):
Joinpoint是在程序执行过程中能够插入切面的一个点。这个点可以是方法被调用时、异常被抛出时、甚至字段被编辑时。切面代码可以通过这个点插入到程序的一般流程中,从而添加新的行为。

切入点(Pointcut):
切入点可以缩小切面通知的连接点的范围。如果说advice定义了切面的“什么”和“何时”,那么切入点就定义了“何地”。切入点的定义匹配advice要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义匹配的类和方法名称模板来指定这些切入点。有些AOP框架允许创建动态切入点,可以根据运行时的状态(比如方法的参数值)来应用通知。

切面(Aspect):
切面就是通知和切入点的结合。通知和切入点共同定义了关于切面的全部内容——它的功能、在何时和何地完成其功能。

目标(Target):
“目标”是被通知的对象,它可以是我们编写的一个对象,或第三方对象。如果没有AOP,这个对象就必须包含自己的主要逻辑和交叉事务的逻辑。通过使用AOP,目标对象就可以着重于自己的主要逻辑,不必考虑要被应用的任何通知。

代理(Proxy)
代理是向目标对象应用通知之后被创建的对象。对于客户对象来说,目标对象(AOP之前)和代理对象(AOP之后)是一样的——它们就应用是这样的。这样一来,程序的其他部分就不必修改对代理对象的支持。

织入(Weaving)
织入是把切面应用到目标对象来创建新的代理对象的过程。切面在指定连接点织入到目标对象。在目标对象的生命周期里有多个时机可以发生织入过程:
  • 编译时:切面在目标类编译时被织入。这需要特殊的编译器,AspectJ在织入编译器就以这种方式织入切面。
  • 类加载时:切面在目标类加载到JVM时被织入。这需要特殊的ClassLoader,它可以在目标类被加载到程序之前增强类的字节代码。AspectJ5的“加载时织入(LTW)”就以这种方式支持织入切面。
  • 运行时:切面在程序运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会动态创建一个代理对象来委托给目标对象。这就是Spring AOP织入切面的方式。

2.Spring对AOP的支持

Spring对AOP的支持具有以下4种情况:
  • 经典的基于代理的AOP(各版本Spring)
  • @AspectJ注解驱动的切面(仅Spring 2.0);
  • 纯POJO切面(仅Spring2.0);
  • 注入式AspectJ切面(各版本Spring)
前三种都是基于代理的AOP的变体,因此,Spring对AOP的支持局限于方法注入。如果我们的AOP需求超过了简单方法注入的范畴(比如构造器或属性注入),就应该考虑在AspectJ里实现切面,利用Spring的从属注入把Spring的Bean注入到AspectJ切面

由于Spring是在运行时创建代理,所以我们不需要使用特殊的编译器把切面织入到Spring的AOP。

Spring生成被代理类的方式有两种。如果目标对象实现的接口中包含需要代理的方法,Spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口、织入任何通知、并且把对这些接口的任何调用都转发到目标类。如果目标类没有实现一个接口或实现的接口中不包含需要代理的方法,Spring就 使用CGLIB库生成目标类的一个子类。在创建这个子类时,Spring织入通知,并且把对这个子类的调用委托到目标类。在使用这种方式时,有两点需要注意:
  • 使用前一种创建接口的代理方式比使用代理类更受人喜欢,因为它能够更好地实现程序的松耦合。
  • 被标记为final的方法不能被通知。Spring为目标类创建一个子类,需要被通知的任务方法都会被覆盖并被织入通知,这对于final类型的方法是不可能的。
Spring是基于动态代理的,这只支持方法连接点。如果需要方法截取之外的功能,可以利用AspectJ来补充Spring的AOP。

3.典型的Spring切面

场景:在一场表演中,表演前观众找位置,并关闭手机,表演精彩时观众鼓掌,表演槽糕的时候观众要求退票。现在在这个场景下使用Spring的切面
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:
<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)了:
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()方法:
public void before(Method arg0, Object[] arg1, Object arg2)
			throws Throwable {}
第一个参数代表要使用这个通知的方法。第二个参数是方法被调用时要传递的参数。最后一个参数是方法调用的目标(也就是被调用方法所在的对象)。

返回后通知——AfterReturningAdvice
这个通知需要实现afterReturning()方法:
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类可以用周围通知来重写。如:
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类来定义正则表达式切点:
<bean id="performancePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
	<property name="pattern" value=".*perform" />
</bean>

接下来需要把切入点与通知关联起来,可以用类org.springframework.aop.support.DefaultPointcutAdvisor把这种关系关联起来。
<bean id="audienceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
	<property name="advice" ref="audienceAdvice" />
	<property name="pointcut" ref="performancePointcut" />
</bean>

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

联合切点与通知
RegexpMethodPointcutAdvisor是个特殊的通知者类,可以在一个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切点表达式:
<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:
<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。示例:
<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,达到简化配置的目的。

4.自动代理

自动代理能够让切面的切点定义来决定哪个Bean需要代理,不需要我们为特定的Bean明确地创建代理,从而提供了一个更完整的AOP实现。

实现自动代理Bean的方式有两种:
  • “基于Spring上下文里声明的通知者Bean的基本自动代理”:通知者的切点表达式用于决定哪个Bean和哪个方法要被代理。
  • “基于@AspectJ注解驱动切面的自动代理”:切面里包含的通知里指定的切点将用于选择哪个Bean和哪个方法要被代理。

4.1 为Spring切面创建自动代理

Spring提供了BeanPostProcessor的一个方便实现:DefaultAdvisorAutoProxyCreator,它会自动检查通知者的切点是否匹配Bean的方法,并且使用通知的代理来替换这个Bean的定义。要使用DefaultAdvisorAutoProxyCreator,只需要以下三步:
  1. 像上一节中定义通知者Bean。
  2. 添加DefaultAdvisorAutoProxyCreator的Bean定义。注意不需要id:
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />

  3. 声明目标Bean(像普通的Bean一样声明),但实际上会以代理Bean取代:
    <bean id="duke" class="com.springinaction.springido1.PoeticJuggler" autowire="constructor">
    <constructor-arg ref="sonnet29" />
    </bean>

4.2 自动代理@AspectJ切面

AspectJ5里一个主要新功能是能够把POJO类注解为切面,这通常被称为@AspectJ。

利用@AspectJ注解,我们不需要声明任何额外的类或Bean,就可以把它转化为一个切面。如:
package com.sin90lzc.test;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

//声明切面
@Aspect
public class Audience {
	public Audience() {
	}

	// 定义切入点
	@Pointcut("execution(* *.perform(...))")
	public void performance() {
	}

	@Before("performance()")
	public void takeSeats() {
		System.out.println("The audience is taking their seats.");
	}

	@Before("performance()")
	public void turnOffCellPhones() {
		System.out.println("The audience is turning off their cellphones");
	}

	@AfterReturning("performance()")
	public void applaud() {
		System.out.println("CLAP CLAP CLAP CLAP CLAP");
	}

	@AfterThrowing("performance()")
	public void demandRefund() {
		System.out.println("Boo!We want our money back!");
	}
}
@Pointcut注解在@AspectJ切面里定义一个可重用的切点。赋给@Pointcut注解的值是一个AspectJ切点表达式,表示这个切点应该匹配任意一个类的perform()方法。这个切点的名称是源自注解所应用的方法的名称,所以本例中切点的名称就是performance()。实际的performance()方法与此并不相关,方法本身只是一个标记。

最后,我们还必须在Spring上下文里声明一个自动代理Bean,它知道如何把@AspectJ注解的Bean转化为代理通知。Spring提供了一个自动代理创建器类——AnnotaionAwareAspectJAutoProxyCreator,我们可以在Spring上下文里把它注册为一个<bean>。为了简化,可使用Spring提供的一个自定义配置元素:
<aop:aspectj-autoproxy />

这个元素会在Spring上下文创建一个AnnotaionAwareAspectJAutoProxyCreator,从而根据@Pointcut注解定义的切点来自动代理相匹配的Bean。要使用该配置元素,还需要添加aop命名空间:
<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">
</beans>

AnnotaionAwareAspectJAutoProxyCreator也基于典型的Spring通知者来创建代理,也就是说,它也会完成与DefaultAdvisorAutoProxyCreator同样的工作。因此,如果在Spring上下文里声明了通知者Bean,它们也会被自动用于通知被代理的Bean。

4.3 定义纯粹的POJO切面

Spring2.0在aop命名空间里还提供了其他一些配置元素,简化了把类转化为切面的操作。
Spring2.0的AOP配置元素
AOP配置元素 功能
<aop:advisor> 定义一个AOP通知者
<aop:after> 定义一个AOP后通知(不考虑被通知的方法是否成功返回)
<aop:after-returning> 定义一个AOP返回后通知
<aop:after-throwing> 定义一个AOP抛出后通知
<aop:around> 定义一个AOP周围通知
<aop:aspect> 定义一个切面
<aop:before> 定义一个AOP前通知
<aop:config> 顶级AOP元素。大多数<aop:*>元素必须包含在<aop:config>里
<aop:Pointcut> 定义一个切点

下面示例把audience Bean转化为切面:
<bean id="audience" class="com.springinaction.springido1.Audience" />

<aop:config>
	<aop:aspect ref="audience">
		<aop:pointcut id="performance" expression="execution(* *.perform(..))" />
		<aop:before method="takeSeats" pointcut-ref="performance" />
		<aop:before method="turnOffCellPhones" pointcut="execution(* *.perform(..))" />
		<aop:after-returning method="applaud"
			pointcut-ref="performance" />
		<aop:after-throwing method="demandRefund"
			pointcut-ref="performance" />
	</aop:aspect>
</aop:config>

4.4 注入AspectJ切面

你可能感兴趣的:(spring,AOP,bean,正则表达式,action,performance)