面向切面编程

来源:互联网 发布:组策略优化工具 编辑:程序博客网 时间:2024/05/16 08:06

AOP术语


通知(Advice)

切面也有目标——它必须要完成的工作。 在AOP术语中, 切面的工作被称为通知
通知定义了切面是什么以及何时使用。 除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。 它应该应用在某个方法被调用之前? 之后? 之前和之后都调用? 还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:
前置通知(
Before) : 在目标方法被调用之前调用通知功能;
后置通知(
After) : 在目标方法完成之后调用通知, 此时不会关心方法的输出是什么;
返回通知(
After-returning) : 在目标方法成功执行之后调用通知;
异常通知(
After-throwing) : 在目标方法抛出异常后调用通知;
环绕通知(
Around) : 通知包裹了被通知的方法, 在被通知的方法调用之前和调用之后执行自定义的行为
连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点。 这个点可以是调用方法时、 抛出异常时、 甚至修改一个字段时。 切面代码可以利用这些点插入到应用的正常流程之中, 并添加新的行为
切点(Poincut)

如果说通知定义了切面的什么何时的话, 那么切点就定义了何处。 切点的定义会匹配通知所要织入的一个或多个连接点。 我们通常使用明确的类和方法名称, 或是利用正则表达式定义所匹配的类和方法名称来指定这些切点 。有些AOP框架允许我们创建动态的切点, 可以根据运行时的决策(比如方法的参数值) 来决定是否应用通切面知。
切面(Aspect)
切面是通知和切点的结合。 通知和切点共同定义了切面的全部内容——它是什么, 在何时和何处完成其功能
引入(introduction)

引入允许向现有的类添加新方法或属性。 

例如,创建一个Auditable通知类, 该类记录了对象最后一次修改时的状态。 只需一个方法,setLastModified(Date), 和一个实例变量来保存这个状态。 然后, 这个新方法和实例变量就可以被引入到现有的类中, 从而可以在无需修改这些现有的类的情况下, 让它们具有新的行为和状态
织入(Weaving)

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

在目标对象的生命周期里有多个点可以进行织入:
编译期: 切面在目标类编译时被织入。 这种方式需要特殊的编译器。
AspectJ的织入编译器就是以这种方式织入切面的。
类加载期: 切面在目标类加载到
JVM时被织入。 这种方式需要特殊的类加载器(ClassLoader) , 它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-timeweavingLTW) 就支持以这种方式织入切面。
运行期: 切面在应用运行的某个时刻被织入。 一般情况下, 在织入切面时,
AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
通知包含了需要用于多个应用对象的横切行为; 

连接点是程序执行过程中能够应用通知的所有点; 

切点定义了通知被应用的具体位置(在哪些连接点)
Spring对aop的支持

