您的位置:首页 > 编程语言 > Java开发

Spring之AOP

2018-03-03 21:48 190 查看
DI有助于应用对象的解耦,而AOP可以实现横切关注点与它所影响的对象之间的解耦。而横切关注点就是那些散布在应用多处的功能,比如安全、日志、事务管理。AOP是面向切面的编程,也就是把一些功能模块化,有点重用功能的意思,但是常见的面向对象技术是继承或者委托,但是如果在整个应用里都使用相同的基类,继承往往会导致一个脆弱的对象体系,而使用委托可能需要对委托对象进行复杂的调用。
使用AOP,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以什么方式在什么地方应用,而无需修改受影响的类。
AOP的术语:
1.通知:切面的工作称为通知,通知定义了切面是什么以及何时使用。
Spring切面可以应用下面五种通知:
前置通知:在目标方法被调用之前调用通知功能。
后置通知:在目标方法完成之后调用通知,此时不会关心方法的输出是后面。
返回通知:在目标方法成功后调用通知功能。
异常通知:在目标方法抛出异常后调用通知。
环绕通知:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
2.连接点:我们的应用可能也有数以千计的时机应用通知。这些时机就被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至是修改一个字段时。就比如我要在这个时机进行调用我所模块化的安全管理的方法。
3.切面:是通知和切点的结合。通知和切点共同定义了切面的内容——它是什么,在何时和何处完成其功能。类是对物体特征的抽象,切面就是横切关注点的抽象。
4.切点:有助于缩小切面所通知的连接点的范围。
5.引入:引入允许我们向现有的类添加新方法或属性。在不修改代码的情况下,引入可以在运行期为类动态的添加一些方法或者字段。比如,我们创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让他们具有新的行为和状态。
6.织入:是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:1.编译期:切面在目标编译时被织入。AspectJ的织入编译器就是以这种方式织入切面的。2.类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。3.运行期:切面在应用运行的某时刻被织入。在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring提供了4种类型的AOP支持:
1.基于代理的经典Spring AOP:引入了简单的声明式AOP和基于注解的AOP之后,Spring经典的AOP看起来就显得非常笨重和过于复杂,直接使用ProxyFactory Bean会让人觉得厌烦。
2.纯POJO切面:借助Spring的aop命名空间,我们可以将纯POJO转换为切面。实际上,这些POJO只是提供了满足切点条件时所要调用的方法。但是这项技术需要XML的配置,但这的确是声明式的将对象转化为切面的简单方式。
3.@AspectJ注解驱动的切面:Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致。这种AOP风格的好处在于能够不实用XML来完成功能。
4.注入式AspectJ切面(适用于Spring任何版本):如果AOP需求超过了简单的方法调用,如构造器或属性拦截,那么你需要考虑使用AspectJ来实现切面。
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础上,所以Spring对AOP的支持局限于方法拦截。
Spring所创建的通知都是用标准的Java代码类编写的。这样的话,我们就可以使用与普通Java开发一样的集成开发环境(IDE)来开发切面了。而且,定义通知所应用的切点通常会使用注解或在Spring配置文件里采用XML来编写。
Spring在运行时通知对象:通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之间,会执行切面逻辑。也就是说当方法调用目标对象时候,先会被代理类接收,然后线执行切面逻辑,也就是模块化出来的功能,之后再调用目标bean。
直到引用需要被代理的bean时,Spring才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有的bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
使用AOP最重要的就是编写切面:
1.利用注解来创建切面
先看一个表演类示例:@Aspect
public class Audeience{
//表演之前
@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("Demanding a refund");
}

}看这个Audience类,加上了@Aspect的注解,表明这个类不仅是一个POJO,还是一个切面。切面里面包含了功能方法跟切点,首先来讲切点的编写:
比如上面的示例:execution(** concert.Performance.perform(..)),第一个execution是AspectJ的指示器,AspectJ的指示器有如下:
1.arg():限制连接点匹配参数为指定类型的执行方法。
2.@args():限制连接点匹配参数由指定注解标注的执行方法。
3.execution():用于匹配是连接点的执行方法。
4.this():限制连接点匹配AOP代理的bean引用为指定类型的类。
5.target:限制连接点匹配目标对象为指定类型的类。
6.@target():限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解。
7.within():限制连接点匹配指定的类型。
8.@within():限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)
9.@annotation:限定匹配带有指定注解的连接点。
第一个*表示返回任意类型,第二个*表示匹配该类中所有的方法。concert.Performance.perform表示concert包中的Performance类的perform方法。后面的(..)表示切点要选择任意的perform方法,无论该方法的入参是什么。execution(** concert.Performance.perform(..))表示当满足这个表达式中所有条件的方法执行时,根据通知方法来会进行一些操作,就比如上例的@Before,就是通知方法会在目标方法调用之前执行操作silenceCellPhones方法。execution(** concert.Performance.perform(..))&&within(concert.*)限定条件可以用&&或者and表示与关系,利用||或者or表示或关系,使用!或者not表示非关系 。Spring使用AspectJ注解来声明通知方法:
@After:通知方法会在目标方法返回或抛出异常后调用。
@AfterReturning:通知方法会在目标方法返回后调用。
@AfterThrowing:通知方法会在目标方法抛出异常后调用。
@Around:通知方法会将目标方法封装起来。
@Before:通知方法会在目标方法调用之前执行。
Spring引入了新的bean指示器,它允许我们在切点表达式中使用bean的ID来标识bean。例如execution(** concert.Performance.perform(..)) and bean('woodstock'),也就是切面的通知会编织到ID为woodstock的bean中。
为了解决频繁使用的切点表达式。可以用@Poincut解决,看下面的例子:@Aspect
public class Audeience{
@Pointcut("execution(** concert.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");
}

}把频繁使用的切点表达式利用@Poincut创建成一个可重用的切点,performance()方法本身就是一个标识,好比是切点的一个代表,被传入到通知的注解中。
如果要把这个切面的类也声明为一个bean,则需要使用JavaConfig,在配置类上面加上如下注解:@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public ckass ConcertConfig{
@Bean
public Audeience audeience(){
return new Audeience();
}
}@EnableAspectJAutoProxy注解就是启动自动代理功能。或者使用XML配置,在配置文件中加入<aop:aspectj-autoproxy>。
重点讲一下上述的环绕通知:它能够让你所编写的逻辑将被通知的目标方法完全包装起来,实际上就像在一个通知方法中同时编写前置通知和后置通知。以下是重写了Aduience:@Aspect
public class Audeience{
@Pointcut("execution(** concert.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");
}
}

}首先环绕通知就是把之前的前置通知和后置通知都包含在一个里面,而watchPerformance()方法会接受一个ProceedingJoinPoint的参数,这个参数必须要有,因为要在通知中通知它来调用被通知的方法。通知方法中可以做任何事,当要将控制权交给被通知的方法时,它需要调用proceed方法。
当切面所通知的方法有参数的时候需要传递参数,看以下示例:@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.containsKey(trackNumber)
? trackCounts.get(trackNumber) : 0;
}
}切点中包含了给通知方法的参数。execution(* soundsystem.CompactDisc.playTrack(int))中的int就是参数类型,args(trackNumber)就是指定参数。args(trackNumber)表示传递给playTrack()方法的int类型参数也会传递到通知中去。参数的名称trackNumber也与切点方法签名中的参数想匹配。也就是args(trackNumber)中的与trackPlayed(int trackNumber)中的参数名要一样。然后方法countTrack中的参数跟切点中的trackPlayed(int trackNumber)也要名称一样。这样就可以传递参数了。
通过注解引入新的功能:
之前的都是在为现有的方法添加额外的功能,而我们还可以为类添加新的方法。如下例:@Aspect
public class EncoreableIntroducer{
@Declareparents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}可以看到EncoreableIntroducer是一个切面,通过@Declareparents注解将Encoreable接口引入到Performance bean中。
@Declareparents注解由三部分组成:1.value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的加好表示Performance的所有子类型,而不是Performance本身)。2.defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定DefaultEncoreable来实现。3.所标注的静态属性指明了要引入接口。在这里,我们引入了Encoreable接口。这样的话就把实现Encoreable接口的DefaultEncoreable类中实现的方法都新添加到了Performance类型的bean中。
2.在XML中声明切面
Spring的aop命名空间:
1.<aop:advisor>:定义AOP通知器
2.<aop:after>:定义AOP后置通知
3.<aop:after-returning>:定义AOP返回通知
4.<aop:after-throwing>:定义AOP异常通知
5.<aop:around>:定义AOP环绕通知
6.<aop:aspect>:定义一个切面
7.<aop:aspect-autoproxy>:启用@AspectJ注解驱动的切面
8.<aop:before>:定义AOP前置通知
9.<aop:config>:顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内
10.<aop:declare-parents>:以头名的方式为被通知的对象引入额外的接口
11.<aop:pointcut>:定义一个切点
先看一个示例:public class Audeience{
//手机静音
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
//入座
public void takeSeats(){
System.out.println("Taking seats");
}
//鼓掌
public void applause(){
System.out.println("CLAP CLAP CLAP!!!");
}
//表演失败
public void demandRefund(){
System.out.println("Demandi
afdd
ng a refund");
}

}
<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>
用法如上例,ref是引用了一个POJO bean,该bean实现了切面的功能,在这里就是audience。
上例可改为:<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))"/>
<aop:before
pointcut="performance"
method="silenceCellPhones"/>
<aop:before
pointcut="performance"
method="takeSeats"/>
<aop:after-returning
pointcut="performance"
method="applause"/>
<aop:after-throwing
pointcut="performance"
method="demandRefund"/>
</aop:aspect>
</aop:config>这样就把切点表达式写成一个了,通过id来进行传入到pointcut中。
声明环绕通知:public class Audeience{
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");
}
}
}
<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>
为通知传递参数:
public class TrackCounter{
private Map<Integer,Integer> trackCounts =
new HashMap<Integer,Integer>();

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;
}
}
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="trackPlayed"
expression="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)"/>
<aop:before
pointcut-ref="trackPlayed"
method="countTrack">
</aop:aspect>
</aop:config>
通过切面侵入新功能:<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"/>
</aop:aspect>类型匹配Performance接口的那些bean在父类结构中会增加Encoreable接口,最后解决的问题是Encoreable接口中的方法实现,有两种方法,一种就是上例中的default-impl用全限定类名来显示指定Encoreable的实现类,或者使用delegate-ref来实现比如<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
delegate-ref="encoreableDelegate"/>
</aop:aspect>不过这个encoreableDelegate是bean的ID。
<bean id="encoreableDelegate"
class ="concert.DefaultEncoreable">
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java Spring AOP