【Spring】【笔记】《Spring In Action》第4章 面向切面的Spring

前导:
     在软件开发中,分布于应用中多处的功能称为:横切关注点(cross-cutting concerns)。
     横切关注点从概念上是与应用的业务逻辑相分离的,将横切关注点与业务逻辑相分离是面向切面编程AOP要解决的。

4.1 什么是面向切面编程
     横切关注点可以被模块化为特殊的类,这些类被称为切面。

4.1.1 AOP术语
通知 Advice
     切面的工作 被称为通知。
     通知定义了切面是什么、何时使用。
     Spring切面可以应用5种类型的通知:
  • Before:在方法被调用之前调用通知
  • After:在方法完成之后调用通知,无论方法执行是否成功
  • After-returning:在方法成功执行之后调用通知
  • After-throwing:在方法抛出异常后调用通知
  • Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

连接点 Joinpoint
     连接点是在应用执行过程中能够插入切面的一个点。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为

切点 Pointcut
     切点用来缩小切面所通知连接点的范围,定义了切面的“何处”。
     切点的定义会匹配通知所要织入的一个或多个连接点。通常使用明确的类和方法名称来指定切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。某些AOP框架允许创建动态切点。

切面 Aspect
     切面是通知和切点的结合。通知和切点共同定义了关于面向的全部内容——它是什么,在何时,在何处完成其功能

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

织入 Weaving
     织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入。
  • 编译期——切面在目标类编译时被织入。需要特殊的编译器,如AspectJ的织入编译器
  • 类加载期——切面在目标类加载到JVM时被织入。需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。如AspectJ 5的LTW(load-time weaving)
  • 运行期——切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。如Spring AOP

小结:连接点是程序执行过程中能够应用通知的所有点。切点定义了通知被应用在哪些连接点上。

4.1.2 Spring对AOP的支持
     Spring提供了4种AOP支持:
  • 基于代理的经典AOP
  • @AspectJ注解驱动的切面
  • 纯POJO切面
  • 注入式AspectJ切面(适合Spring各版本)
     前3中都是Spring基于代理的AOP变体,因此,Spring对AOP的支持局限于方法拦击。如果AOP需求超过了简单方法拦截的范畴,那么应该考虑在AspectJ里实现切面,利用Spring的DI(依赖注入)把Spring Bean注入到AspectJ切面中。
     
一些Spring AOP框架的关键点:
  • Spring通知是Java编写的
  • Spring在运行期通知对象
  • Spring只支持方法连接点

4.2 使用切点选择连接点
     在SpringAOP中需要使用AspectJ的切点表达式语言来定义切点
      *Sping仅支持AspectJ切面指示器(pointcut designator)的一个子集。
     Spring AOP所支持的AspectJ切点指示器
AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的Bean引用为指定类型的类
target() 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(使用spring  AOP时,方法定义在由指定的注解所标注的类里)
@annotation 限制匹配带有指定注解连接点
     在Spring中尝试使用AspectJ其他指示器时,会抛出IllegalArgumentException异常。
     Spring支持的指示器中,只有execution指示器时唯一的执行匹配,其他的都是用于限制匹配的。

4.2.1 编写切点
e.g.
execution(     *           com.springinaction.springidol.Instrument.  play (  ..         )      )
方法执行时触发   返回类型任意   方法所属的类型                              方法     使用任意参数
可以使用&&、||、!、and、or、not来连接多个指示器

4.2.2 使用Spring的bean()指示器
     Spring2.5引入。使用Bean ID或Bean名称作为参数来限制切点只匹配特定的Bean。

4.3 在XML中声明切面
     Spring的AOP配置元素
AOP配置元素 描述
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知(无论被通知的方法是否执行成功)
<aop:after-returning> 定义AOP after-returning通知
<aop:after-throwing> 定义after-throwing通知
<aop:around> 定义AOP环绕通知
<aop:aspect> 定义切面
<aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
<aop:before> 定义AOP前置通知
<aop:config> 顶层AOP配置元素。大多数<aop:*>元素必须包含在<aop:config>元素内
<aop:declare-parents> 为被通知的对象引入额外的接口,并透明的实现
<aop:pointcut> 定义切点

4.3.1 前置和后置通知
e.g.:
audience