Spring提供了4种类型的AOP支持:
基于代理的经典
Spring AOP
POJO切面;
@AspectJ注解驱动的切面;
注入式
AspectJ切面(适用于Spring各版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上, 因此,SpringAOP的支持局限于方法拦截
现在Spring提供了更简洁和干净的面向切面编程方式
借助Springaop命名空间, 我们可以将纯POJO转换为切面。 这些POJO只是提供了满足切点条件时所要调用的方法。这种技术需要XML配置, 但这的确是声明式地将对象转换为切面的简便方式
Spring借鉴了AspectJ的切面, 以提供注解驱动的AOP。 本质上, 它依然是Spring基于代理的AOP, 但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。 这种AOP风格的好处在于能够不使用XML来完成功能。
如果AOP需求超过了简单的方法调用(如构造器或属性拦截) ,那么需要考虑使用AspectJ来实现切面。 在这种情况下, 上文所示的第四种类型能够帮助你将值注入到AspectJ驱动的切面中。
spring通知是java编写的

spring在运行时通知对象


Spring的切面由包裹了目标对象的代理类实现。代理类处理方法的调用, 执行额外的切面逻辑, 并调用目标方法
直到应用需要被代理的bean时,Spring才创建代理对象。 如果使用的是ApplicationContext的话, 在ApplicationContextBeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。 因为Spring运行时才创建代理对象, 所以我们不需要特殊的编译器来织入Spring AOP的切面
spring只支持方法级别的连接点

因为Spring基于动态代理, 所以Spring只支持方法连接点。如果需要方法拦截之外的连接点拦截功能, 那么我们可以利用Aspect来补充Spring AOP的功能
通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。 通知和切点是切面的最基本元素
Spring仅支持AspectJ切点指示器(pointcut designator) 的一个子集
4.1 Spring借助AspectJ的切点表达式语言来定义Spring切面
AspectJ
示器
描 述arg() 限制连接点匹配参数为指定类型的执行方法@args() 限制连接点匹配参数由指定注解标注的执行方法execution() 用于匹配是连接点的执行方法this() 限制连接点匹配AOP代理的bean引用为指定类型的类target 限制连接点匹配目标对象为指定类型的类@target()限制连接点匹配特定的执行对象, 这些对象对应的类要具有指定类
型的注解
within() 限制连接点匹配指定的类型@within()限制连接点匹配指定注解所标注的类型(当使用Spring AOP时, 方
法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点Spring中尝试使用AspectJ其他指示器时, 将会抛出IllegalArgument-Exception异常
只有execution指示器是实际执行匹配的, 而其他的指示器都是用来限制匹配的
编写切点

public interface Performance {public void perform();}
编写Performanceperform()方法触发的通知


使用execution()指示器选择Performanceperform()方法。 方法表达式以*号开始, 表明了我们不关心方法返回值的类型。 然后, 我们指定了全限定类名和方法名。 对于方法参数列表, 我们使用两个点号(..) 表明切点要选择任意的perform()方法, 无论该方法的入参是什么

需要配置的切点仅匹配concert包。 在此场景下, 可以使用within()指示器来限制匹配

使用了&&操作符把execution()within()指示器连接在一起形成与(and) 关系(切点必须匹配所有的指示器) 。 使用||操作符来标识或(or) 关系, 而使用!操作符来标识非(not) 操作。因为&XML中有特殊含义, 所以在SpringXML配置里面描述切点时,使用and来代替&&ornot可以分别用来代替||!
在切点中选择bean

bean()指示器, 它允许我们在切点表达式中使用beanID来标识beanbean()使用bean IDbean名称作为参数来限制切点只匹配特定的bean

在执行Performanceperform()方法时应用通知, 但限定beanIDwoodstock
可以使用非操作为除了特定ID以外的其他bean应用通知

使用注解创建切面

使用注解来创建切面是AspectJ 5所引入的关键特性。AspectJ 5之前,编写AspectJ切面需要学习一种Java语言的扩展, 但是AspectJ面向注解的模型可以非常简便地通过少量注解把任意类转变为切面。
定义切面

@Aspectpublic class Audience {@Before("execution(** concert.Performance.perform(..))")public void silenceCellPhones(){System.out.println("Silencing cell phones");}@Before("execution(** concert.Performance.perform(..))")public void taskSeats(){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("Demanding a refund");}}
@AspectJ注解表明Audience不仅仅是一个POJO, 还是一个切面。Audience类中的方法都使用注解来定义切面的具体行为
注 解 通 知@After 通知方法会在目标方法返回或抛出异常后调用@AfterReturning 通知方法会在目标方法返回后调用@AfterThrowing 通知方法会在目标方法抛出异常后调用@Around 通知方法会将目标方法封装起来@Before 通知方法会在目标方法调用之前执行上面相同的切点表达式重复了四遍
@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点
@Aspectpublic class Audience {@Pointcut("execution(** concert.Performance.perform(..))")public void performance(){}@Before("performance()")public void silenceCellPhones(){System.out.println("Silencing cell phones");}@Before("performance()")public void taskSeats(){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");}}
@Pointcut注解设置的值是一个切点表达式, 就像之前在通知注解上所设置的那样
performance()方法的实际内容并不重要, 在这里它实际上应该是空的。 其实该方法本身只是一个标识, 供@Pointcut注解依附

Audience只是一个Java类, 只不过它通过注解表明会作为切面使用 

依然可以

@Beanpublic Audience getAudience(){return new Audience();}

启动自动代理

javaConfig

@EnableAspectJAutoProxy//启用AspectJ自动代理@Configuration//注解表明该类会作为组件类,并告知spring要为这个类创建bean,此组件默认不启用@ComponentScan//这个注解会在spring中启用组件扫描,默认会扫描这个包及这个包下面的子包中的@Component public class ConcertConfig {@Beanpublic Audience audience(){return new Audience();}}

xml

<?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:c="http://www.springframework.org/schema/c"  xmlns:context="http://www.springframework.org/schema/context"  xmlns:aop="http://www/springframework.org/schema/aop"  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"><context:xomponent-scan base-package="concert"/><aop:aspectj-autoproxy/><!--启用代理--><bean class="concert.Audience"/><!--申明bean--></beans>
AspectJ自动代理会为使用@Aspect注解的bean创建一个代理, 这个代理会围绕着所有该切面的切点所匹配的bean 。SpringAspectJ自动代理仅仅使用@AspectJ作为创建切面的指导, 切面依然是基于代理的。 在本质上, 它依然是Spring基于代理的切面。尽管使用的是@AspectJ注解, 但仍限于代理方法的调用。 如果想利用AspectJ的所有能力, 我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。
创建环绕通知

所编写的逻辑将被通知的目标方法完全包装起来。就像在一个通知方法中同时编写前置通知和后置通知。 

@Aspectpublic class Audience1 {@Pointcut("execution(** concert.Performance.perform(..))")public void performance(){}@Around("performance()")//此切点的环绕通知方法public void watchPerformance(ProceedingJoinPoint jp){try {//之前System.out.println("Sliencing cell phones");System.out.println("Taking seats");jp.proceed();//之后System.out.println("CLAP");} catch (Throwable e) {System.out.println("Demanding a refund");}}}

@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知 。将控制权交给被通知的方法时, 它需要调用ProceedingJoinPointproceed()方法
不调用proceed()方法, 从而阻塞对被通知方法的访问

处理通知中的参数
@Aspectpublic 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.containsKey(trackNumber)?trackCounts.get(trackNumber):0;}}

切点表达式中的args(trackNumber)限定符。 表明传递给playTrack()方法的int类型参数也会传递到通知中去。 参数的名称trackNumber也与切点方法签名中的参数相匹配。

这个参数会传递到通知方法中, 这个通知方法是通过@Before注解和命名切点trackPlayed(trackNumber)定义的。 切点定义中的参数与切点方法中的参数名称是一样的, 这样就完成了从命名切点到通知方法的参数转移。
@Aspectpublic 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.containsKey(trackNumber)?trackCounts.get(trackNumber):0;}}
@Configuration@EnableAspectJAutoProxypublic class TrackCounterConfig {@Beanpublic CompactDisc cDisc(){BlankDisc cd = new BlankDisc();cd.setArtist("11");cd.setTitle("22");List<String>tracks = new ArrayList<>();tracks.add("33");tracks.add("44");cd.setTracks(tracks);return cDisc();}@Beanpublic TrackCounter trackCounter(){return new TrackCounter();}}
public interface CompactDisc {void playTrack(int i);}
public class BlankDisc implements CompactDisc{private String title;private String artist;private List<String> tracks;public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getArtist() {return artist;}public void setArtist(String artist) {this.artist = artist;}public List<String> getTracks() {return tracks;}public void setTracks(List<String> tracks) {this.tracks = tracks;}@Overridepublic void playTrack(int i) {}}
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = TrackCounterConfig.class)public class TrackCounterTest {@Rulepublic final StandardOutputStreamLog log = new StandardOutputStreamLog();@Autowiredprivate CompactDisc cDisc;@Autowiredprivate TrackCounter counter;@Testpublic void testTrackCounter(){cDisc.playTrack(1);cDisc.playTrack(2);cDisc.playTrack(3);cDisc.playTrack(3);cDisc.playTrack(3);cDisc.playTrack(3);cDisc.playTrack(7);cDisc.playTrack(7);}}

