SpringInAction读书笔记--第4章面向切面

1.什么是面向切面编程

  在软件开发中,散布于应用中多处的功能被称为横切关注点,这些横切关注点从概念上是与应用的业务逻辑相分离的,但往往分直接嵌入到应用的业务逻辑之中,把这些横切关注点与业务逻辑相分离正式面向切面(AOP)要解决的问题。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。

  例如,安全是一个横切关注点,应用中许多方法都会涉及到安全规则,如果要重用通用功能的话,常见的面向对象技术是继承或委托,但是继承往往会导致一个脆弱的对象体系,而使用委托可能需要对委托对象进行复杂的调用。切面提供了一种替代方案,在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处,首先每个关注点都集中于一个地方,而不是分散到多处代码中,其次,应用只包含它们的业务逻辑代码,安全、事务管理等被转移到切面中了。

  定义AOP术语

  AOP常用的术语有通知(advice)、切点(pointcut)和连接点(joinpoint)。切面的工作被称为通知,通知描述了切面要完成的工作和何时执行这个工作,Spring切面可以应用5种类型的通知:

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

  应用通知的时机被称为连接点,连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。

  切点定义了“何处”应用通知,切点的定义会匹配通知要织入的一个点或多个连接点,我们通常使用明确的类和方法名称或利用正则表达式定义所匹配的类和方法名称指定这些切点,有些AOP框架允许创建动态的切点,可以根据运行时的决策决定是否应用通知。

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

  引入(Introduction)允许我们向现有的类添加新方法或属性,我们可以创建一个通知类,该类通过一个实例变量和Setter方法记录了对象最后一次修改时的状态。然后这个新方法和实例变量就可以被引入到现有的类中,从而在无需修改现有的类的情况下,使它们具有新的行为和状态。

  织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期有多个点可以进行织入:编译期、类加载期、运行期,Spring AOP是在应用运行时织入的。

  Spring对AOP的支持

  Spring对AOP的支持在很多方面借鉴了AspectJ项目,Spring提供了4种类型的AOP支持:

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

  经典的Spring AOP模型显得笨重和复杂,不过多讨论;纯POJO借助Spring的aop命名空间可以转化为切面,但是需要XML配置;注解驱动的好处在于可以不适用XML配置;第4种类型的AOP可以将值注入到AspectJ驱动的切面。

  Spring通知是Java编写的,切点的定义使用注解或XML。(AspectJ最初是使用特有的AOP语言(现在支持基于注解的切面),这意味着需要额外学习新的工具和语法,而Spring AOP无需新的语法)。

  Spring在运行时通知对象

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

2.通过切点选择连接点

  Spring AOP要使用AspectJ的切点表达式语言定义切点,因为Spring是基于代理的,所以它仅支持AspectJ切点指示器的一个子集,其中只有execution是实际执行匹配的,其它的指示器都是限制匹配的。

execution(** concert.Performance.perform(..))

//* = 返回任意类型
//concert.Performace = 全限定类名
//perform = 方法名
//(..) = 使用任意参数
execution(** concert.Performance.perform(..) and within(concert.*))

//使用within()限制匹配,仅匹配concert包
execution(** concert.Performance.perform(..) and bean('woodstock'))

//限定bean的ID

3.使用注解创建切面

  定义切面

  Spring使用@AspectJ注解标注该类不仅仅是一个POJO,还是一个切面。类中的方法都使用注解定义切面的具体行为。

@Aspect
public class Audience {
    @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("Applause");
    }
    @AfterThrowing("execution(** concert.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("Demand Refund");
    }
}

  这四个方法的切点表达式都是相同的,我们可以使用@Pointcut注解在切面内定义可重用的切点。

@Aspect
public 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 takeSeats(){
        System.out.println("Taking Seats");
    }
    @AfterReturning("performance()")
    public void applause(){
        System.out.println("Applause");
    }
    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("Demand Refund");
    }
}

  如果只将Audience类装配为Spring中的bean,即便使用了@AspectJ注解,它也不会被视为切面。这些注解不会解析,也不会创建将其转换为切面的代理。需要启用自动代理功能。

  使用JavaConfig启动自动代理功能

