第四章 面向切面的Spring

散布与应用多处的功能称为横切关注点(cross-cutting concern),通常来讲,这些横切关注点是与项目业务逻辑相分离的,但往往会嵌入其中,把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题
  • 本章将会展示Spring对切面的支持,包括如何把普通类声明为一个切面和如何使用注解创建切面
  • 还会看到AspetcJ–另一种流行的Aop实现–如何补充SpringAop框架的功能
为什么要使用切面编程:

如果需要重用通用功能的话,最常见的面向对象的技术是继承(inheritance)和委托(delegation),但如果在整个应用中都采用相同的基类,继承往往会导致形成一个脆弱的对象体系,而使用委托将会产生复杂的委托调用

切面编程的优点
  • 切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更加清晰简洁
  • 在使用面向切面变成的时候,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改手影响的类
  • 横切关注点被模块化为特殊的类,这些类称为切面(aspcet)

定义AOP术语

与大多数技术一样,AOP已经形成了自己的术语,比较常见的术语有

  • advice(通知)
  • pointcut(切点)
  • join point(连接点)

为了理解AOP,我们必须了解这些术语,在进入某个领域之前,我们必须学会在这个领域如何说话

advice(通知)

通知类型通知含义
前置通知(Before)在目标方法调用之前调用此功能
后置通知(After)在目标方法调用之后调用此功能,此时不会关注此方法输出了什么
返回通知(After-returning)在目标方法成功之后执行此通知
异常通知(After-throwing)在目标方法抛出异常后调用通知
环绕通知(Around)通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

John Point(连接点)

我们的应用可能有数以千计的时机应用通知,这些时机被称为连接点,连接点是在应用执行过程中能够插入切面的一个点

这些点可以是调用方法时,抛出异常时,甚至修改一个字段时,切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为.

pointcut(切点)

如果说通知定义了切面的 "什么"和 "何时"的话,那么切点就定义了何处,切点有助于缩小切面所通知的连接点的范围

Aspect(切面)

切面是通知和切点的结合,通知和切点定义了切点的全部内容

Introduction(引入)

引入允许我们向现有的类添加新方法和属性

(在无需修改这些现有的类的情况下,让他们具有新的行为和状态)

weaving(织入)

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中,在目标对象的生命周期有多个点可以织入

  • 编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器,AspcetJ的织入编译器就是在这个时候被织入切面的
  • 类加载期:切面在目标类加载到jvm时被织入,这种方式需要特殊的类加载器(ClassLoader),他可以在目标类被引入应用之前增强该目标的字节码,AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面
  • 运行期,切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,Aop容器会为目标对象动态的创建一个代理对象,SpringAop就是以这种方式织入切面的.

通知包含了需要用于多个应用对象的横切行为;

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

切点定义了被通知应用的具体位置;

其中关键的概念是切点定义了那些连接点会得到通知


Spring对AOP的支持

Spring提供了四种类型的AOP支持

  • 基于代理的经典SpringAOP;
  • 纯POJO切面
  • @Aspect注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各版本)

前三种都是SpringAOP实现的变体,SpringAOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截

Spring经典的AOP显得非常笨重和过于复杂,所以不会再介绍经典的SpringAOP.


借助SpringAOP的命名空间,我们可以将纯POJO转换为切面,实际上这些pojo只是提供了满足切点条件所要调用的方法,遗憾的是,这种技术需要xml配置,但这的确是声明式的将对象转换为切面的简便方式

Spring借鉴了Aspect的切面,以提供注解驱动的AOP.本质上,他依然是Spring基于代理的AOP,但是编程模型几乎与编写成熟的AspectJ注解切面完全一致,这种AOP的好处在于能够不使用Xml来完成功能

如果你的Spring需求超过了简单的方法调用(如构造器或者属性拦截),那么你需要考虑使用AspectJ来实现切面,在这种情况下,第四种类型能够将值注入到AspectJ驱动的切面中

Spring框架的关键知识

  • Spring所创建的通知都是用标准的java类编写的

  • 定义通知所应用的切点通常会使用注解或在Spring配置文件里用xml来编写

     这两种语法对java开发人员都是非常熟悉的
    
  • AspectJ与此恰好相反,虽然AspectJ现在支持基于注解的切面,但AspectJ最初是以java语言拓展实现的,这种方法有优点也有缺点.

优点:

    (通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集)

缺点:

    我们需要额外学习新的工具和语法