使用spring aop可以为bean引入新的方法,代理拦截调用并委托给实现该方法的其他对象

@Aspectpublic class EncoreableIntroducer {@DeclareParents(value = "concert.Performance+",defaultImpl=DefaultEncoreable.class)public static Encoreable encoreable;}

通过@DeclareParents注解, 将Encoreable接口引入到Performancebean
@DeclareParents注解由三部分组成:
value属性指定了哪种类型的bean要引入该接口。 在本例中, 就是所有实现Performance的类型。 (标记符后面的加号表示是Performance的所有子类型, 而不是Performance本身。 )
defaultImpl属性指定了为引入功能提供实现的类。 在这里,我们指定的是DefaultEncoreable提供实现。
@DeclareParents注解所标注的静态属性指明了要引入了接口。 在这里, 我们所引入的是Encoreable接口 
EncoreableIntroducer同样要声明为bean
在xml中声明切面
需要声明切面, 但是又不能为通知类添加注解的时候, 那么就必须转向XML配置了
SpringAOP配置元素能够以非侵入性的方式声明切面
AOP配置元素用 途<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> 定义一个切点
<aop:config><aop:aspect ref="audience"><!--引入audiencebean--><aop:before poincut="execution(**concertxml.Performance.perform(..))" method="silenceCellPhones"/><aop:before poincut="execution(**concertxml.Performance.perform(..))" method="taskSeats"/><aop:after-returning poincut="execution(**concertxml.Performance.perform(..))" method="applause"/><aop:after-throwing poincut="execution(**concertxml.Performance.perform(..))" method="demandRefund"/></aop:aspect></aop:config>
大多数的AOP配置元素必须在<aop:config>元素的上下文内使用bean声明为一个切面时, 我们总是从<aop:config>元素开始配置的
使用<aop:aspect>元素声明了一个简单的切面。ref元素引用了一个POJO bean