public class Audience {
      public void takeSeats() {
           System. out.println( "The audience is taking their seats");
     }
      public void turnOffCellPhones() {
           System. out.println( "The audience is truning off their cellphones");
     }
      public void applaud() {
           System. out.println( "papapapapapa");
     }
      public void demandRefund() {
           System. out.println( "Boo! We want our money back" );
     }
}

spring-idol.xml

< bean id= "audience" class = "com.sa.Audience"/>
      <aop:config >
            <aop:aspect ref = "audience">
                 <aop:before
                      pointcut= "execution(* com.sa.Performer.perform(..))"
                      method= "takeSeats" />
                 <aop:before
                      pointcut= "execution(* com.sa.Performer.perform(..))"
                      method= "turnOffCellPhones" />
                 <aop:after-returning
                      pointcut= "execution(* com.sa.Performer.perform(..))"
                      method= "applaud" />
                 <aop:after-throwing
                      pointcut= "execution(* com.sa.Performer.perform(..))"
                      method= "demandRefund" />
            </aop:aspect >
      </aop:config >
    
     可以定义一个命名切点以避免重复定义切点,然后使用pointcut-ref引用它
<aop:config >
    <aop:aspect ref = "audience">
          < aop:pointcut expression= "execution(* com.sa.Performer.perform(..))" id= "performance" />
                 <aop:before
                      pointcut-ref= "performance"
                      method= "takeSeats" />
          ...

4.3.2 声明环绕通知
     使用环绕通知,可以完成 前置通知和后置通知实现相同的功能,但是只需要在一个方法中实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存状态
audience

  public void watchPerformace(ProceedingJoinPoint joinpoint) {
            try {
                System. out.println( "The audience is taking their seats");
                System. out.println( "The audience is turning off their phones");
                 long start = System. currentTimeMillis();
                 joinpoint.proceed();
                 long end = System. currentTimeMillis();
                System. out.println( "PaPaPaPaPa");
                System. out.println( "The performance took " + (end - start) + "milliseconds .");
           } catch (Throwable t) {
                System. out.println( "Boo! We want our money back !");
           }
     }

spring-idol.xml

     < aop:config>
            <aop:aspect ref = "audience">
                 <aop:pointcut expression = "execution(* com.sa.Performer.perform(..))" id= "performance2" />
                 <aop:around
                      pointcut-ref= "performance2"
                      method= "watchPerformance" />
            </aop:aspect >
      </aop:config >
  • 使用ProceedingJoinPoint作为方法的入参(org.aspectj.lang.ProceedingJoinPoint),这个对象可以让我们在通知里调用被通知方法。调用ProceedingJoinPoint的proceed()方法可以把控制转给被通知方法。这里就相当于,前置通知结束时调用proceed来执行被通知方法。
  • 如果不主动调用procedd()方法,通知会阻止被通知的方法的调用。
  • 可以在通知里多次调用被通知的方法,以实现重试逻辑

4.3.3 为通知传递参数
     
 spring-idol.xml

      <aop:config>
            <aop:aspect ref = "magician">
                 <aop:pointcut
                      id= "thinking"
                      expression= "execution(* com.sa.Thinker.thinkOfSomething(String)) and args(thoughts)" />
                 <aop:before
                      pointcut-ref= "thinking"
                      method= "interceptThought"
                      arg-names= "thoughts" />
            </aop:aspect >
      </aop:config >
     在切点中指定参数,在切面中设置arg-names属性为切点中设定的参数,标识该参数必须传递给相应的方法。

4.3.4 通过切面引入新功能
     “引入”,切面可以为Spring Bean添加新的方法
     切面只是实现了它们所包装Bean的相同接口的代理,“引入”让代理还能发布新的接口,这样切面所通知的Bean看起来实现了新的接口,即便底层实现类并没有实现这些接口。
     当引入接口的方法被调用时,代理将此调用委托给实现了新接口的某个其他对象。实际上,Bean的实现被拆分到了多个类。
spring-idol.xml

<aop:aspect>
  <!-- 二选一 1.Bean方式 2.全限定类名-->
  <aop:declare-parents
     types-matching= "com.sa.Performer+"
     implement-interface= "com.sa.Contestant"
     delegate-ref= "contestantDelegate" />

  <aop:declare-parents
     types-matching= "com.sa.Performer+"
     implement-interface= "com.sa.Contestant"
     default-impl= "com.sa.GraciousContestant" />