Spring在运行时通知对象

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中

    (代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean)
    当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑

直到应用需要到被代理的bean时,Spring才会创建被代理的对象.

    如果使用的是ApplicationContext的话,在ApplicationContext
    从BeanFactory中加载所有的bean的时候,Spring才会创建被代理的对象

因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入SpringAOP的切面

Spring只支持方法级别的连接点

因为Spring基于动态代理,所以Spring只支持方法连接点(这与其他的一些AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,他们还提供了字段和构造器接入点)

  • Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改

  • Spring不支持构造器连接点,我们无法在bena创建时应用通知

  • 但是方法拦截可以满足大部分的需求,如果需要方法拦截之外的连接点拦截功能,那么我们可以利用AspectJ来补充SpringAOP的功能


通过切点来选择连接点

通知和切点是切面的基本元素,因此,了解如何编写切点十分重要

在SpringAOP中,要使用AspctJ的切点表达式语言来定义切点

关于SpringAOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器(pointcut designator的一个子集)

SpringAOP所支持的AspectJ切点指示器

AspectJ指示器描述
arg()限制连接点匹配参数为指定类型的执行方法
@args()限制连接点匹配参数由指定注解标注的执行方法
execution()用于匹配是连接点的指定方法
this()限制连接点匹配AOP代理的bean引用为指定的类
target限制连接点匹配目标对象为指定类型的类
@target()限制连接点匹配特定的执行对象,这些对象的类要具有指定类型的注解
within()限制连接点匹配指定的类型
@within()限制连接点匹配指定注解所标注的类型(当使用SpringAOP时,方法定义由指定的注解所标注的类里)
@annotation限定匹配带有指定注解的连接点

在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常

在上述的指示器中,只有execution()指示器是实际执行匹配的,这说明execution()指示器是我们在编写切点时最主要使用的指示器,再次基础上,我们使用其他指示器来限制所匹配的切点.

编写切点

为了阐述Spring的切面,我们需要个主题来定义切面的切点

package concert;

public interface performance {
public void perform();
}

performance可以展现任何形式的表演,假设我们想编写performance的perform()方法触发的通知,我们需要编写一个切点表达式,这个表达式能够设置当perform()方法执行通知时的调用

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

现在假设我们需要匹配的切点进匹配concert包,在此场景下,可以使用within()指示器来限制匹配

//使用&&(and) 和 within()指示器来限定范围
execution(*concert.Performance.perform(...))
&& within(concert.*)

这里注意我们使用了&& 来表达excution()和within()的and关系,相同的我们可以使用|| 和!来表达或和否的关系

在切点中选择bean

除了表里列举的指示器外,Spring还引入了一个新的bean指示器

  • 它允许我们在切点表达式中使用bean的id来标识bean,bean()使用bean ID 或bean 名称作为参数来限制切点来匹配特定的bean

    //在这里,我们希望在执行演出方法时应用通知,但限定bean的id为woodstock
    execution(*concert.Performance.perform(…))
    and bean(‘woodstock’)

在某些场景下,限定切点为指定的bean非常有意义,我们还可以使用非操作符为除了bean id以外的其他bean应用通知

execution(*concert.Performance.perform(..))
    and !bean('woodstock')
如何编写通知和使用这些切面?

使用注解创建切面

使用注解创建切面是AspectJ5所引入的关键特性
  • 在AspectJ 5 之前,编写AspectJ切面需要学习一种java语言的拓展
  • 但是AspectJ面向注解的模型可以非常简便的通过少量注解把任意类转换成切面

我们定义的performance接口,他是切面中切点的目标对象,接下来使用AspectJ注解来定义切面

定义切面

一场演出就需要有观众,所以我们将观众(audience)定义为切面

从演出的角度来看,观众是非常重要的,但是观众并不是核心,而是一个单独的关注点
,所以将观众定义为一个切面,并将它应用到演出上是很明智的选择
定义一个观众类(Audience)
package concert;


import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class Audience {


//表演之前,关闭手机
@Before("execution(* concert.performance.perform(..))")
public void silencingCellPhones(){
    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("Demand refund");
}


}

Audience类用了@AspectJ注解进行标注,这表明他不仅是个pojo,而且还作为切面,Audience中的方法都使用注解来定义切面的具体行为