定义切点

<aop:config><aop:aspect ref="audience"><!--引入audiencebean--><aop:pointcut id="performance" expression="execution(**concertxml.Performance.perform(..))"/><aop:before poincut-ref="performance" method="silenceCellPhones"/><aop:before poincut-ref="performance" method="taskSeats"/><aop:after-returning poincut-ref="performance" method="applause"/><aop:after-throwing poincut-ref="performance" method="demandRefund"/></aop:aspect></aop:config>
声明环绕通知
使用环绕通知, 我们可以完成前置通知和后置通知所实现的相同功能, 而且只需要在一个方法中 实现。 因为整个通知逻辑是在一个方法内实现的, 所以不需要使用成员变量保存状态
public void watchPerformance(ProceedingJoinPoint jPoint){try {System.out.println("Silencing cell phones");System.out.println("Taking seats");jPoint.proceed();System.out.println("CLAP CLAP CLAP");} catch (Throwable e) {e.printStackTrace();}}
<aop:config><aop:aspect ref="audience"><!--引入audiencebean--><aop:pointcut id="performance" expression="execution(**concertxml.Performance.perform(..))"/><aop:around pointcut-ref="performance" method="watchPerformance" /></aop:aspect></aop:config>
为通知传递参数
@Aspectpublic class TrackCounter {private Map<Integer, Integer> trackCounts = new HashMap<Integer,Integer>();public void trackPlayed(int 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;}}
public interface CompactDisc {void playTrack(int i);}
public class BlankDisc implements CompactDisc{private String title;private String artist;private List<String> tracks;public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getArtist() {return artist;}public void setArtist(String artist) {this.artist = artist;}public List<String> getTracks() {return tracks;}public void setTracks(List<String> tracks) {this.tracks = tracks;}@Overridepublic void playTrack(int i) {}}
<bean id="trackCounter" class="soundsystemxml.TrackCounter"/><bean id="cd" class="soundsystemxml.BlankDisc"><property name="title" value="1"/><property name="artist" value="2"/><property name="tracks"><list><value>3</value></list><aop:config><aop:aspect ref="trackCounter"><!--引入trackCounterbean--><aop:pointcut id="trackPlayed" expression="execution(*soundsystemxml.CompactDisc.playTrack(int))and args(trackNumber))"/><aop:before pointcut-ref="trackPlayed" method="countTrack" /></aop:aspect></aop:config>
XML中,&符号会被解析为实体的开始
通过切面引入新的功能
<aop:declare-parents>声明了此切面所通知的bean要在它的对象层次结构中拥有新的父类型
有两种方式标识所引入接口的实现。 
使用default-impl属性用全限定类名来显式指定Encoreable的实现。 或者, 我们还可以使用delegate-ref属性来标识
<aop:config><aop:aspect><!-- <aop types-matching="concert.Performance+" declare-parents implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/> --><aop types-matching="concert.Performance+" declare-parents implement-interface="concert.Encoreable" delegate-ref="encoreableDelegate"/></aop:aspect></aop:config><bean id="encoreableDelegate" class="concert.DefaultEncoreable">
使用default-impl来直接标识委托和间接使用delegate-ref的区别在于后者是Spring bean, 它本身可以被注入、 通知或使用其他的Spring配置。
注入AspectJ
AspectJ提供了Spring AOP所不能支持的许多类型的切点 ,Spring基于代理的AOP无法把通知应用于对象的创建过程
可以在切面内部实例化这些协作的对象。 但更好的方式是, 借助Spring的依赖注入把bean装配进AspectJ切面中



通过AspectJ, 我们现在可以把之前分散在应用各处的行为放入可重用的模块中。
使用@AspectJ注解和简化的配置命名空间, 在Spring中装配通知和切点
Spring AOP不能满足需求时, 转向更为强大的AspectJ

















原创粉丝点击