Spring in Action之AOP

Spring in Action之AOP

AOP术语

通知(Advice)

通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:

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

连接点是应用执行过程中能够插入切面的一个点。

切点(Pointcut)

切点的定义会匹配通知所要织入的一个或者多个连接点。通常使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定这些切点。

切面(Aspect)

切面是通知和切点的结合。

引入(Introduction)

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

织入(Weaving)

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

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

非常重要一点:切点定义了哪些连接点会得到通知。

Spring提供了4种类型的AOP支持:

  • 基于代理的经典SpringAOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各个版本)

Spring对AOP的支持局限于方法拦截。

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。Spring的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用方法。

切点表达式:
execution(* com.up.vincent.Performance.perform(..)) and within(com.up.vincent.*)

任意返回类型,方法所属的类,方法,任意参数,指定包下。

execution(* com.up.vincent.Performance.perform(..)) and !bean('woodstock')

切面的通知会被编织到所有ID不为woodstock的bean中。

切面样例代码:

@Aspect
public class Audience {

    @Pointcut("execution(* com.soundsystem.Performance.perform(..))")
    public void performance() {
    }

    @Before("performance()")
    public void silencePhone() {
        System.out.println("xx xx");
    }

    @Before("performance()")
    public void takeSeates() {
        System.out.println("xx xx");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("xx xx");
    }

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

在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启动自动代理功能。

@EnableAspectJAutoProxy
@Configuration
@ComponentScan
public class ConcertConfig {

    @Bean
    public Audience audience() {
        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.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="com.soundsystem"/>

    <!-- 启动AspectJ自动代理 -->
    <aop:aspectj-autoproxy/>
    <!-- 声明Audience这个bean -->
    <bean id="audience" class="com.soundsystem.Audience"/>
</beans>

注意:不管使用JavaConfig还是XML,Aspect自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。

环绕通知:
@Aspect
public class Audience {

    @Pointcut("execution(* com.soundsystem.Performance.perform(..))")
    public void performance() {
    }

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("xx xx");
            joinPoint.proceed();
            System.out.println("yy yy");
        } catch (Throwable throwable) {
            System.out.println("zz zz");
        }
    }
}
处理通知中的参数:
@Aspect
public class TrackCounter {
    private Map<Integer, Integer> trackCounts = new HashMap<>();

    @Pointcut("execution(* com.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;
    }
}

注意:参数的名称trackNumber也与切点方法签名中的参数想匹配。从命名切点到通知方法的参数转移。

通过注解引入新的功能
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
  @DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
  public static Encoreable encoreable;
}

@DeclareParents解释说明:

  • value属性指定了哪种类型的bean要引入该接口,在本例中,也就是所有实现Performance的类型。(标记符后面的+(加号)表示Performance的所有子类型,而不是Performance本身)
  • defaultImpl属性指定了为引入功能提供实现的类,在这里,我们指定的是DefaultEncoreable提供实现。
  • @DeclareParents注解所标注的静态属性指明了要引入的接口,在这里,我们所引入的是Encoreable接口。

在XML中声明切面

Spring中AOP配置元素能够以非侵入性的方式声明切面

AOP配置元素 用途
< aop:advisor > 定义AOP通知器
< aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功)
< aop:after-returning> 定义AOP返回通知
< aop:after-throwing> 定义AOP异常通知
< aop:around> 定义AOP环绕通知
< aop:aspect> 定义一个切面
< aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
< aop:before> 定义一个AOP前置通知
< aop:config> 顶层的AOP配置元素,大多数< aop:*>元素都必须包含在此元素之内
< aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
< aop:pointcut> 定义一个切点

代码样例:

<bean id="audience" class="com.springinaction.springidol.Audience"/>

<aop:config>

  <aop:pointcut id="performance" expression="execution(* *.perform(..))"/>

  <aop:aspect ref="audience">
    <!-- 注意在这些通知中引用方法的时候,只需要写方法名,不需要() -->
    <aop:before method="takeSeats" pointcut-ref="performance"/>

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

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

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

  </aop:aspect>

</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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="trackCounter" class="soundsystem.TrackCounter"/>
    <bean id="cd" class="soundsystem.BlankDisc">
        <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band"/>
        <property name="artist" value="The Beatles"/>
        <property name="tracks">
            <list>
                <value>Sgt. Pepper's Lonely Hearts Club Band</value>
                <value>With a Little Help from My Friends</value>
                <value>Lucy in the Sky with Diamonds</value>
                <value>Getting Better</value>
                <value>Fixing a Hole</value>
            </list>
        </property>
    </bean>
    <aop:config>
        <aop:aspect ref="trackCounter">
            <aop:pointcut id="trackPlayed" expression="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)"/>
            <aop:before pointcut-ref="trackPlayed" method="countTrack"/>
        </aop:aspect>
    </aop:config>
</beans>
XML方式通过切面引入新的功能:
<aop:aspect>
  <aop:declare-parents  types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable" />
</aop:aspect>

注意:default-impl必须使用全限定类名来显示指定Encoreable的实现。除此之外,还可以使用delegate-ref属性来标识。

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

注入AspectJ切面

代码JudgeAspect文件的后缀为.aj

public aspect JudgeAspect {
    public JudgeAspect() {
    }

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

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

    private CriticismEngine criticismEngine;

    public void setCriticismEngine(CriticismEngine criticismEngine) {
        this.criticismEngine = criticismEngine;
    }
}
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;
  }
}
<bean id="criticismEngine" class="com.springinaction.springidol.CriticismEngineImpl">
    <property name="criticisms">
        <list>
            <value>Worst performance ever!</value>
            <value>I laughed, I cried, then I realized I was at the wrong show.</value>
            <value>A must see show!</value>
        </list>
    </property>
</bean>
<bean class="com.springinaction.springidol.CriticAspect" factory-method="aspectOf">
  <property name="criticismEngine" ref="criticismEngine" />
</bean>

通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。

等到Spring为CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。Spring需要通过aspectOf()工厂方法获得切面的引用。

你可能感兴趣的:(spring,AOP)