</aop:aspect >


     <aop:declare-parents>声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类型。也就是:类型匹配types-matching属性指定接口的bean,会实现implement-interface属性指定的接口。
     两种方式标识标识所引入接口的实现。
  1. 使用default-impl属性通过全限定类名来显式指定接口的实现。
  2. 使用delegate-ref属性来引用一个Spring Bean。
     区别在于,delegate-ref引入了一个Spring Bean,本身可以被注入,被通知等等……


4.4 注解切面
     AspectJ 5 引入,通常称为@AspectJ。
Audience

@Aspect
public class Audience {
      @Pointcut( "execution(* com.sa.performer.perform(..))")
      public void performance() {
           ;
     }
      @Before( "performance()")
      public void takeSeats() {
           System. out.println( "The audience is taking their seats");
     }
      @Before( "performance()")
      public void turnOffCellPhones() {
           System. out.println( "The audience is truning off their cellphones");
     }
      @AfterReturning( "performance()")
      public void applaud() {
           System. out.println( "papapapapapa");
     }
      @AfterThrowing( "performance()")
      public void demandRefund() {
           System. out.println( "Boo! We want our money back" );
     }
}
     @Pointcut 注解用于定义一个可以在@AspectJ切面内可重用的切点。其值是一个AspectJ切点表达式。
     切点的名称来源于注解所应用的方法名称。
     因为该类本身包含了所有它需要定义的切点和通知,所以不需要在XML中声明切点和通知。
     我们需要在Spring上下文中声明一个自动代理Bean,该Bean直到如何把@AspectJ注解所标注的Bean转变为代理通知。Spring自带了名为AnnotationAwareAspectJAutoProxyCreator的自动代理创建类。我们可以在Spring上下文中把AnnotationAwareAspectJAutoProxyCreator类注册为一个Bean,为了简化这个操作,Spring在aop命名空间中提供了一个自定义的配置元素:
     <aop:aspectj-autoproxy/>
     <aop:aspectj-autoproxy/>在spring上下文中创建一个AnnotationAwareAspectJAutoProxyCreator类,它会自动代理一些Bean,这些Bean的方法需要与使用@Aspect注解的Bean中所定义的切点相匹配,而这些切点又是使用@Pointcut注解定义出来的。
     要使用<aop:aspectj-autoproxy>配置元素,我们需要在Spring的配置文件中包含aop命名空间:
< beans xmlns= "http://www.springframework.org/schema/beans"
            xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
            xmlns:aop= "http://www.springframework.org/schema/aop"
           xsi:schemaLocation= "http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop-4.2.xsd" >
     *<aop:aspectj-autoproxy>仅仅使用@AspectJ注解作为指引来创建基于代理的切面,但本质上它仍然是一个Spring风格的切面。这意味着:我们虽然使用@AspectJ的注解,但是我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并不依赖spring来创建基于代理的切面。

4.4.1 注解环绕通知
     使用@Around创建环绕通知
@Around( "performance()")
      public void watchPerformance(ProceedingJoinPoint joinpoint) {
            try {
                System. out.println( "The audience is taking their seats");
                System. out.println( "The audience is turning off their phones");
                 long start = System. currentTimeMillis();
                 joinpoint.proceed();
                 long end = System. currentTimeMillis();
                System. out.println( "PaPaPaPaPa");
                System. out.println( "The performance took " + (end - start) + " milliseconds ." );
           } catch (Throwable t) {
                System. out.println( "Boo! We want our money back !");
           }
     }


4.4.2 传递参数给所标注的通知
     
@Aspect
public class Magician implements Mindreader {

      private String thoughts;

      @Pointcut( "execution(* com.sa.Thinker.thinkOfSomething(Sring)) "
                + " && args(thoughts)" )
      public void thinking(String thoughts) {
            //null
     }

      @Before( " thinking(thoughts)")
      public void interceptThought(String thoughts) {
           System. out.println( "Intercepting volunteer's thoughts");
            this. thoughts = thoughts;
     }

      public String getThoughts() {
            return thoughts;
     }

}

     使用注解方式传递参数给所标注的通知的时候,不需要像XML方式那样显示指定一个“args-names”属性所对应的注解,@AspectJ能够依靠Java语法来判断为通知所传递参数的细节。