AspectJ提供了五个注解来定义通知
注解通知
@After通知方法会在目标方法返回后或抛出异常后调用
@AfterReturning通知方法会在目标方法返回后调用
@AfterThrows通知方法会在目标方法抛出异常后调用
@Around通知方法会将目标方法封装起来
@Before通知方法会在目标方法调用之前执行
你可能也发现,在Audience类中,相同的切点表达式被用了四遍,在这种情况下,@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点
    上述案例可以用@Pointcut改写为
    
    package concert;


import org.aspectj.lang.annotation.*;

@Aspect
public class Audience {


//表演之前,关闭手机
@Pointcut("execution(* concert.performance.perform(..))")
public void performance(){}

@Before("performance()")
public void silencingCellPhones(){
    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("Demand refund");
}


}

proformance()方法的实际内容不重要,在这里它实际上应该是空的,其实该方法只是一个标识,供@Pointcut注解依附

需要注意的是,除了注解和实际操作的performance()方法,Audience类依然是一个pojo(我们能够像使用其他java类那样调用他的方法,他的方法也能独立的进行单元测试)

Audience只是一个普通的java类,只不过他通过注解表明会作为切面使用而已

像其他的java类一样,他也可以作为java中的bean

@Bean
public Audience audience(){
    return new Audience();
    
}
但如果仅做到这样的话,Audience只会是Spring容器中的一个bean,即使使用了@AspectJ注解,但他并不会视为切面,这些注解不会解析,也不会创建将其转换为切面的代理.
  • 如果使用javaConfig的话,可以在配置类的类级别上通过EnableAspectJ-AutoProxy注解后启用自动代理功能,下面的例子展现了如何在javaConfig中启用自动代理.

      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 ConcertConfig {

    //声明Audience bean
    @Bean
    public Audience audience(){

      return new Audience();
    

    }
    }

  • 如果需要Sprng的Aop命名空间中的aop:aspectj-autoproxy/元素,下面的xml配置展现了如何完成该功能

      <context:component-scan base-package="concert"/>
      <aop:aspectj-autoproxy/>
      <bean class="concert.Autience"/>
      <bean/>
    

不管是你使用javaConfig还是xml配置,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean

    在这种情况下,将会为Concertbean创建一个代理,Audience类中的通知方法将会在perform()调用前后执行

我们需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的,在本质上,他依然是Spring基于代理的切面,这很重要,因为这以为这尽管使用的是@Aspect注解,但我们仍然限于代理方法的调用

Spring AOP仅使用了AspcetJ的冰山一角,如果想彻底使用Aspect的能力,我们必须在运行时使用AspectJ并且不依赖与Spring来创建基于Spring的切面

最为强大的通知类型—环绕通知

环绕通知

环绕通知是最为强大的通知类型,他能够让你编写的逻辑把被通知的目标方法完全包裹起来(实际上就像是在一个通知方法中同时编写前置通知和后置通知)

使用环绕通知实现Audience切面
    package concert;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class Audience {

//使用环绕通知重写Audience切面

//定义命名的切点
@Pointcut("execution(* concert.performance.perform(..))")
public void performance(){}

//环绕通知方法
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint){


    try {
        //坐好座位
        System.out.println("Taking seats");
        //关闭手机
        System.out.println("Silencing cell phones");
        joinPoint.proceed();
        //欢呼
        System.out.println("clap!clap!clap!!!");
    } catch (Throwable e) {
        //要求退款
        System.out.println("Demand refund");

    }

}



}
关于这个新的通知方法,你首先注意到的可能是它接受peoceedingJoinPoint作为参数,这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法,通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,他需要调用ProceedJoinPoint的proceed()方法.

你也可以不调用proceed()方法,去阻塞对被通知方法的访问

处理通知中的参数
切点定义中的参数与切点方法中的参数是一样的,这样就完成了从命名切点到通知方法的参数转移
到目前为止,在我们所使用的切面中,所包装的都是被通知对象的已有方法,但是,方法包装仅仅是切面所能实现的功能之一

通过注解引入新功能

利用被称为引入的AOP概念,切面可以用Spring bean添加新方法,代理拦截调用并委托给实现该方法的其他对象,实际上,一个bean的实现被委托到其他类中

//假如我们需要为实例中所有的performance实现引入下面的Encoreable接口:

package concert;

public interface Encoreable{
    
    void performEncore();
}

即使我们可以能够访问performance的所有实现,并对其进行修改,让他们都实现Encoreable接口.但是从设计的角度看,这并不是很好的做法,因为并不是所有的performance都是具有Encoreable特性的,另一方面,有可能无法修改所有的performance实现,当使用第三方实现并且没有源码的时候更是如此

