Spring AOP 基础

AOP

使用面向切面编程时,咱们仍然在一个地方定义通用功能,可是能够经过声明的方式定义该功能要以何种方式在何处应用,而无需修改受影响的类。php

术语

横切关注点

影响应用多处的功能(日志、事务、安全)java

加强(Advice)

加强定义了切面要完成的功能以及何时执行这个功能。spring

Spring 切面能够应用 5 种类型的加强:express

  • 前置加强(Before) 在目标方法被调用前调用加强功能
  • 后置加强(After) 在目标方法完成以后调用加强,不关注方法输出是什么
  • 返回加强(After-returning) 在目标方法成功执行以后调用加强
  • 异常加强(After-throwing) 在目标方法抛出异常后调用加强
  • 环绕加强(Around) 在被加强的方法调用以前和调用以后执行自定义行为,即包括前置加强和后置加强。

链接点(Join Point)

应用中每个有可能会被加强的点被称为链接点。编程

切点(Pointcut)

切点是规则匹配出来的链接点。安全

切面(Aspect)

切面是加强和切点的结合,定义了在什么时候和何处完成其功能。app

引入(Introduction)

引入容许咱们向现有的类中添加新方法和属性。能够在不修改现有的类的状况下,让类具备新的行为和状态。框架

织入(Weaving)

织入是把切面应用到目标对象中并建立新的代理对象的过程。在目标对象的生命周期里有多个点能够进行织入:dom

  • 编译器:切面在目标类编译时织入。这种方式须要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载器:切面在目标类加载到 JVM 时被织入。这种方式须要特殊的类加载器(ClassLoader),它能够在目标类被引入应用以前加强该目标类的字节码。AspectJ5 的加载时织入(LTW)支持以这种方式织入。
  • 运行期:切面在应用运行时的某个时刻被织入。通常状况下,在织入切面时,AOP 容器会为目标对象动态地建立一个代理对象。Spring AOP 就是以这种方式织入切面的。

Spring 对 AOP 的支持

Spring 对 AOP 的支持在不少方面借鉴了 AspectJ 项目。目前 Spring 提供了 4 种类型的 AOP 支持:this

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

Spring AOP 构建在动态代理基础之上,所以 Spring 对 AOP 的支持局限于方法拦截。

运行时加强

经过在代理中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截被加强方法的调用,再把调用转发给真正的目标 bean。在代理拦截到方法调用时,在调用目标 bean 方法以前,会执行切面逻辑。

直到应用须要代理的 bean 时,Spring 才建立代理对象。若是使用 ApplicationContext 的话,在 ApplicationContextBeanFactory 中加载全部 bean 的时候,Spring 才会建立被代理的对象。

方法级别的链接点

Spring 基于动态代理实现 AOP,因此 Spring 只支持方法链接点。其余的 AOP 框架好比 AspectJ 与 JBoss,都提供了字段和构造器接入点,容许建立细粒度的加强。

切点表达式

Spring AOP 中,使用 AspectJ 的切点表达式来定义切点。Spring 只支持 AspectJ 切点指示器(pointcut designator)的一个子集。

指示器

AspectJ 指示器 描述
arg( ) 限制链接点匹配参数为指定类型的执行方法
execution( ) 用于匹配链接点
this 指定匹配 AOP 代理的 bean 引用的类型
target 指定匹配对象为特定的类
within( ) 指定链接点匹配的类型
@annotation 匹配带有指定注解的链接点

编写切点

package concert;

public interface Performance {
    public void perform();
}
复制代码

Performance 类能够表明任何类型的现场表演,好比电影、舞台剧等。如今编写一个切点表达式来限定 perform() 方法执行时触发的加强。

execution(* concert.Performance.perform(..))
复制代码

每一个部分的意义以下图所示:

切点表达式

也能够引入其余注解对匹配规则作进一步限制。好比

execution(* concert.Performance.perform(..)) && within(concert.*)
复制代码

within() 指示器限制了切点仅匹配 concert 包。

Spring 还有一个 bean() 指示器,容许咱们在切点表达式中使用 bean 的 ID 表示 bean。

execution(* concert.Performance.perform(..)) && bean('woodstock')
复制代码

以上的切点就表示限定切点的 bean 的 ID 为 woodstock

使用注解建立切面

定义切面

在一场演出以前,咱们须要让观众将手机静音且就座,观众在表演以后鼓掌,在表演失败以后能够退票。在观众类中定义这些功能。

@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("CLAP CLAP CLAP!!!");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }

}
复制代码

@AspectJ注解表名了该类是一个切面。 @Pointcut定义了一个类中可重用的切点,写切点表达式时,若是切点相同,能够重用该切点。 其他方法上的注解定义了加强被调用的时间,根据注解名能够知道具体调用时间。

到目前为止,Audience仍然只是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,可是这些注解仍然不会解析,由于目前还缺少代理的相关配置。

若是使用 JavaConfig,在配置类的类级别上使用 @EnableAspectJAutoProxy注解启用自动代理功能。

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {

    @Bean
    public Audience audience() {
        return new Audience();
    }
   
}
复制代码

若是使用 xml ,那么须要引入 <aop:aspectj-autoproxy> 元素。

<?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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <context:component-scan base-package="concert"/>

  <aop:aspectj-autoproxy/>

  <bean class="concert.Audience"/>
</beans>
复制代码

环绕加强

环绕加强就像在一个加强方法中同时编写了前置加强和后置加强。

@Aspect
public class Audience {

    @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 throwable) {
            System.out.println("Demanding a refund");
        }
    }
}
复制代码

