阅读 183

Spring AOP 基础

AOP

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

术语

横切关注点

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

增强(Advice)

增强定义了切面要完成的功能以及什么时候执行这个功能。

Spring 切面可以应用 5 种类型的增强:

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

连接点(Join Point)

应用中每一个有可能会被增强的点被称为连接点。

切点(Pointcut)

切点是规则匹配出来的连接点。

切面(Aspect)

切面是增强和切点的结合,定义了在何时和何处完成其功能。

引入(Introduction)

引入允许我们向现有的类中添加新方法和属性。可以在不修改现有的类的情况下,让类具有新的行为和状态。

织入(Weaving)

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

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

Spring 对 AOP 的支持

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

  • 基于代理的经典 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

关注下面的标签,发现更多相似文章
评论