但是,借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性的改变现有的实现

为了实现该功能,我们要创建一个新的切面

    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;


}

EncoreableIntroducer也是一个切面,但是,它与我们之前创建的切面不同,它没有提供前置,后置或环绕通知,而是通过@DeclareParents注解,将Encoreable接口引入到Performance bean中

@DeclareParents注解由三部分组成
  • value属性指定了哪种类型的bean要引入该接口,在本例中,也就是所有实现了Performance的类型(标记符后面的加号表示是Performance的所有子类型,而不是Performance本身.)
  • defaultImpl属性指定了为引入功能提供实现的类,在这里,我们指定的是DefaultEncoreable提供实现
  • @DeclareParents注解所标注的静态属性指明了要引入接口,在这里,我们所引入的是Encoreable接口

和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:

在Spring中,注解和自动代理提供了一种很便利的方式来创建切面,但是面向注解的切面有一个劣势,就是需要源码,否则就要采取下面的方式

在Xml中声明切面

在Spring的AOP命名空间中,提供了多个元素在Xml中声明切面

Spring的AOP配置元素能够以非侵入性的方式声明切面

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定义一个切点
后面用示例展示用Xml如何实现注解相同的效果
通过Xml将无法注解的Audience声明为切面
<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="exection(** concert.Performance.perform(..))"
method = "applause"/>

<aop:after- throwing

pointcut="exection(** concert.Performance.perform(..))"
method ="demandRefund"/>

</aop:aspect>

</aop:comfig>
使用aop:pointcut定义命名切点
<aop:config>
<aop:aspcet ref="audience">

<aop:pointcut
id = "performance"
expression="excution(** concert.Performance.perform(..))"/>

<aop:before

pointcut-ref = "performance"
method = "silenceCellPhones"/>

<aop:before

pointcut-ref= "performance"
 method = "takeSeats"/>

 <aop:after- returning
 
    pointcut-ref = "performance"
 method = "applause"/>
 
 
 <aop:after- throwing

     pointcut-ref = "performance"
method ="demandRefund"/>


</aop:aspect>
</aop:config>
在Xml中使用aop:around元素声明环绕通知
    <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>

用XML为通知传递参数

    推荐用注释解决这个问题,xml配置太麻烦

通过切面引入新的功能

Aop引入不是AspectJ特有的,使用SpringAOP命名空间中的aop:declare-parents元素,我们可以实现相同的功能

<aop:aspect>
<aop:declare-parents

types-matching = "concert.Performance+"
implement-interface = "concert.Encoreable"
default-impl = "concert.DefaultEncoreable"

/>
</aop:aspect>

在本例中,我们使用default-impl属性用权限定类名来显式指定Encoreable的实现,或者,我们还可以使用delegate-ref属性来标识

aop:aspect
<aop:declare-parents

types-matching = "concert.Performance+"
implement-interface = "concert.Encoreable"
default-ref = "encoreableDelegate"

/>
</aop:aspect>

delegate-ref属性引入了一个Spring bean作为引入的委托,这需要在Spring上下文中存在一个id为encoreableDelegate的bean

<bean id= "encoreableDelegate"

class="concert.DefaultEncoreable"/>

区别:

使用default-impl和使用delegate-ref的区别在于后者是Springbean,它本身可以被注入,通知,或使用其他Spring配置

注入AspectJ切面

相比Spring Aop只能将方法作为切点,AspectJ提供了Spring Aop所不能支持的很多类型的切点

但是精心设计且有意义的切面可以依赖其他类来完成他们的工作,如果在执行通知时,切面依赖于一个类或多个类,我们可以在切面内部实例化这些协作的对象,但更好的方式是我们可以借助Spring的依赖注入把bean装配进AspectJ切面中.

切面也需要注入,像其他的bean一样,Spring也可以为AspectJ切面注入依赖

Spring需要通过aspectOf()工厂方法获得切面的引用,然后像元素规定的那样在该对象上执行依赖注入

这部分的内容在Spring实战这本书中的叙述十分模糊,书上的案例残缺不全,后续会通过别的渠道来弥补

小结

Spring的AOP框架使我们将关注点放到了类本身,为减少代码的冗余的降低耦合度奠定了坚实的基础

我们会在后面越过Spring的基础内容,我们将看到Spring如何实际的构建项目.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值