【Spring实战——面向切面的Spring】1.5使用注解创建切面
使用注解来创建切面是AspectJ 5所引入的关键特性。AspectJ 5之前, 编写AspectJ切面需要学习一种Java语言的扩展,但是AspectJ面向注解 的模型可以非常简便地通过少量注解把任意类转变为切面。
我们已经定义了Performance接口,它是切面中切点的目标对象。 现在,让我们使用AspecJ注解来定义切面。
1.5.1定义切面
流程图:
如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的 角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不 是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。
程序清单4. 1展现了Audience类,它定义了我们所需的一个切面。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class Audience {
@Before("execution(**concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
@Before("execution(**concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
@AfterReturning("execution(**concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("execution(**concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("CLAP CLAP CLAP!!!");
}
}
程序清单4.1 Audience类:观看演出的切面
1. @AspectJ注解的作用是什么?
Audience类使用@AspectJ注解进行了标注。
该注解表明Audience不仅仅是一个POJO,还是一个切面。
Audience类中的方法都使用注解来定义切面的具体行为。
可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。AspectJ提供了五个注解来定义通知,如表4.2所示。
表4.2 Spring使用AspectJ注解来声明通知方法
Audience使用到了前面五个注解中的三个。
takeSeats ()和 silence CellPhones ()方法都用到了@Before注解,表明它们应 该在演出开始之前调用。
applause ()方法使用了@AfterReturning注解,它会在演出成功返回后调用。
demandRefund()方法上添加了@AfterThrowing注解,这表 明它会在抛出异常以后执行。
2.我们这时候会发现,这些注解都有一个切点表达式作为值?
所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。反之,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。
3.切点表达式用了四次,存在一定的问题?
触发时间:在Performance的perform()方法执行时触发。
缺点:在任务执行前后,触发了四次,虽然能达到预期功能,但是给人怪怪的感觉。是不是可以定义一次呢,在每次需要的时候引用它,是一个很好的方案。
这时候,我们可以使用@PointCut注解,在@Aspect内定义可重用的切点。
package com.spring.cut;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class Audience {
/**
* 定义命名的切点
*/
@Pointcut("execution(**concert.Performance.perform(..))")
public void performace() {
}
/**
* 表演之前
*/
@Before("performace() ")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
@Before("performace() ")
public void takeSeats() {
System.out.println("Taking seats");
}
/**
* 表演之后
*/
@AfterReturning("performace() ")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
/**
* 表演失败之后
*/
@AfterThrowing("performace() ")
public void demandRefund() {
System.out.println("CLAP CLAP CLAP!!!");
}
}
程序清单4.2 通过**@Pointcut**注解声明频繁使用的切点表达式
4. 使用@Pointcut注解代替切面表达式,应该怎么做?
1.在Audience类中,我们使用了@Pointcut注解来定义performance()方法。
2.@Pointcut注解接受一个切点表达式作为参数,就像我们之前在通知注解中所使用的那样。
5.这样做的意义在哪里?
通过在performance()方法上添加@Pointcut注解,我们扩展了切点表达式语言的能力,这样我们可以在任何需要使用performance()的地方使用它,而不必再使用更长的切点表达式。我们将所有通知注解中的长表达式都替换成了performance()。
实际上,performance()方法的具体内容并不重要,在这里它是空的。该方法只是一个标识,供@Pointcut注解使用。
6.Audience的意义何在?
Audience只是一个通过注解表明将用作切面的Java类。
像其他的Java类一样,它可以装配为Spring中的bean:
@Bean
public Audience. audience(){
return new Audience();
}
7.Audience自动代理会怎么样?
如果你就此止步的话,Audience只会是Spring容器中的一个bean。 即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。
8.如何启动自动代理功能?
如果你使用JavaConfig的话,可以在配置类的类级别上通过使用EnableAspectJ-AutoProxy注解启用自动代理功能。
程序清单 4.3展现了如何在JavaConfig中启用自动代理。
@Configuration
@EnableAspectJAutoproxy//启用AspectJ自动代理
@ComponentScan
public class ConcertConfig{
@Bean//声明Audience bean
public Audience audience(){
return new Audience();
}
}
程序清单4.3 在JavaConfig中启用AspectJ注解的自动代理
假如你在Spring中要使用XML来装配bean的话,那么需要使用Spring aop命名空间中的aop:aspectj-autoproxy元素。下面的XML 配置展现了如何完成该功能。
程序清单4.4 在XML中,通过Spring的aop命名空间启用AspectJ自 动代理
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/c"
xmlns:aop="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="concert"/>
<aop:aspectj-autoproxy/>
<!--声明Audience bean-->
<bean class="com.spring.cut.Audience"/>
</beans>
不管你是使用JavaConfig还是XML,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面 的切点所匹配的bean。在这种情况下,将会为Concertbean创建一个 代理,Audience类中的通知方法将会在perform()调用前后执行。
9.Spring的Aspect自动代理特性?
Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。
在本质上,它依然是 Spring基于代理的切面。换句话说虽然使用的 是@AspectJ注解,仍然限于代理方法的调用。
10.如何充分利用Aspect的能力?
如果想利用 AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring 来创建基于代理的切面。
到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前 置通知和后置通知。但是表4.2还提到了另外的一种通知:环绕通知 (around advice ) 。环绕通知与其他类型的通知有所不同,因此值得 花点时间来介绍如何进行编写。
1.5.2创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知Aui的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
为了阐述环绕通知,我们重写Audience切面。这次,我们使用一个 环绕通知来代替之前多个不同的前置通知和后置通知。
程序清单4.5 使用环绕通知重新实现Audience切面
package com.spring.cut;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Audience {
/**
* 定义命名的切点
*/
@Pointcut("execution(**concert.Performance.perform(..))")
public void performace(){}
/**
* 环绕通知方法
*/
@Around("performace()")
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");
}
}
}
1.@ around注解在上述方法的意义何在?代表了什么?
在这里,@Around注解表明watchPerformance ()方法会作为performance ()切点的环绕通知。
2.在这程序中,执行流程是怎么样的?
在这个通知中,观众在演出之 前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样, 如果演出失败的话,观众会要求退款。
3.通知的执行效果与前置通知和后置通知效果怎么样?
这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。
4.关于这个新的通知方法,参数是怎么样的?
这个通知方法,它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。
5.通知方法中控制权转移?
通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。
6.为什么需要调用proceed()方法?
需要注意的是,别忘记调用proceed()方法。如果不调这个方法的 话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方 法。
7.如果不调用proceed方法会怎么样?
有意思的是,你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
1.5.3 处理通知中的参数
1.我们切面如今的参数?
到目前为止,我们的切面都很简单,没有任何参数。
唯一的例外是我们为环绕通知所编写的watchPerformance ()示例方法中使用了 ProceedingJoinPoint作为参数。
除了环绕通知,我们编写的其他通知不需要关注传递给被通知方法的任意参数。这很正常,因为我们所通知的perform()方法本身没有任何参数。
2.如果切面所通知的方法确实有参数该怎么办呢?切面能访问和使用传递给被通知方法的参数吗?
假设你想记录每个磁道被播放的次数。一种方法就是修改playTrack()方法,直接在每次调用的时候记录这个数量。但是,记录磁道的播放次数与播放本身是不同的关注点,因此不应该属 于playTrack()方法。看起来,这应该是切面要完成的任务。
为了记录每个磁道所播放的次数,我们创建了TrackCounter类, 它是通知playTrack()方法的一个切面。下面的程序清单展示了这 个切面。
程序清单4.6 使用参数化的通知来记录磁道播放的次数
package com.spring.cut;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
/**
* @author huyang
*/
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
/**
* 通知play-Track()方法,切面使用@Pointcut注解定义命名 的切点
*
* @param trackNumbers
*/
@Pointcut("execution(* com.spring.cut(int))" + "&& args(trackNumbers)")
public void trackPlayed(int trackNumbers) {
}
/**
* 在播放前,为该磁道计数
*用@Before将一个方法声明为前置通知
*
* @param trackNumbers
*/
@Before("trackPlayed(trackNumbers)")
public void countTrack(int trackNumbers) {
int currentCount = getPlayCount(trackNumbers);
trackCounts.put(trackNumbers, currentCount + 1);
}
public int getPlayCount(int trackNumbers) {
return trackCounts.containsKey(trackNumbers) ? trackCounts.get(trackCounts) : 0;
}
}
图4.6将切点 表达式进行了分解,以展现参数是在什么地方指定的。
图4 .6 在切点表达式中声明参数,这个参数传入到通知方法中
切点表达式中的args (trackNumber)限定 符。它表明传递给playTrack()方法的int类型参数也会传递到通 知中去。
参数的名称trackNumber也与切点方法签名中的参数相匹配。
这个参数会传递到通知方法中,这个通知方法是通过@Before注解 和命名切点trackPlayed(trackNumber)定义的。
切点定义中的 参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。
现在,我们可以在Spring配置中将BlankDisc和TrackCounter定 义为bean,并启用AspectJ自动代理,如程序清单4.7所示。
程序清单4.7 配置TrackCount记录每个磁道播放的次数
package com.spring.cut;
import com.spring.CompactDisc;
import com.spring.Constant.BlankDisc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.util.ArrayList;
import java.util.List;
/**
* @author huyang
*/
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers() {
BlankDisc cd = new BlankDisc();
cd.setTitle("Sgt");
cd.setArtist("The Beatles");
List<String> tracks = new ArrayList<String>();
tracks.add("Sgt");
tracks.add("With a Little Help from my Friends");
tracks.add("Getting Better");
tracks.add("Fixing a Hole");
cd.setTracks(tracks);
return cd;
}
public TrackCounter trackCounter() {
return new TrackCounter();
}
}
最后,为了证明它能正常工作,你可以编写如下的简单测试。它会播 放几个磁道并通过TrackCounter断言播放的数量。
程序清单4.8 测试TrackCounter切面
package com.spring.cut;
import com.spring.CompactDisc;
import org.aspectj.lang.annotation.AfterThrowing;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static junit.framework.TestCase.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TrackCounterConfig.class)
public class TrackCounterConfigTest {
@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(7);
assertEquals(1,counter.getPlayCount(1));
assertEquals(1,counter.getPlayCount(1));
assertEquals(1,counter.getPlayCount(1));
assertEquals(1,counter.getPlayCount(1));
}
}
到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已 有方法。但是,方法包装仅仅是切面所能实现的功能之一。让我们看 一下如何通过编写切面,为被通知的对象引入全新的功能。
1.5.4 通过注解引入新功能
一些编程语言,例如Ruby和Groovy,有开放类的理念。它们可以不用 直接修改对象或类的定义就能够为对象或类增加新的方法。不过, Java并不是动态语言。一旦类编译完成了,我们就很难再为该类添加 新的功能了。
1.如何切面为现有的方法增加额外的功能?
当 然,我们还没有为对象增加任何新的方法,但是已经为对象拥有的方法添加了新功能。如果切面能够为现有的方法增加额外的功能,为什 么不能为一个对象增加新的方法呢?
2.如何使用AOP增加新方法?
实际上,利用被称为引入的AOP 概念,切面可以为Springbean添加新方法。
3.如果实现新接口,代理暴露新接口的话会怎么样?
回顾一下,在Spring中,切面只是实现了它们所包装bean相同接口的 代理。那样的话,切面所通知的bean看起来像是实现了新的接口,即便 底层实现类并没有实现这些接口也无所谓。
图4.7展示了它们是如何 工作的。
图4 .7 使用Spring AOP ,我们可以为bean引入新的方法。 代理拦截调用并委托给实现该方法的其他对象
我们需要注意的是,当引入接口的方法被调用时,代理会把此调用委 托给实现了新接口的某个其他对象。实际上,一个bean的实现被拆分到了多个类中。
为了验证该主意能行得通,我们为示例中的所有的Performance实 现引入下面的Encoreable接口:
package com.spring.cut;
/**
* @author huyang
*/
public interface Encoreable {
void performEncore();
}
4.我们需要有 一种方式将这个接口应用到Performance实现中?
我们现在假设你 能够访问Performance的所有实现,并对其进行修改,让它们都实 现Encoreable接口。
5.从设计的角度来看,有什么问题?
从设计的角度来看,这并不是最好的做 法,并不是所有的Performance都是具有Encoreable特性的。另外一方面,有可能无法修改所有的Performance实现,当使用第三 方实现并且没有源码的时候更是如此。
6.我们这时候引入AOP功能,会怎么样?
值得庆幸的是,借助于AOP的引入功能,我们可以不必在设计上妥协 或者侵入性地改变现有的实现。为了实现该功能,我们要创建一个新 的切面:
- 引入AOP功能没有不太清楚,希望再研究一下。
package com.spring.cut;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
/**
* @author huyang
*/
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value = "com.spring.cut",defaultImpl=DEfaultEncoreable.class)
public static Encoreable encoreable;
}
可以看到,EncoreableIntroducer是一个切面。但是,它与我们 之前所创建的切面不同,它并没有提供前置、后置或环绕通知,而是 通过@DeclareParents注解,将Encoreable接口引入到Performance bean中。
@DeclareParents注解由三部分组成:
value属性指定了哪种类型的bean要引入该接口。
在本例中,也就是所有实现Performance的类型。 (标记符后面的加号表示 是Performance的所有子类型,而不是Performance本身。)
defaultImpl属性指定了为引入功能提供实现的类。
在这里,我们指定的是DefaultEncoreable提供实现。
@DeclareParents注解所标注的静态属性指明了要引入了接口。
在这里,我们所引入的是Encoreable接口。
和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:
<bean class="concert.EncoreableIntroducer"/>
Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使 用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给 被代理的bean或被引入的实现,这取决于调用的方法属于被代理的 bean还是属于被引入的接口。
在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。 它非常简单,并且只涉及到最少的Spring配置。但是,面向注解的切 面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到 这一点,必须要有源码。
共同学习,写下你的评论
评论加载中...
作者其他优质文章