《Spring实战》读书笔记3


一、面向切面的Spring

       

在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),例如,日志、安全和事务管理,通常这些横切关注点从概念上是与应用的业务逻辑相分离的,把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

1、AOP术语

【】通知(Advice):切面要完成的工作,被称为通知;(通知定义了切面是什么以及何时使用)

Spring切面有5种类型的通知:

       * 前置通知(Before):在目标方法被调用之前调用通知功能;

       * 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出时什么;

       * 返回通知(After-returning):在目标方法成功执行之后调用通知;

       * 异常通知(After-throwing):在目标方法抛出异常后调用通知;

       * 环绕通知(Around):通知包裹了被通知的方法,在通知的方法调用之前和调用之后执行自定义的行为;

【】连接点(Join point):连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。

【】切点(Poincut):通常一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。切点定义了“何处”应用切面。

【】切面(Aspect):切面是通知和切点的结合,切面也就是在何时和何处完成其功能。

【】引入(Introduction):引入允许我们向现有的类添加新方法或属性。从而可以在无需修改这些现有类的情况下,让它们具有新的行为和状态;

【】织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中;

       在目标对象生命周期的多个点可以进行织入:

       * 编译器:切面在目标类编译时被织入。这种方式需要特殊的编译器;

       * 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。

       * 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。

 注:通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。


2、Spring对AOP的支持

【】Spring提供了4中类型的AOP支持

      * 基于代理的经典Spring AOP;

      * 纯POJO切面;

      * @AspectJ注解驱动的切面;

      * 注入式AspectJ切面(适用于Spring各版本)。

注:前三种都是Spring AOP实现的变体,Spring AOP创建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

      @AspectJ注解驱动的切面,是Spring借鉴AspectJ的切面,以提供注解驱动的AOP,本质上,它依然是Spring基于代理的AOP。

      注入式AspectJ切面,适用于你的AOP需求超过了简单的方法调用,这时可以考虑使用AspectJ来实现切面。

【】关于Spring AOP框架的一些关键知识

     * Spring通知是Java编写的;

     * Spring在运行时通知对象;

     * Spring只支持方法级别的连接点;

【】Spring AOP框架的实现机制

     Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。



3、编写切点的基础知识

【】在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。Spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。

      Spring AOP所支持的AspectJ切点指示器如下:

      * arg() : 限制连接点匹配参数为指定类型的执行方法

      * @args():限制连接点匹配参数由指定注解标注的执行方法

      * execution():用于匹配时连接点的执行方法

      * this() : 限定连接点匹配AOP代理的bean引用为指定类型的类

      * target : 限制连接点匹配目标对象为指定类型的类

      * @target():限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解

      * within() : 限制连接点匹配指定的类型

      * @within():限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)

      * annotation :限定匹配带有指定注解的连接点

      在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。

如下例子展示切点的编写:

      例如:我们有一个需要应用切面的一个主题,如:

package concert;

public interface Performance {
	public void perform();
}
       如果使用execution()指示器选择Performance的perform()方法。表达式是:execution(* concert.Performance.perform(..) ), 其中方法表达式以“*”号开始,表明不关心方法返回值的类型。然后指定额全限定类名和方法名,方法参数列表中的两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参时什么。

       如果希望切点只匹配concert包,可以使用within()指示器来限制匹配。如:execution( * concert.Performance.perform(..)) && within(concert.*)

注:表达式中的“&&”是逻辑运算符,也可以是“||”、“!”;

       由于“&”在XML中有特殊含义,所以Spring的XML配置里面描述切点时,我么可以使用and来代替“&&”,使用or来替代“||”, not来替代“!”;

【】除了上述指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。

      例如:execution( * concert.Performance.perform()) and bean( ‘woodstock’) ,该表达式表示,在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。


二、使用注解创建切面

     

使用注解创建切面是AspectJ 5所引入的关键特性,AspectJ面向注解的模型可以非常简便地通过少量注解把任意类转变为切面。

Spring使用AspectJ注解来声明通知方法

@After  :  通知方法会在目标方法返回或抛出异常后调用

@AfterReturning : 通知方法会在目标方法返回后调用

@AfterThrowing:通知方法会在目标方法抛出异常后调用

@Around : 通知方法会将目标方法封装起来

@Before : 通知方法会在目标方法调用之前执行


(1)、定义切面

 @Aspect 注解表明这是一个切面

@Pointcut注解,定义一个可重用的切点

