面向切面编程

本节课示例代码:https://github.com/jingjiecb/section3

image-20210310104042454

Spring框架最重要的内容之一,面向切面编程

软件编程方法的发展:

  1. 面向过程编程 POP
  2. 面向对象编程 OOP
  3. 面向切面编程 AOP
  4. 函数式编程 FP
  5. 反应式编程 Rx

面向切面编程:对于已经写好的业务代码,如果想添加新的功能比如日志或者认证,直接通过集成或者委托会对代码构成侵入。面向切面编程可以不修改任何原本的业务代码,将新功能加入。

例如:对于音乐会对象,要求不修改任何业务代码,能够增加新的行为(添加新的方法)。

概念

横切关注点:需要切入到业务逻辑中的(通用)功能和模块,例如日志、安全、事务、缓存。

织入:将横切关注点的功能加入原有的目标对象的过程。

通知Advice:包括什么时候切入(切入时机)以及做什么(新功能)

切点Poincut:在何处切入(切入时机)。通过切点表达式指名。

切面Aspect:一个写着各种切点和通知的类,可包含多个通知。用@Aspect指明。

引入:引入新的行为和状态

Tips: 将切面应用到原有的逻辑中,只需要在JavaConfig配置类中配置切面类的Bean即可。

需要在配置类上面通过注解@EnableAspectJAutoProxy启动代理,才能切入。开启代理后,从Spring容器中拿到的是代理对象,代理对象可以根据切面,在调用实际对象的方法前后进行插入新行为。Spring实际上是在运行期进行织入。

定义切入

通知类型:

  • @Before 在被切入的方法执行之前执行。有多个Before切入时,按照写的顺序进行切入。
  • @AfterReturning 方法正常返回后调用
  • @AfterThrowing 方法抛出异常后调用
  • @After 上面两种情况导致方法结束时调用
  • @Around 可以自由指定被切入方法的执行时机。参数中传入一个ProceedingJoinPoint对象,调用它的proceed()方法就可以起到调用原来被切入方法的作用。

Around的例子如:

@Aspect
public class Audience2 {
    @Pointcut("execution(* concert.Performance.perform( .. )) ")
    public void performance() {
    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println(".Silencing cell phones");
            System.out.println(".Taking seats");
            joinPoint.proceed();
            System.out.println(".CLAP CLAP CLAP!!!");
        } catch (Throwable e) {
            System.out.println(".Demanding a refund");
        }
    }
}

另外可以通过@PointCut注解复用切点,例如:

@Aspect
public class Audience1 {
    // 声明一个切点
    @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("Demand a refund");
    }
}

切面表达式

被切入方法可能有传入的参数。在切面表达式中通过args关键字可以获取到方法调用传入的参数。例如:

@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);
}

可以通过within关键字指定切入类的范围,例如 within(soundSystem.*) 表示对soundSystem包下的指定类进行切入,而不管其他包。

也可以用过bean关键字指定特定名字的Bean进行织入。例如bean(sgtPeppers)表示只对名字为sgtPeppers的Bean进行织入。

引入

为一个类增加新的方法,而不改变原来的代码文件。

首先需要为新行为定义一个接口,并写出具体实现该接口的类,指明具体的业务逻辑。然后通过一个切面类,中间用注解指明切点和具体实现。例如:

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value = "concert.Performance+",//后面的+表示应用到所有实现了该接口的Bean
            defaultImpl = DefaultEncoreable.class) //表示具体实现类
    public static Encoreable encoreable; // 以成员变量的方式加入即可
}

注意:织入的包含新行为的对象是单例的。

XML配置织入

<aop:aspectj-autoproxy/>

<bean id="audience" class="concert2.Audience"/>
<bean id="concert" class="concert.Concert"/>

<aop:config>
    <aop:aspect ref="audience">
        <aop:before method="silenceCellPhones"
                    pointcut="execution(* concert.Performance.perform(..))"/>
        <aop:before method="takeSeats"
                    pointcut="execution(* concert.Performance.perform(..))"/>
        <aop:after method="applause"
                   pointcut="execution(* concert.Performance.perform(..))"/>
        <aop:after-throwing method="demandRefund"
                            pointcut="execution(* concert.Performance.perform(..))"/>
    </aop:aspect>
</aop:config>

<aop:aspectj-autoproxy/>表示启动代理。后面通过aop:config标签进行配置,指定切面、切点、切入的方法等信息。

当然也可以进行切点复用:

<aop:aspectj-autoproxy/>

<bean id="audience" class="concert2.Audience"/>
<bean id="concert" class="concert.Concert"/>

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut id="performance"
                      expression="execution(* concert.Performance.perform(..))"/>

        <aop:before method="silenceCellPhones"
                    pointcut-ref="performance"/>

        <aop:before method="takeSeats"
                    pointcut-ref="performance"/>

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

        <aop:after-throwing method="demandRefund"
                            pointcut-ref="performance"/>
    </aop:aspect>
</aop:config>

先定义一个aop:pointcut即可。

也可以像下面一样声明around:

<aop:config>
    <aop:aspect ref="audience">
        <aop:pointcut id="performance"
                      expression="execution(* concert.Performance.perform(..))"/>

        <aop:around method="watchPerformance"
                    pointcut-ref="performance"/>
    </aop:aspect>
</aop:config>

引入:

<!--    <bean id="encoreableDelegate" class="concert.DefaultEncoreable"/>-->    
<aop:config>
    <aop:aspect>
        <aop:declare-parents types-matching="concert.Performance+"
                             implement-interface="concert.Encoreable"
                             default-impl="concert.DefaultEncoreable"/>
<!--                                 delegate-ref="encoreableDelegate"/>-->

    </aop:aspect>
</aop:config>

如果要使用单例,则可以通过先配置bean,然后在aop:declare-parents中写明 delegate-ref 指向单例bean即可。