一、面向切面的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”)