例如,下面代码定义了一个切面Audience:

@Aspect
public class Audience {

	@Pointcut("execution(** com.xiaoxiaoyusheng.Performance.perform(..))*")
	public void performance() {}
	
	@Before("performance()")
	public void silenceCellPhones() {
		System.out.println("Silencing cell phones");
	}
	
	@Before("performance()") 
	public void takeSeats() {
		System.out.println("Taking seats");
	}
	
	@AfterReturning("performance()")
	public void applause() {
		System.out.println("CLAP CLAP CLAP!!!");
	}
	
	@AfterThrowing("performance()") 
	public void demandRefund() {
		System.out.println("Demanding a refund");
	}
	
}
其中,@Aspect 注解表明这是一个切面, @Pointcut注解定义了一个切点,剩余注解声明了通知方法。
注:切点定义中的performance()方法的实际内容并不重要,在这里它实际上应该是空的,其实该方法本身只是一个标识,供@Pointcut注解依附。

注:此时Audience只是一个Java类,只不过通过注解表明会作为切面适用而已。


(2)、启动切面自动代理

【】在JavaConfig中启用AspectJ注解的自动代理

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
	
	@Bean
	public Audience audience() {
		return new Audience();
	}
}

【】在XML中,通过Spring的aop命名空间启用AspectJ自动代理

<?xml version="1.0" encoding="UTF-8"?>
<bean xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/aop
	http://www.springframework.org/schema.aop/spring-aop.xsd
	http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd">
	<context:component-scan base-package="com.xiaoxiaoyusheng" />
	<aop:aspectj-autoproxy />
    <bean class="com.xiaoxiaoyusheng.Audience" />	
</bean>


(3)、创建环绕通知

      环绕通知能够将目标方法完全包装起来,就像在一个通知方法中同时编写前置通知和后置通知。

例如:

@Aspect
public class Audience {

	@Pointcut("execution(** com.xiaoxiaoyusheng.Performance.perform(..))*")
	public void performance() {}
	
	@Around("performance()")
	public void watchPerformance(ProceedingJoinPoint jp) {
		try {
			System.out.println("Silencing cell phones");
			System.out.println("Taking seats");
			jp.proceed();
			System.out.println("CLAP CLAP CLAP!!!");
		} catch (Throwable e) {
			System.out.println("Demanding a refund");
		}
	}
}
注:@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知,上例中使用一个通知方法,实现了之前多个通知方法实现的通知功能。

注意ProceedingJoinPoint作为参数是必须的,因为环绕通知中通过它来调用被通知的方法。当需要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法,如果不调用这个方法,那么通知实际上会阻塞对被通知方法的调用。与之类似,你也可以在通知中对它进行多次调用。


(4)、处理通知中的参数

      上述的切面都很简单,没有任何参数,是因为我们不关心目标方法的参数,而且perform()本身也没有参数。其实在切点声明时是可以提供通知方法的参数的。例如:

execution( * soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)

      上述切点表达式中声明了参数,这个参数会被传入到通知方法中,切点表达式中的args(trackNumber)限定符,它表明传递给目标方法playTrack()的int类型参数也会传递到通知方法中去。参数的名称tragckNUmber也与切点方法签名中的参数相匹配。

下例演示了带参数的通知的完整过程:

首先是记录次数的切面:

@Aspect
public class TrackCounter {

	private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
	
	@Pointcut ("execution(* com.xiaoxiaoyusheng.soundsystem.CompactDisc.playTrack(int)) " + 
	"&& args(trackNumber)")
	public void playTrack(int trackNumber) {}
	
	@Before("playTrack(trackNumber)")
	public void countTrack(int trackNumber) {
		int currentCount = getPlayCount(trackNumber);
		trackCounts.put(trackNumber, currentCount + 1);
	}
	
	public int getPlayCount(int trackNumber) {
		return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
	}
}
配置该切面类和目标类

