五、面向切面的spring(1)
spring的依赖注入看完了,接下来是spring中与DI一并重要的AOP了,开始吧,GO。
在软件开发中,散布于应用中多处的功能被称为横切发关注点,通常来讲,这些横切关注点从概念上市与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑中)。这些横切关注点与业务逻辑想分离正是面向切面编程所要解决的问题。
一、什么是面向切面编程
如果要重用通用功能的话,最常用的面向对象技术是继承(inheritance)和委托(delegation)。但是,如果咋整个应用中都使用相同基类,继承往往会导致一个脆弱的对象体系;而委托可能需要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程的时候,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,二无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中,其次,服务模块更简洁因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
1、AOP中的术语
(1)通知(Advice) 通知定义了切面是什么以及何时使用。
类比于:抄表员登记用电量并回去向电力公司报告,抄表员的主要工作是记录电量
spring切面可以应用5种类型的通知:
1)前置通知(Before):在目标方法被调用钱调用通知功能
2)后置通知(After):在目标方法完成之后调用通知,此时不关心方法的输出是什么
3)返回通知(After-returning):在目标方法成功执行之后调用通知
4)异常通知(After-throwing):在目标方法抛出异常之后调用通知
5)环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为
(2)连接点(Join point)
类比于:抄表员也许能够读取各种类型的设备,但是为了完成工作 ,他的目标应该是房屋内所安装的电表
我们的应用可能也有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点
(3)切点(Pointcut) 定义了切面的“何处”,在什么地方使用
类比于:电力公司为每一个抄表员都分别指定了某一块区域而定住户
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
(4)切面(Aspect)
类比于:抄表员知道自己要做的事情(报告电表量)和从哪些住户收集信息。因此,他能玩成工作了
切面是通知和切点的结合。通知和切点定义了切面的全部内容分--它是什么,在何时何处完成去功能
(5)引入(Introduction)
引入允许我们向现有的类添加新方法和属性
(6)织入(Wearing)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
1)编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的
2)类加载期:切面在目标加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码,AspectJ5的加载时织入(load-time wearing,LTW)就支持这种方式织入切面的
3)运行期:切面在应用运行的某一个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。spring AOP 就是以这种方式织入切面的
2、spring对AOP的支持 创建切点来定义切面所织入的连接点是AOP框架的基本功能
spring提供了四种类型的AOP支持:
1)基于代理的经典的spring AOP
2)纯POJO切面
3)@AspectJ 注解驱动的切面
4)注入式AspectJ 切面(适用于spring 的各种版本)
前三种都是spring AOP实现的变体,spring AOP构建在动态代理基础之上,因此,spring对AOP 的支持局限于方法拦截
spring AOP 框架的一些关键知识:
1)spring所创建的通知都是用标准的Java类编写的。定义通知所应用的切点通常会使用注解或在spring配置文件里采用XML来编写
2)通过在代理类中包裹切面,spring在运行时期把切面织入到spring管理的bean中,直到应用需要被代理的bean时,spring才会创建代理对象
3)因为spring基于动态代理,所以spring只支持方法级别的连接点
二、通过切点来选择连接点
spring借助AspectJ 的切点表达式语言来定义spring切面
AspectJ指示器 | 描述 |
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点 匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定的类型的bean |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配待定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解标注的类型(当使用spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注解的连接点 |
在spring尝试使用AspectJ 其他指示器的时候,将会跑出IllgalArgumentException异常。execution指示器是我们编写切点定义时最主要的指示器
1、编写切点
spring切面的讲解,需要有个主题来定义切面的切点。定义了一个Performance接口
package concert; public interface Performance { public void perform(); }
execution(* concert.Performance.perform(...))
注:表达式从* 号开始,表明了我们不关心方法的返回值类型,然后我们指定了全限定类名和方法名。对于参数列表,我们使用(..)表明切点要选择任意的perform()方法,无论给方法的入参是什么。
execution(* concert.Performance.perform(...)) && within(concert.*)
注:&&是execution()指示器和within()指示器连接在一起形成与(and)关系,within()指示器来限定匹配,当concert包下任意类的方法被调用的时候
2、在切点中选择bean
bean()指示器,使用bean ID 和bean名称作为参数来限制切点只匹配特定的bean
execution(* concert.Performance.perform(...)) and bean('woodstock')
注:限定的bean的ID 为woodstock,切面的通知会编织到ID为woodstock的bean中
execution(* concert.Performance.perform(...)) !and bean('woodstock')
注:切面的通知会编织到ID不为woodstock的bean中
三、使用注解创建切面
1、定义切面
//Audience类;观看演出的切面 package concert; import org.aspectj.lang.annotation.AtferReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; //@Aspect注解表明该类不仅是一个POJO,还是一个切面 @Aspect public class Audience { //表演之前 @Befor("execution(** concert.Performance.perform(..))") public void sienceCellPhones(){ System.out.println("Silencing cell phone"); } //表演之前 @Befor("execution(** concert.Performance.perform(..))") public void takeSeats(){ System.out.println("Taking setas"); } //表演之后 @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("Demanding a refund"); } }
spring中使用AspectJ注解来声明通知方法
1)@After:通知方法会在目标方法返回或抛出异常后调用
2)@AfterBeturning:通知方法会在目标方法返回后调用
3)@AfterThrowing:通知方法会在目标方法抛出异常后调用
4)@Around:通知方法将目标方法封装起来
5)@Before:通知方法会在目标方法调用之前执行
//Audience类;观看演出的切面 package concert; import org.aspectj.lang.annotation.AtferReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; //@Aspect注解表明该类不仅是一个POJO,还是一个切面 @Aspect public class Audience { //定义命名的切点 //该方法本身只是一个标识,供@Pointcut注解依附 @Pointcut("execution(** concert.Performance.perform(..))") public void performance(){} //表演之前 @Befor("performance()") public void sienceCellPhones(){ System.out.println("Silencing cell phone"); } //表演之前 @Befor("performance()") public void takeSeats(){ System.out.println("Taking setas"); } //表演之后 @AfterReturning("performance()") public void applause(){ System.out.println("CLAP CLAP CLAP !!!"); } //表演失败之后 @AfterThrowing("performance()") public void demandRefund(){ System.out.println("Demanding a refund"); } }
这样的话只是编写了切面,Audience只是spring容器中的一个bean,即使使用了AspectJ 注解,并不会视为切面,这些注解不会解析,也不会创建将其转化为切面的代理,接下来:
package concert; import org.springframework,context.annotation.Bean; import org.springframework,context.annotation.ComponentScan; import org.springframework,context.annotation.Configuration; import org.springframework,context.annotation.EnableAspectJAutoProxy; @Configuration //启动AspectJ自动代理 @EnableAspectJAutoProxy @ComponentScan public class ConectConfig() { @Bean public Audience audience(){ return new Audience(); } } //XML中配置 <context:component-scan base-package="concert" /> //启用AspectJ自动代理 <aop:aspectj-autoproxy> //声明Audience bean <bean class="concert.Audience" />
注意:spring的AspectJ自动代理仅仅是使用@AspectJ作为创建切面的指导,切面依然是基于代理的。。。
2、创建环绕通知(环绕通知是最为强大的通知类型)
package concert; 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 performance() {} @Around public void watchPerformance(ProceedingJoinPoint jp){ try{ System.out.println("Silencing cell phones"); System.out.println("Taking seats"); jp.proceed(); }catch(Throwable e){ System.out.println("Demanding a refund"); } } }
3、处理通知中的参数
直接上代码吧,这个是BlackDisc类。
//使用参数化的通知来记录磁道播放的次数 package soundsystem; import java.util.HashMap; import java.util.Map; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; @Aspect public class TrackCounter { private Map<Integer,Integer> trackCounts = new HashMap<Integer,Integer>(); @Pointcut(execution(* soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)) public void trackPlayed(int trackNumber) {} @Before("trackPlayed(trackNumber)") public void countTrack(int trackNumber){ int currentCount = getPlayCount(trackNumber); trackCounts.put(trackNumber, currentCount +1); } public int getPlayCount(int trackNumber){ return trackCounts.countainsKey(trackNumber) ? trackCounts.get(trackNumber) : 0; } }
execution(* soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)
在切点表达式中声明参数,这个参数传入到通知方法中。需要关注的是args(trackNumber)限定符,它表明传递给palyTrack()方法的int类型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数相匹配,这个参数会传递到通知方法中,这个通知方法是通过@Befor注解和命名切点trackPlayed(trackNumber)定义的。切点定义中的参数与切点方法中的参数名称是一样的。这样就完成了从命名切点到通知方法的参数转移。
package soundsystem; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration //启用AspectJ自动代理 @EnableAspectJAutoProxy public class TrackCountConfig { @Bean public CompactDisc sgtPeppers(){ BlankDisc cd = new BlankDisc(); cd.setTitle("蓝莲花"); cd.setArtist("许巍"); List<String> tracks = new ArrayList<String>(); tracks.add("那一年"); tracks.add("空谷幽兰"); tracks.add("爱情"); tracks.add("此时此刻"); cd.setTrack(tracks); return cd; } @Bean public TrackCounter trackCounter(){ return new TrackCounter(); } } //这里写一个测试的类 package soundsystem; import static org.junit.Assert.*; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.StandardOutputStreamLog; 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; @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.palyTrack(1); cd.palyTrack(2); cd.palyTrack(3); cd.palyTrack(3); cd.palyTrack(3); cd.palyTrack(3); cd.palyTrack(7); cd.palyTrack(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)); } }
4、通过注解引入新功能
利用被称为引入的AOP概念,切面可以为spring bean添加新方法
package concert; public interface Encoreable { void performEncore(); } package concert; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclareParents; @Aspect public class EncoreableIntroducer { @DeclareParents(value="concert.Performance" defaultImpl=DefaultEncoreable.class) public static Encoreable encoreable; }
spring的自动代理机制将会获取到它的声明,当spring发现一个bean使用了@Aspect注解时,spring就会创建一个代理,然后将调用委托给代理bean或被引入的实现,这取决于调用方法属于被代理的bean还是属于被引入的接口。