4.4.3 标注引入
     @AspectJ的@DeclareParents对应于<aop:declare-parents>,在基于@AspectJ注解所标注的类内使用时,@DeclareParents工作方式与<aop:declare-parents>几乎相同。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;

@Aspect
public class ContestantIntroducer {

      @DeclareParents(
                value = "com.sa.Performer + " ,
                defaultImpl = GraciousContestant. class)
      public static Contestant contestant;
}
     ContestantIntroducer是一个切面。这个切面没有提供通知。它为Performer Bean引入了Contestant接口。标注引入对应于<aop:declare-parents>引入的属性:
  • value:等同于<aop:declare-parents>的types-matching属性。标识应该被引入指定接口的Bean的类型(也就是被引入的类)
  • defaultImpl:等同与<aop:declare-parents>的default-impl属性。它标识该类提供了所引入接口的实现。
  • 由@DeclareParents注解所标注的static属性指定了将被引入的接口
     需要像其他切面一样,把@Aspect注解标注的切面声明为Spring上下文中的一个Bean。
     Spring发现使用了@Aspect注解所标注的Bean时,<aop:aspectj-autoproxy>将自动创建单例。一句被调用的方式是属于被代理的Bean还是引入的接口,该代理把调用委托给被代理的Bean或引入的实现。
     *@DeclareParents没有对应于<aop:declare-parents>的delegate-ref属性所对应的等价物。这是因为@DeclareParents是一个@AspectJ注解,是一个不同于Spring的项目,因此它的注解不了解Spring的Bean。这意味着: 如果想委托给Spring所配置的Bean,@DeclareParents不能完成,只能使用<aop:declare-parents>

4.5 注入AspectJ切面
     spring与aspectJ的配合:如果在执行通知时,切面依赖于一个或多个类,我们可以使用Spring的依赖注入把Bean装配仅AspectJ切面中。

JudgeAspect aspect

package com.sa;

public aspect JudgeAspect {

    public JudgeAspect() {

    }

    pointcut performance():execution(* perform(..));

    after() returning() :performance() {
        System.out.println(criticismEngine.getCriticism());
    }

    //injected
    private CriticismEngine criticismEngine;
    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}

CriticismEngineImpl

public class CriticismEngineImpl implements CriticismEngine {
    public CriticismEngineImpl() {

    }

    public String getCriticism() {
        int i = (int)(Math.random() * criticismPool.length);
        return criticismPool[i];
    }

    //injected
    private String[] criticismPool;
    public void setCriticismPool(String[] criticismPool) {
        this.criticismPool = criticismPool;
    }
}

spring-idol.xml

<bean id="criticismEngine"
    class="com.sa.CriticismEngineImpl">
    <property name="criticisms">
        <list>
            <value>I'm not being rude, but that was appalling.</value>
            <value>You may be the least talented person in this show.</value>
            <value>Do everyone a favor and keep your day job.</value>
        </list>
    </property>
</bean>

     AspectJ切面根本不需要Spring就可以织入应用,但是如果想使用Spring的依赖注入为AspectJ切面注入协作者,那么就需要在Spring配置中把切面声明为一个Spring Bean。
<bean class="com.sa.JudgeAspect"
     facotry-method="aspectOf">
     <property name="criticismEngine" ref="criticismEngine"/>
</bean>
     通常情况下,Spring Bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。当Spring有机会为JudgeAspect注入CriticismEngine时,JudgeAspect已经被实例化了。
     因为Spring无法负责创建JudgeAspect,那就不能在Spring中将JudgeAspect普通的声明为一个Bean。我们需要一种方式为Spring获得已经由AspectJ创建的JudgeAspect实例的句柄,从而可以注入CriticismEngine。AspectJ切面都提供了一个静态的asepectOf()方法,返回切面的一个单例。所以为了获得切面的实例,我们必须使用factory-method来调用aspectOf方法来代替调用JudgeAspect的构造器方法。
     总结:Spring不能使用<bean>声明来创建一个JudgeAspect实例——它已经在运行时由AspectJ创建了。spring通过aspectOf()工厂方法获得切面的引用,然后像<bean>元素规定的那样在该对象上执行依赖注入。



第4章总结
     AOP有效减少了代码冗余,并让我们的类关注自身的主要功能。
     















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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值