@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {

	@Bean
	public CompactDisc sgtPeppers() {
		BlankDisc cd = new BlankDisc();
		cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
		cd.setArtist("The Beatles");
		List<String> tracks = new ArrayList<String>();
		tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
		
		cd.setTracks(tracks);
		
		return cd;
	}
	
	@Bean
	public TrackCounter trackCounter() {
		return new TrackCounter();
	}
}
测试切面:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {

	@Rule
	public final StandardOutputStreamLog log = new StandardOutputStreamLog();
	
	@Autowired
	private CompactDisc cd;
	
	@Autowired
	private TrackCounter counter;
	
	@Test
	public void testTrackCounter() {
		cd.playTrack(1);
		cd.playTrack(2);
		cd.playTrack(3);
		cd.playTrack(3);
		cd.playTrack(3);
		cd.playTrack(3);
		cd.playTrack(7);
		cd.playTrack(7);
		
		assertEquals(1, counter.getPlayCount(1));
		assertEquals(1, counter.getPlayCount(2));
		assertEquals(4, counter.getPlayCount(3));
		assertEquals(0, counter.getPlayCount(4));
		
		assertEquals(0, counter.getPlayCount(5));
		assertEquals(0, counter.getPlayCount(6));
		assertEquals(2, counter.getPlayCount(7));
	}
}


(5)、通过注解引入新功能

       通过切面还可以为被通知的类引入新的功能,利用AOP概念“引入”,切面可以为Spring bean添加新的方法。

【】Spring AOP 为Spring bean引入新方法的原理

       在Spring中,切面只是实现了它们所包装bean相同接口的代理,虽然不能直接给bean增加方法,但是可以为代理类增加新的方法,这样代理暴露的新接口,就可以称为新引入的功能:

【】该引入功能有什么用?

比如,我们现在有一个需求,想往接口A的实现类中,引入一个新的接口B;最简单的方法就是直接访问接口A的所有实现,并对其进行修改,但是从设计的角度来看,这并不是最好的做法,并不是所有的实现了接口A的类,都具有接口B的特性,另外,如果接口A的实现是第三方的(没有源码),此时就不能修改所有的A接口的实现;

      此时借助AOP的引入功能,我们可以在不妥协设计、不入侵源码的基础上,通过创建切面为接口A的实现增加接口B的功能。

【】怎样使用引入功能?

@DeclareParents注解由两部分组成:

*** value属性指定了哪种类型的bean要引入该接口;

*** defaultImpl属性指定了为引入功能提供实现的类。

例如,我们可以创建这样的切面:

@Aspect
public class EncoreableIntroducer {

	@DeclareParents(value="com.xiaoxiaoyusheng.spring_aop.Performance+",
			defaultImpl=DefaultEncoreable.class)
	public static Encoreable encoreable;
}
其中:Performance的所有实现类,就是我们想要引入Encoreable接口的地方;其中的‘+’号表示所有实现Performance的类型。

           这里的DefaultEncoreable就是为引入功能提供实现的类。

           使用@DeclareParents注解所标注的静态属性指明了要引入的接口。

在Spring应用中需要将上述切面声明为bean,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现。

注:面向注解的切面声明有一个明显的劣势:需要能够为通知类添加注解(必须能访问通知类的源码)。另一种可选方案是在Spring XML配置文件中声明切面。


三、使用XML声明切面
      

优先选择基于注解的切面,如果声明切面时不能为通知类添加注解的时候,那么才转向XML配置

在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面:

【】<aop:advisor> : 定义AOP通知器

【】<aop:after> : 定义AOP后置通知(不管被通知的方法是否执行成功)

【】<aop:after-returning> : 定义AOP返回通知

【】<aop:after-throwing>: 定义AOP异常通知

【】<aop:around>: 定义AOP环绕通知

【】<aop:aspect>: 定义一个切面

【】<aop:aspectj-autoproxy> : 启用@AspectJ注解驱动的切面

【】<aop:before> : 定义一个AOP前置通知

【】<aop:config>: 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内

【】<aop:declare-parents>:以透明的方式为被通知的对象引入额外的接口

【】<aop:pointcut> : 定义一个切点


(1)、声明前置和后置通知

假设我们现在将Audience的切面注解全部去掉了,可以使用如下xml配置声明切面:

<aop:config>
	<aop:aspect ref="audience">
		<aop:before 
			pointcut="execution(** concert.Performance.perform(..))"
			method="silenceCellPhones"/>
		<aop:before
			pointcut="execution(** concert.Performance.perform(..))"
			method="takeSeats"/>
		<aop:after-returning
			pointcut="execution(** concert.Performance.perform(..))"
			method="applause"/>
		<aop:after-throwing
			pointcut="execution(** concert.Performance.perform(..))"
			method="demandRefund"/>
		
	</aop:aspect>
</aop:config>

对于上述示例中重复的pointcut属性声明,在XML中同样可以消除重复,使用的是<aop:pointcut>