能够看到,这个加强达到的效果与分开写前置加强与后置加强是同样的,可是如今全部的功能都位于同一个方法内。 注意该方法接收 ProceedingJoinPoint 做为参数,这个对象必需要有,由于须要经过它来调用被加强的方法。 注意,在这个方法中,咱们能够控制不调用 proceed() 方法,从而阻塞对加强方法的访问。一样,咱们也能够在加强方法失败后,屡次调用 proceed() 进行重试。

加强方法参数

修改 Perform#perform() 方法,添加参数

package concert;

public interface Performance {
    public void perform(int audienceNumbers);
}
复制代码

咱们能够经过切点表达式来获取被加强方法中的参数。

@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))")
    public void performance(int audienceNumbers){}
复制代码

注意,此时方法接收的参数为 int 型,args(audienceNumbers) 指定参数名为 audienceNumbers,与切点方法签名中的参数匹配,该参数不必定与加强方法的参数名一致。

引入加强

切面不只仅可以加强现有方法,也能为对象新增新的方法。 咱们能够在代理中暴露新的接口,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其余对象。实际上,就是一个 bean 的实现被拆分到多个类中了。 定义 Encoreable 接口,将其引入到 Performance 的实现类中。

public interface Encoreable {

    void performEncore();

}
复制代码

建立一个新的切面

@Aspect
public class EncoreableIntroducer {

    @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}
复制代码

咱们使用了 @AspectEncoreableIntroducer 标记为一个切面,可是它没有提供前置、后置或环绕加强。经过@DeclareParents注解将Encoreable接口引入到了 Performance bean 中。

@DeclareParents 注解由三部分组成:

  • value 属性指定了哪一种类型的 bean 要引入该接口。在上述代码中,类名后面的 + 号表示是 Performance 的全部子类型,而不是它自己。
  • defaultImpl 属性指定了为引入功能提供实现的类。
  • @DeclareParents 注解所标注的静态属性指明了要引入的接口。

一样地,咱们在 Spring 应用中将该类声明为一个 bean:

<bean class="concert.EncoreableIntroducer" />
复制代码

Spring 的自动代理机制将会获取到它的声明,并建立相应的代理。而后将调用委托给被代理的 bean 或者被引入的实现,具体取决于调用的方法属于被代理的 bean 仍是属于被引入的接口。

在 XML 中声明切面

更新一下 Audience 类,将它的 AspectJ 注解所有移除。

public class Audience {
    
  
    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("Demanding a refund");
    }

}
复制代码

声明前置与后置加强

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:config>
    <aop:aspect ref="audience">
      <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/>

      <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>
</beans>
复制代码

如上所示,就将一个普通方法变为了加强。 大多数的 AOP 配置元素都必须在 <aop:config> 元素的上下文内使用。元素名基本上都与注解名相对应。 这里,咱们一样将同一个切点表达式写了四遍,将它提取出来。

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

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

      <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>
</beans>
复制代码

注意,此时 <aop:pointcut> 标签位于 <aop:aspect> 下层,故只能在该切面中引用。若是想要一个切点可以被多个切面引用,能够将 <aop:aspect> 元素放在 <aop:config> 下第一层。

环绕加强

定义环绕加强方法

public class Audience {

    public void performance(int audienceNumbers){}

    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 throwable) {
            System.out.println("Demanding a refund");
        }
    }
}
复制代码

在 xml 中使用 <aop:around> 指定方法名与切点便可。

<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <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>
</beans>
复制代码

为加强传递参数

获取参数主要就在于切点表达式。

<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/>
复制代码

这样能在 xml 中定位到一个参数类型为 int ,参数名为 audienceNumbers 的切点。 注意在 xml 中使用了 and 代替 &&(在 XML 中,&符号会被解析为实体的开始)。

引入加强

<aop:declare-parents types-matching="concert.Performance+"
        implement-interface="concert.Encoreable"
        default-impl="concert.DefaultEncoreable"/>
复制代码

types-matching指定了要匹配的类型,与注解中的 value 值功能相同。

注入 AspectJ 切面

AspectJ 切面提供了 Spring AOP 所不能支持的许多类型的切点。 切面颇有可能依赖其余类来完成它们的工做。咱们能够借助 Spring 的依赖注入把 bean 装配进 AspectJ 切面中。

建立一个新切面。

public aspect CriticAspect {

    private CriticismEngine criticismEngine;

    public CriticAspect() {
    }

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

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

    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}
复制代码

注入的 CritismEngine 的实现类

public class CriticismEngineImple implements CriticismEngine {

    public CriticismEngineImple() {
    }

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

    public void setCriticismPool(String[] criticismPool) {
        this.criticismPool = criticismPool;
    }
}
复制代码

CriticAspect主要做用是在表演结束后为表演发表评论。 实际上,CriticAspect 是调用了 CriticismEngine的方法来发表评论。经过 setter 依赖注入为 CriticAspect 设置 CriticismEngine

切面注入

在配置文件中将 CriticismEngine bean 注入到 CriticAspect 中。

<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf">
   <property name="criticismEngine" ref="criticismEngine"/>
 </bean>
复制代码

通常状况下,Spring bean 由 Spring 容器初始化,可是 AspectJ 切面是由 AspectJ 在运行期建立的。因此在运行期间,AspectJ 建立好了 CriticAspect 实例,每一个 AspectJ 都会提供一个静态的 aspectOf() 方法,返回切面的的单例。 使用factory-method 调用 aspectOf()方法向 CriticAspect 中注入 CriticismEngine

相关文章
相关标签/搜索