面向切面的Spring
什么是面向切面编程
面向切面编程,就是可以在多个不相关的业务功能(方法)中添加相同的功能(切面)。可以使这些功能和业务功能解耦,可以让我们把更多的注意力放在业务代码中。
切面例如有:事务管理、安全、日志等。
横切关注点:就是可以影响程序中多个点的功能。
AOP相关的术语
在这里,我们来了解一下AOP的相关术语。
通知(Advise)
通知,定义了切面是什么和什么时候执行,就是定义了要加入的功能和什么时候执行这个功能。
根据什么时候执行,通知分为一下5类:
- 前置通知(before):在方法执行前执行通知。
- 后置通知(after):在方法执行完后执行通知,不关注方法的返回值。
- 返回通知(after-returning):在方法执行成功后执行通知
- 异常通知(after-throwing):当方法有异常发生时执行通知。
- 环绕通知(around):在方法执行前和执行后执行通知
连接点(join point)
连接点,就是可以执行通知的点(地方)
切点(pointcut)
切点:是连接点的子集,是加了通知的连接点。
切面(Aspect)
切面:是通知和切点的组合,两者定义了切面是什么(是什么功能)和什么时候使用,切面应用的地方。
引入(instruction)
引入允许我们向现有类添加属性和方法
织入(weaving)
织入是把切面应用到目标对象中创建代理对象的过程。织入可以发生在目标对象的生命周期的多个点上:
- 编译期:在目标类编译时把切面织入。这种方式需要特殊的编译器,AspectJ的织入编译器就是使用这种方式织入切面。
- 类加载期:在目标类加载到JVM时将切面织入,这种方式需要特殊的类加载器,在目标类引入到应用之前增强目标类的字节码,AspectJ 5的加载期织入就支持这种织入切面的方式。
- 运行期:在应用执行的某个时刻织入切面。在织入切面时,AOP容器会动态地为目标对象生成代理对象。Spring AOP就是使用这种方式织入切面。
Spring对AOP的支持
Spring提供了4种对AOP的支持:
- Spring经典的基于代理的AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入AspectJ切面(所有的Spring版本都可以使用)
前三种都是Spring AOP实现的变体。
Spring经典的基于代理的AOP过于笨重,这里就不详细展开。
Spring的通知是用java写的
我们可以使用Java定义通知,而使用AspectJ,我们要学习一些工具和语法。
Spring在运行期通知对象
Spring的AOP是在运行期把通知应用到目标对象中,创建目标对象的代理对象。当拦截到方法调用时,在目标bean的方法执行之前,会先执行切面的逻辑。
Spring只支持方法级别的连接点
连接点有不同的粒度,例如方法级别、字段级别和构造器级别。
而Spring只支持方法级别的连接点,就是只能拦截方法的调用。连接点不支持字段级别和构造方法级别,不能在字段值改变和bean创建时应用通知。
AspectJ和JBoss支持字段和构造器的级别。
方法级别的连接点已经能够满足绝大部分的情况,如果需要字段级别和构造器级别的连接点,可以使用AspectJ作为Spring的补充。
通过切点来选择连接点
切点定义了切面的通知应用在什么地方,所以定义切点是一个重要的事。
在Spring AOP中,切点使用AspectJ的切点表达式语言。Spring支持的AspectJ切点指示器如下:
AspectJ指示器 | 描述 |
---|---|
execution() | 用于匹配执行方法的连接点 |
args() | 限制连接点匹配参数是给定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用是给定的类型 |
target() | 限制连接点匹配目标对象是给定的类型 |
@target() | 限制连接点匹配可执行对象,这个对象的类有给定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配有指定注解的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配具有指定注解的连接点 |
如果在Spring中使用其他的AspectJ指示器,那么会报IllegalArgumentException。
从表中看到,只有execution()指示器是用于执行匹配,其他的指示器都是限制匹配。所以,在定义切点时,execution()指示器是唯一的指示器,其他的指示器用于限制匹配的连接点。
编写切点
为了编写切点,我先定义一个接口Performance。
package concert;
public interface Performance {
public void perform();
}
定义切点,当调用Performance的perform()方法时,织入通知。
execution(* concert.Performance.perform(..))
- *: 表示任意的返回值
- concert.Performance:全限定类名
- perform:方法名
- …:括号的…代表任意参数,表示任意的参数perform都是切点
再定义一个切点,除了上面切点的要求外,再加一个条件,就是限定在concert包中,这里,就要使用within指示器了
execution(* concert.Performance.perform(..)) && within(concert.*)
- &&:表示与(and)操作符
除了&&,还有或(or)和非(not)操作符,||和!。
注意:在xml中,&有特殊意义,所以在xml中使用and代替&&,使用or和not代替||和!。
在切点中选择bean
除了上面表中列出的指示器,Spring还支持bean指示器,可以限定切点只作用在特定的bean中,bean指示器的参数是bean的id或bean的名称。
语法:
- bean(bean id)
再定义一个切点,除了上一节中第一个的切点的条件外,再加一个,只作用在id为woodstock的bean。
execution(* concert.Performance.perform(..)) && bean("woodstock")
只有当bean id是Woodstock的bean调用了concert.Performance.perform方法时,才会织入通知。
除了限定一个bean,我们也可以使用非操作符!,限定切点是除了某个bean外的所有bean。
execution(* concert.Performance.perform(..)) && !bean("woodstock")
除了bean id是Woodstock的其他所有bean,调用了concert.Performance.perform方法时,才会织入通知。
创建注解切面
我们使用AspectJ注解来定义切面,在AspectJ 5之前,我们需要学习另外的知识,在AspectJ 5及之后,只要使用少量的注解,我们就可以把任何的类转成切面。
定义切面
我们可以将一个java pojo类转成切面,在类级别上使用AspectJ注解@Aspect,注解@Aspect表明了这个类是切面。
在表演中,如果没有观众,是不可以的,但是如果从表演的功能看,观众不是核心的,所以我们把观众作为通知织入到表演中。
@Aspect
public class Audience {
@Before("execution(* concert.Performance.perform(..))")
public void slienceCellphone() {
System.out.println("slience cellphone");
}
@Before("execution(* concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("take seats");
}
@AfterReturning("execution(* concert.Performance.perform(..))")
public void applause() {
System.out.println("applause");
}
@AfterThrowing("execution(* concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("demand refund");
}
}
在表演开始前,观众要设置手机静音和坐下,表演好,就鼓掌,表演不符合要求,就要求退款。
关于方法上的通知注解,说明如下:
注解 | 通知 |
---|---|
@Before | 在目标方法执行前执行通知 |
@After | 在目标方法返回或抛出异常时执行通知 |
@AfterReturning | 在目标方法返回后执行通知 |
@AfterThrowing | 在目标方法抛出异常后执行通知 |
@Around | 通知方法环绕着目标方法执行 |
上面代码中切点表达式作为通知注解的值。
上面的代码,我们有不满意的地方,就是切点都是一样的,但是每写一个增强,我们都要写一次那个长长的切点表达式。我们可以使用注解@Pointcut来使我们只需写一次切点表达式,对于要多次使用的切点表达式,我们应该使用@Pointcut来简化。@Pointcut是方法级别的。
语法:
@Pointcut("切点表达式")
上面的代码可以改成下面这样:
@Aspect
public class Audience {
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance() {}
@Before("performance()")
public void slienceCellphone() {
System.out.println("slience cellphone");
}
@Before("performance()")
public void takeSeats() {
System.out.println("take seats");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("applause");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("demand refund");
}
}
@Pointcut修饰的方法的方法体应该是空的,然后通知注解的值就是这个方法,我们就不需要写多次长长的切点表达式了。
Audience类就是一个普通的Java pojo类,我们可以把他交给spring管理,在Java config中配置。
@Configuration
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
这样,就把audience对象交给了Spring来管理,但是会产生一个问题,Spring不会把Audience作为切面处理,把会解析里面的注解。
为了解决这个问题,可以从2个方面解决:
- 基于Java config
- 基于xml
基于Java config
如果使用Java Config显示装配bean,那么在javaconfig类中加上注解@EnableAspectJAutoProxy
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
基于xml
如果是使用xml显示方式装配bean,那么要使用aop命名空间的标签<aop:aspectj-autoproxy />
<?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: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="concert" />
<bean class="concert.Audience" />
<aop:aspectj-autoproxy />
</beans>
实现环绕通知
环绕通知是最强大的通知类型,它可以在目标方法执行之前和之后执行一些逻辑,可以实现之前前置通知和后置通知的效果。
为了使用环绕通知,我们重写Audience切面。
@Aspect
public class Audience {
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance() {}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("silence cell phone");
System.out.println("take seats");
jp.proceed();
System.out.println("applause");
} catch(Throwable t) {
System.out.println("demand refund");
}
}
}
这样,我们使用一个方法,就实现了之前4个方法的效果。
我们注意到,在通知方法里有一个ProceedingJoinPoint参数,我们使用这个参数调用目标方法,一定要有这个参数,使用ProceedingJoinPoint的proceed() 方法调用目标方法。我们其实也可以不调用,proceed方法,那么,目标方法就会被阻塞,得不到调用。
有意思的是,ProceedingJoinPoint的proceed()方法,我们可以不调用,那么目标方法就会阻塞;我们也可以调用多次,这种情形的使用情景,例如有,当目标方法失败时,重复执行目标方法。
处理通知中的参数
在上面的通知方法中,只有环绕通知使用了一个ProceedingJoinPoint参数,其他的都没有使用参数,可是,其实在通知方法中,是可以使用传给目标方法的参数值的。
首先我们定义一个CompactDisc类
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
@Override
public void play() {
for(int i=0; i<tracks.size(); i++) {
playTrack(i);
}
}
public void playTrack(int trackNum) {
System.out.println("play " + trackNum + " track");
}
}
在这里,我们想记录每个磁道播放的次数。
首先我们可以在playTrack()方法中,操作播放的次数,但是,记录播放次数不是播放的关注点,所以,把记录磁道播放次数作为切面比价合适。
现在我们定义一个切面,TrackCounter。
@Aspect
public class TrackCounter {
Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>(); //记录磁道播放次数
@Pointcut("execution(* soundSystem.BlankDisc.playTrack(int))
&& args(trackNumber)")
public void trackPlayed(int trackNumber) {}
@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber) {
int oriCount = getCount(trackNumber);
trackCounts.put(trackNumber, oriCount + 1);
}
public int getCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
注意:切点表达式中的args参数名trackNumber要和切点方法的参数名一致。
图:C:\Users\Administrator\Desktop\resources\Spring\img\chapter4\处理通知中的参数.png
接着使用Java显示配置将BlankDisc和TrackCounter交给Spring管理。
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
@Bean
public BlankDisc sgtPeppers() {
String title = "field of hope";
String artist = "seed";
List<String> tracks = new ArrayList<String>();
tracks.add("field of hope");
tracks.add("my love");
tracks.add("love story");
//...
BlankDisc bd = new BlankDisc(title, artist, tracks);
}
@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}
测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {
@Autowired
priavte CompactDisc cd;
@Autowired
priavte TrackCounter trackCounter;
@Test
public void test() {
cd.playTrack(1);
cd.playTrack(2);
cd.playTrack(1);
cd.playTrack(5);
System.out.println(trackCounter.getCounter(1));
System.out.println(trackCounter.getCounter(2));
System.out.println(trackCounter.getCounter(3));
System.out.println(trackCounter.getCounter(4));
System.out.println(trackCounter.getCounter(5));
}
}
通过注解引入功能
在上面的切面中,都是为目标方法加上功能。其实我们可以为目标对象或类添加方法,思路是:
- 创建一个有抽象方法的接口;
- 把这个接口引入到目标类中;
- 把切面应用到这个抽象方法中。
通过上面的3个步骤,就可以为一个目标类添加新的方法。
demo:
- 接口
public interface Encoreable {
void performEncore();
}
- 实现类
public class DefaultEncoreable implements Encoreable {
public void performEncore() {
//...
}
}
- 切面
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
@DeclareParents由三部分组成:
- value:表明这个接口要引入到哪些bean中,其中,最后的一个加号+表示子类型,把这个接口引入到Performance的子类型中。
- defaultImpl:提供给引入的默认实现类。
- 被@DeclareParents注解的静态属性:是要被引入的接口。
猜测:Encoreable接口有一个实现类DefaultEncoreable。
最后使用Java config或xml来把EncoreableIntroducer放到Spring容器上下文中。
<bean class="concert.EncoreableIntroducer" />
在xml中声明切面
虽然基于注解的切面开发很方便,但是也有情况不能使用注解,例如如果没有源码的情况,这时,就需要使用xml的方式定义切面。
在xml中使用到的标签如下:
标签 | 作用 |
---|---|
<aop:config> | 最顶级的AOP元素,大部分的<aop:*>的标签都定义在里面 |
<aop:aspect> | 定义一个切面 |
<aop:pointcut> | 定义一个切点 |
<aop:before> | 定义一个前置通知 |
<aop:after> | 定义一个后置通知(无论被增强方法是否成功返回,都会执行) |
<aop:after-returning> | 返回通知 |
<aop:after-throwing> | 异常通知 |
<aop:around> | 环绕通知 |
<aop:advisor> | 定义一个增强器 |
<aop:aspectJ-autoProxy> | 启用@AspectJ的注解驱动 |
<aop:declare-parents> | 用透明的方式为被增强的方法引入额外的接口 |
首先使用上面的观众类作为增强类,不过去掉了注解。
public class Audience {
public void slienceCellphone() {
System.out.println("slience cellphone");
}
public void takeSeats() {
System.out.println("take seats");
}
public void applause() {
System.out.println("applause");
}
public void demandRefund() {
System.out.println("demand refund");
}
}
声明前置通知和后置通知
<bean id="audience" class="cencert.Audience" />
<aop:config>
<aop:aspect ref="audience">
<aop:before pointcut="execution(* cencert.Performer.preform(..))"
method="slienceCellphone" />
<aop:before pointcut="execution(* cencert.Performer.preform(..))"
method="takeSeats" />
<aop:after-returning pointcut="execution(* cencert.Performer.preform(..))"
method="applause" />
<aop:after-throwing pointcut="execution(* cencert.Performer.preform(..))"
method="demandRefund" />
</aop:aspect>
</aop:config>
上面定义好了通知,但是有个问题,每次都要写一长串的切点表达式,很麻烦,我们可以定义在切点标签中,如下
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performence" expression="execution(* cencert.Performer.preform(..))" />
<aop:before pointcut-ref="performence" method="slienceCellphone" />
<aop:before pointcut-ref="performence" method="takeSeats" />
<aop:after-returning pointcut-ref="performence" method="applause" />
<aop:after-throwing pointcut-ref="performence" method="demandRefund" />
</aop:aspect>
</aop:config>
上面的performence切点只能在这个切面中使用,如果想在多个切面中共用一个切面,把切点标签定义在切面标签的外面。
声明环绕通知
我们使用之前定义的增强类。
public class Audience {
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("silence cell phone");
System.out.println("take seats");
jp.proceed();
System.out.println("applause");
} catch(Throwable t) {
System.out.println("demand refund");
}
}
}
<bean id="audience" class="cencert.Audience" />
<aop:config>
<aop:pointcut id="perform" expression="execution(* concert.Performer.perform(..))" />
<aop:aspect ref="audience">
<aop:around pointcut-ref="perform" method="watchPerformance" />
</aop:aspect>
</aop:config>
为通知传递参数
我们想让通知方法也能获取到被通知方法的参数值。
这里使用之前的例子,我们要计算每个磁道播放的次数,所以,被通知的方法就是播放歌曲的方法,我们可以在这个播放歌曲的方法里面计算磁道播放的次数,但是正如上面所说的,计算磁道播放次数不应该在播放歌曲的业务逻辑中考虑。
使用之前定义的被通知类和通知类。
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
@Override
public void play() {
for(int i=0; i<tracks.size(); i++) {
playTrack(i);
}
}
public void playTrack(int trackNum) {
System.out.println("play " + trackNum + " track");
}
}
public class TrackCounter {
Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>(); //记录磁道播放次数
public void countTrack(int trackNumber) {
int oriCount = getCount(trackNumber);
trackCounts.put(trackNumber, oriCount + 1);
}
public int getCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
使用trackCounts记录每个磁道播放的次数。
接着,我们在xml中定义切面。
<bean id="trackCounter" class="soundsystem.TrackCounter" />
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut id="playedTrack" expression="execution(* soundsystem.CompactDisc.playTrack(int) and args(trackNum))" />
<aop:before pointcut-ref="playedTrack" method="countTrack" />
<aop:aspect>
</aop:config>
这里写的切点表达式和之前在基于注解的基本是一样的,除了使用and关键字代替了&&,因为在xml中,&代表一个实体的开始。
通过切面引入新的功能
这里使用上面定义的接口和实现类。
- 接口
public interface Encoreable {
void performEncore();
}
- 实现类
public class DefaultEncoreable implements Encoreable {
public void performEncore() {
//...
}
}
接着在xml中配置
<aop:aspect>
<aop:declareParent
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"
/>
</aop:aspect>
这个xml的作用是,将匹配types-matching属性值的类都在类继承级别中添加implement-interface属性值作为父接口。剩下的问题就是接口的方法的实现从哪里来,这里是从default-impl属性值得到接口的方法的实现。
default-impl这里的写法是写类的全限定类名,其实,还有另一种写法,如下
<aop:aspect>
<aop:declareParent
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
delegate-ref="EncoreableDelegate"
/>
</aop:aspect>
<bean id="EncoreableDelegate" class="concert.DefaultEncoreable" />
使用delegate-ref属性值是实现类的id。
使用default-impl和delegate-ref的区别只有,后者要把bean放入spring上下文中,这个bean就可以进行依赖注入、spring配置等操作。
注入AspectJ切面
使用Spring AOP定义切面,已经可以满足我们大部分的情况,但是,也有一些特殊情况,例如,我们需要在对象创建时添加增强,而Spring AOP不能实现这个功能,这时,我们就需要使用基于AspectJ的切面,相比Spring AOP,AspectJ更为强大。AspectJ的切面和Spring AOP是独立的,使用AspectJ的切面不需要依赖Spring。
现在,举个例子,我们需要一个评论员,在表演结束后作出评论。
package com.spirnginaction.springidol
public class CriticAspect {
pointcut performance() : execution(* perform(..))
after-returning() : performance() {
System.out.println(criticismEngine.getCriticsim());
}
private CriticsimEngine criticsimEngine;
public void setCriticsimEngine(CriticsimEngine criticsimEngine) {
this.criticsimEngine = criticsimEngine;
}
}
pointcut performance() : execution(* perform(…))
这个代码的含义是,
- pointcut:定义切点。
- performance():切点的名称。
- execution(* perform(…)):切点表达式,这里的意思是应用在所有的perform方法中。
after-returning()代码块的含义是:
- after-returning():通知类型,这是返回通知。
- performance():切面名。
- 方法体:增强。
这个切面,用到了CriticsimEngine对象,这里CriticsimEngine是一个接口,有一个getCriticsim方法。
package com.spirnginaction.springidol
public interface CriticsimEngine {
void getCriticsim();
}
package com.spirnginaction.springidol
public class CriticsimEngineImpl implements CriticsimEngine {
public String getCriticsim() {
int i = (int)Math.random() * criticsimPool.length;
return criticsimPool[i];
}
private String[] criticsimPool;
public void setCriticsimPool(String[] criticsimPool) {
this.criticsimPool = criticsimPool;
}
}
现在,切面还有一个未解决的问题,就是CriticsimEngine对象从哪里来,在创建切面对象时创建CriticsimEngine对象并且注入进切面对象,是没问题的。但是,还有更好的方法,就是使用Spring的依赖注入,可以降低耦合度。
所以,我们要把切面类和CriticsimEngineImpl配置在Spring容器中,但是有一点要注意,基于AspectJ的切面类,是在AspectJ运行时创建切面对象的。
<bean id="criticsimEngine" class="com.spirnginaction.springidol.CriticsimEngineImpl">
<property name="criticsimPool">
<list>
<value>bad</value>
<value>well</value>
<value>ok</value>
...
</list>
</property>
</bean>
<bean class="com.spirnginaction.springidol.CriticAspect" factory-method="aspectOf">
<property name="criticsimEngine" ref="criticsimEngine"></property>
</bean>
这个AspectJ的切面的bean标签和我们之前写的基本一样,唯一的不同是这里多了一个factory-method属性,AspectJ为每个AspectJ切面都定义了一个aspectOf()方法,这个方法返回切面类的单例对象,因为基于AspectJ的切面对象不由Spring创建,而是由AspectJ在运行期创建,当spring容器需要注入的时候,切面对象已经创建好了。