<aop:config>
	<aop:aspect ref="audience">
		<aop:pointcut
			id="performance"
			expression="execution(** concert.Performance.perform(..))"/>
		<aop:before 
			pointcut-ref="performance"
			method="silenceCellPhones"/>
		<aop:before
			pointcut-ref="performance"
			method="takeSeats"/>
		<aop:after-returning
			pointcut-ref="performance"
			method="applause"/>
		<aop:after-throwing
			pointcut-ref="performance"
			method="demandRefund"/>
		
	</aop:aspect>
</aop:config>
注:如果想让定义的切点能够在多个切面使用,可以把<aop:pointcut>元素放在<aop:config>元素的范围内。


(2)、声明环绕通知

在XML中声明环绕通知与声明其他类型的通知并没有太大区别,示例:

<aop:config>
	<aop:aspect ref="audience">
		<aop:pointcut
			id="performance"
			expression="execution(** concert.Performance.perform(..))"/>
		
		<aop:around
			pointcut-ref="performance"
			method="watchPerformance"/>
	</aop:aspect>
</aop:config>
注意:环绕通知方法watchPerformance同样也是需要ProceedingJoinPoint 参数的。


(3)、为通知传递参数

以上面的TrackCounter为例:

<bean id="trackCounter"
	class="soundsystem.TrackCounter" />
<bean id="cd"
	class="soundsystem.BlankDisc" >
	<property name="title" value="Sgt. Perpper's Lonely Hearts Club Band" />
	<property name="artist" value="The Beatles" />
	<property name="tracks">
		<list>
			<value>A</value>
			<value>B</value>
			<value>C</value>
		</list>
	</property>
</bean>

<aop:config>
	<aop:aspect ref="trackCounter">
		<aop:pointcut id="trackPlayed" expression="execution(* soundsystem.CompactDisc.playTrack(int))
			and args(trackNumber)" />
			
		<aop:before pointcut-ref="trackPlayed"
			method="countTrack" />
	</aop:aspect>
</aop:config>
注意:expression中使用and关键字而不是“&&”。


(4)、通过切面引入新功能

例如:

<aop:aspect>
	<aop:declare-parents
	types-matching="concerts.Performance+"
	implement-interface="concert.Encoreable"
	default-impl="concert.DefaultEncoreable" />
</aop:aspect>
注意:这里的default-impl属性用全限定类名来显式指定Encoreable的实现,当然也可以替换为delegate-ref属性,该属性引用了一个Spring bean作为引入的委托。

四、注入AspectJ切面

   

      Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP是一个功能比较弱的AOP解决方案。

      AspectJ切面与Spring是相互独立的,它们可以织入到任意的Java应用中。

      通常一个精心设计的切面依赖于一个或多个类,可以在切面内部实例化这些协作对象,更好的方式是借助Spring的依赖注入把bean装配到AspectJ切面中。

例子:假设CriticAspect类是一个切面,它依赖一个接口CriticismEngine,CriticismEngineImpl实现了CriticismEngine接口。我们需要将CriticismEngineImpl注入到切面CriticAspect中。

首先,需要将CriticismEngineImpl声明为一个Spring bean。如下:

<bean id="criticismEngin"
	class="com.springinaction.springidol.CriticismEngineImpl">
	...
</bean>
然后,使用Spring的依赖注入为AspectJ切面注入协作者,(当然,AspectJ切面根本不需要Spring就可以织入应用中),将切面声明为一个Sprign配置中的bean。如下:

<bean class="com.springincation.springidol.CriticAspect"
	factory-method="aspectOf">
	<property name="criticismEngin" ref="criticismEngin" />
</bean>

特别注意的地方是: 切面bean的配置与普通的Spring bean配置并不相同,最大的不同在于切面bean的配置中使用了factory-method属性,而普通bean则没有,因此,在获取切面实例时会调用factory-mthod指定的方法而不是构造方法。普通bean在实例化时是调用构造方法的。

       这样配置的原因: Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。等到Spring有机会为切面注入依赖的对象时,切面已经被实例化了(因此不能再调用构造方法),又因为所有的AspectJ切面都提供了一个静态的aspectOf()方法,该方法返回切面的一个单例。因此Spring可以在实例化bean时,通过该方法获取切面的实例。具体做法就是使用factory-method来调用aspectOf()方法。(因此,需要在配置中指定factory-mthod属性,设置其值为“aspectOf”)



     

猜你喜欢

转载自blog.csdn.net/xiaoxiaoyusheng2012/article/details/78692855