@Configuration
@EnableAspectJAutoProxy             //启用AspectJ自动代理
public class ConcertConfig {
    @Bean
    public Audience audience(){     //声明 Audience Bean
        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: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-3.0.xsd   
    http://www.springframework.org/schema/context   
    http://www.springframework.org/schema/context/spring-context-3.0.xsd 
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    
    <aop:aspectj-autoproxy />                   <!--  启用Aspectj自动代理 -->
    <bean class="concert.Audience"/>          <!-- 声明Audience bean -->
</beans>

  AspectJ自动代理会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。这样的话会为Concert bean创建一个代理,Audience类中的通知方法将会在perform方法调用前后执行。

  创建环绕通知

  环绕通知是更为强大的通知类型,它可以让切面的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

@Aspect
public class Audience {
    @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("Applause");
        } catch (Throwable e) {
            System.out.println("Demand Refund");
        }
    }
}

  通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的proceed()方法。有时你可以不调用proceed()方法,从而阻塞对被通知方法的访问。类似的,也可以在通知中对其多次调用,实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。

  处理通知中的参数

  如果被通知的方法中有参数,切面能够访问和使用传递给被通知方法的参数。

@Aspect
public class TrackCounter {
    private HashMap<Integer, Integer>    trackCounts    = new HashMap<>();

    @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类型参数也会传递到通知中去,切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。

  通过注解引入新功能

  如果需要为已知的API添加新功能,由于是已知的API,我们不能修改其类,只能通过外部包装,最简单的办法就是实现某个我们自定义的接口,这个接口包含了想要添加的方法。但是Java不是一门动态的语言,无法在编译后动态添加新的功能,这时候可以用aop的declare-parents来做。我们为所有的Performance实现引入Endoreable接口。

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
    public static Encoreable encoreable;
    
    //value属性指定了哪种类型的bean要引入该接口,在本例中也就是所有实现Performance的类型,加号表示是其子类型
    //defaultImpl属性指定了为引入功能提供实现的类
    //@DeclareParents注解所标注的静态属性指明了要引入的接口
}

  和其它的切面一样,我们需要在Spring应用中将EncoreableIntorducer声明为一个bean。在使用时,直接通过getBean获得bean再转换成相应的接口就可以使用了。

@Test
public void intro(){
    ApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);
    Encoreable concert = (Encoreable) context.getBean("concert");
    concert.performEncore();
}

4.在XML中声明切面

  面向注解的切面声明必须能够为通知类添加注解,所以必须要有源码。如果没有源码或者不想将AspectJ注解放到代码中,Spring提供了在XML配置文件中声明切面。

  声明前置和后置通知

<aop:config>
    <aop:aspect ref="audience">
        <aop:before 
            pointcut="execution(** concert.Performance.perform(..))" method="silence"/>
        <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="refund"/>    
    </aop:aspect>
</aop:config>

  当切点重复时,需要使用<aop:pointcut>元素。

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut
            id="performance" expression="execution(** concert.Performance.perform(..))"/>
        <aop:before 
            pointcut-ref="performance" method="silence"/>
        <aop:before
            pointcut-ref="performance" method="takeSeats"/>
        <aop:after-returning
            pointcut-ref="performance" method="applause"/>
        <aop:after-throwing
            pointcut-ref="performance" method="refund"/>    
    </aop:aspect>
</aop:config>

  声明环绕通知

  环绕通知可以在一个方法中实现前置通知和后置通知,在前置通知和后置通知之间只需要局部变量共享信息而不需使用成员变量,避免了线程安全问题。我们首先重写Audience类。

@Aspect
public class Audience {
    public void watchPerformance(ProceedingJoinPoint jp){
        try {
            System.out.println("Silence");
            System.out.println("Seats");
            jp.proceed();
            System.out.println("Applause");
        } catch (Throwable e) {
            System.out.println("Refund");
        }
    }
}

   声明环绕通知使用<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>

   为通知传递参数

<aop:config>
    <aop:aspect ref="trackCounter">
        <aop:pointcut
            id="trackPlayed" expression="execution(* soundsystem.CompactDsic.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>
posted @ 2016-12-03 16:34  语陌1988  阅读(316)  评论(0编辑  收藏  举报