面向切面的Spring

在软件开发中,发布于应用中多处的功能被称为横切关注点。通常,这些横切关注点从概念上是与应用的业务逻辑相分离的(但往往直接嵌入到应用的业务逻辑之中)。将横切关注点与业务逻辑相分离是AOP所要解决的。

 

一、AOP术语

1.通知(Advice):定义切面是什么以及何时使用。除了描述要完成的工作,还要解决何时执行工作。

5种类型的通知:

Before,在方法被调用之前调用通知

After,在方法完成之后调用通知,无论方法执行是否成功

After-returning,在方法成功执行之后调用通知

After-throwing,在方法抛出异常后调用通知

Around,通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

 

2.连接点,是在应用执行过程中能够插入切面的一个点,切面可以利用这些点插入到应用的正常流程之中,并添加新的行为。

 

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

 

4.切面,是通知和切点的结合,是什么,在何时和何处完成其功能。

 

5.引入,允许向现有的类添加新方法或属性,例如可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态,只需一种方法setLastModified(Date),和一个实例变量来保存这个状态。然后这个新方法和变量就可以被引入到现有的类中。从而在无需修改现有类的情况下,让它们具有新的行为和状态。

 

6.织入,将切面应用到目标对象来创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。在目标对象的声明周期有多个点可以进行织入。

编译期:在目标类编译时被织入,需要特殊编译器,AspectJ的织入编译器就是以这种方式织入切面的。

类加载期:在目标类加载到JVM时被织入,需要特殊的类加载器,可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的LTW(load-time-weaving)支持以这种方式织入切面。

运行期:在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象,Spring AOP以这种方式织入切面。

 

二、Spring对AOP的支持

提供4种各具特色的AOP支持:

1.基于代理的经典AOP

2.@AspectJ注解驱动的切面

3.纯POJO切面

4.注入式AspectJ切面(适合Spring各版本)

前3种都是Spring基于代理的AOP变体,因此,Spring对AOP的支持局限于方法拦截。如果需要构造器或属性拦截,那么应该考虑在AspectJ里实现切面,利用Spring的DI把Spring Bean注入到AspectJ切面中。

 

AOP框架的关键点:

1.Spring通知是Java编写的,可以使用与普通Java开发一样的IDE来开发切面,而且,定义通知所应用的切点通常在Spring配置文件里采用XML来编写。

2.Spring在运行期通知对象,通过在代理类中包裹切面,Spring在运行期将切面织入到Spring管理的Bean中。

代理类封装了目标类,并拦截被通知的方法的调用,再将调用转发给真正的目标Bean

当拦截到方法调用时,在调用目标Bean方法之前,代理会执行切面逻辑

直到应用需要被代理的Bean时,Spring才创建代理对象。如果使用ApplicationContext,在ApplicationContext从BeanFactory中加载所有Bean时,Spring创建被代理的对象。因为Spring运行时才创建代理对象,所以不需要特殊的编译器织入Spring AOP的切面。

 

3.Spring只支持方法连接点

 

二、使用切点选择连接点

切点用于准确定位应该在什么地方应用切面的通知。

在Spring AOP中,需要使用AspectJ的切点表达语言来定义切点。

Spring仅支持AspectJ切点指示器的一个子集。

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法

@arg()

限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是链接点的执行方法
this() 限制链接点匹配AOP代理的Bean引用为指定类型的类
target() 限制链接点匹配目标对象为指定类型的类
@target() 限制连接点匹配待定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotaion 限制匹配带有指定注解连接点

 

 

 

 

 

 

 

 

 

1.编写切点

该切点表达式指示选择Instrument的play()方法执行时触发通知。(..)标识其诶单选择任意的play()方法,不管方法的入参是什么。

指示器之间可以混合使用,用and、or、not(&&、||、!)连接。

 

2.使用Spring的bean指示器

Spring 2.5引入一个新的bean()指示器,允许在切点表达式中使用Bean的ID来标识Bean。

在执行Instrument的play()方法时应用通知,但限定Bean的ID为eddie。

 

三、在XML中声明切面

AOP配置元素

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

 

 

 

 

 

 

 

 

 

创建一个公众类:

import org.aspectj.lang.ProceedingJoinPoint;

public class Audience {
    public void takeSeats() {
        System.out.println("The audience is taking their seats.");
    }
    
    public void turnOffPhones() {
        System.out.println("The audience is turning off their cellphones.");
    }
    
    public void applaud() {
        System.out.println("CLAP CLAP CLAP CLAP CLAP...");
    }
    
    public void demandRefund() {
        System.out.println("Boo! We want out money back!");
    }
    
}

Audience只是一个有几个方法的简单Java类。把它注册为Spring应用上下文的一个Bean

<bean id="audience" class="cn.edu.stu.springidol.Audience" />

把audience Bean变成一个切面:

<aop:config>
    <aop:aspect ref="audience">
    <aop:pointcut expression="execution(* cn.edu.stu.springidol.Performer.perform(..))" id="performance"/>
    <aop:before pointcut-ref="performance" method="takeSeats"/>
    <aop:before pointcut-ref="performance" method="turnOffPhones"/>
    <aop:after-returning pointcut-ref="performance" method="applaud"/>
    <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
    </aop:aspect>
</aop:config>

切点声明为当调用Performer的perform方法时,应用切面。

测试:

声明一个Instrumentalist Bean,它继承了Performer

<bean id="saxphone" class="cn.edu.stu.springidol.Saxphone" />
    
<bean id="kenny" class="cn.edu.stu.springidol.Instrumentalist">
    <property name="song" value="Jingle Bells" />
    <property name="instrument" ref="saxphone" />
</bean>

测试代码

ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-idol.xml");
Performer p = (Performer) ctx.getBean("kenny");
p.perform();

结果打印出

 

声明环绕通知:
前面的切面使用了前置通知和后置通知,但如果不使用成员变量存储信息,那么在前置通知和后置通知之间共享信息非常麻烦。

例如,希望Audience能够知道参赛者表演了多长时间,如果使用成员变量保存开始时间,以为Audience是单例,将存在线程安全问题。

使用环绕通知,因为整个通知逻辑在一个方法内实现,所以不需要使用成员变量。

public void watchPerformance(ProceedingJoinPoint jointpoint) {
    try {
        System.out.println("The audience is taking their seats.");
        System.out.println("The audience is turning off their cellphone.");
        long start = System.currentTimeMillis();
            
        jointpoint.proceed();//执行被通知的方法
            
        long end = System.currentTimeMillis();
        System.out.println("CLAP CLAP CLAP CLAP CLAP");
        System.out.println("The performance took " + (end - start) + " milliseconds.");
    }  catch(Throwable t) {
        System.out.println("Boo! We want out money back!");
    }
}

 

<aop:config>
    <aop:aspect ref="audience">
    <aop:pointcut expression="execution(* cn.edu.stu.springidol.Performer.perform(..))" id="performance"/>
    <aop:around pointcut-ref="performance" method="watchPerformance"/>

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

测试代码一样,打印结果一样。

 

为通知传递参数:

有时候,通知不仅仅是对方法进行简单包装,还需要校验传递给方法的参数值,这时候为通知传递参数就非常有用了。

public interface MindReader {
    void interceptThoughts(String thoughts);
    String getThoughts();
}

public class Magician implements MindReader {
    private String thoughts;
    @Override
    public void interceptThoughts(String thoughts) {
        System.out.println("Intercepting volunteer's thoughts");
        this.thoughts = thoughts;
    }

    @Override
    public String getThoughts() {
        return thoughts;
    }
}

public interface Thinker {
    void thinkOfSomething(String thoughts);
}

public class Volunteer implements Thinker {
    private String thoughts;
    @Override
    public void thinkOfSomething(String thoughts) {
        this.thoughts = thoughts;
    }
    
    public String getThoughts() {
        return thoughts;
    }
}

MindReader可以截获Thinker的想法thoughts(一个String参数)<bean id="magician" class="cn.edu.stu.springidol.Magician" />

<bean id="magician" class="cn.edu.stu.springidol.Magician" />
<bean id="volunteer" class="cn.edu.stu.springidol.Volunteer" />
<aop:config>
    <aop:aspect ref="magician">
    <aop:pointcut expression="execution(* cn.edu.stu.springidol.Thinker.thinkOfSomething(String)) 
        and args(thoughts)" id="thinking"/>
    <aop:before pointcut-ref="thinking" method="interceptThoughts" arg-names="thoughts"/>
            
    </aop:aspect>
</aop:config>

切点标识了Thinker的thinkOfSomething方法,指定String参数,然后再args参数中标识了将thoughts作为参数,在<aop:before>元素引用了thoughts参数,标识该参数必须传递给Magician的interceptThoughts方法。

 

通过切面引入新功能:

切面只是实现了它们所包装Bean的相同接口的代理。如果代理还能发布新的接口,那么切面所通知的Bean看起来实现了新的接口,即便底层实现类并没有实现这些接口。

当引入接口的方法被调用时,代理将此调用委托给实现了新接口的某个其他对象。实际上,Bean的实现被拆分到多个类。

public interface Contestant {
    void receiveAward();
}

public class GraciousContestant implements Contestant {

    @Override
    public void receiveAward() {
        System.out.println("Receive Award");

    }

}

XML配置

<aop:config>
    <aop:aspect>
        <aop:declare-parents 
            types-matching="cn.edu.stu.springidol.Performer+" 
            implement-interface="cn.edu.stu.springidol.Contestant"
           default-impl="cn.edu.stu.springdiol.GraciousContestant"/>
    </aop:aspect>
</aop:config>

 

四、注解切面

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Audience {
    @Pointcut("execution(* cn.edu.stu.springidol.Performer.perform(..))")
    public void performance() {
        
    }
    
    @Before("performance()")
    public void takeSeats() {
        System.out.println("The audience is taking their seats.");
    }
    
    @Before("performance()")
    public void turnOffPhones() {
        System.out.println("The audience is turning off their cellphones.");
    }
    
    @AfterReturning("performance()")
    public void applaud() {
        System.out.println("CLAP CLAP CLAP CLAP CLAP...");
    }
    
    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Boo! We want out money back!");
    }
    
}

除了注解和无操作的performance方法,Audience类在实现上没有任何改变。

让Spring将Audience应用为一个切面,需要在Spring上下文声明一个自动代理Bean,该Bean知道如何将@AspectJ注解所标注的Bean转变为代理通知。

aop提供的一个自定义的配置元素:

<aop:aspectj-autoproxy />

 

<aop:aspect>元素和@Aspect注解都是把一个POJO转变为一个切面的有效方式,但是<aop:aspect>相对@Aspect的优势是不需要实现切面功能的源码,通过@Aspect,我们必须标注类和方法,它需要源码,而<aop:aspect>可以引用任意一个Bean。

 

创建环绕通知:

@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jointpoint) {
    try {
        System.out.println("The audience is taking their seats.");
        System.out.println("The audience is turning off their cellphone.");
        long start = System.currentTimeMillis();
            
        jointpoint.proceed();//执行被通知的方法
            
        long end = System.currentTimeMillis();
        System.out.println("CLAP CLAP CLAP CLAP CLAP");
        System.out.println("The performance took " + (end - start) + " milliseconds.");
    }  catch(Throwable t) {
        System.out.println("Boo! We want out money back!");
    }
}

 

传递参数给所标注的通知:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Magician implements MindReader {
    private String thoughts;
    
    @Pointcut("execution(* cn.edu.stu.springidol.Thinker.thinkOfSomething(String)) && args(thoughts)")
    public void thinking(String thoughts) {
        
    }
    
    @Override
    @Before("thinking(thoughts)")
    public void interceptThoughts(String thoughts) {
        System.out.println("Intercepting volunteer's thoughts : " + thoughts);
        this.thoughts = thoughts;
    }

    @Override
    public String getThoughts() {
        return thoughts;
    }

}

 

标注引入:

@Aspect
public class ContestantIntroducer {
    @DeclareParents(value = "cn.edu.stu.springidol.Performer+", defaultImpl = GraciousContestant.class)
    public static Contestant contestant;
}

ContestantIntroducer是一个切面,它为Performer Bean引入Contestant接口,单没有delegate-ref的对应物,所以单靠@DeclareParents还不行,必须借助<aop:declare-parents>

 

五、注入AspectJ切面

AspectJ提供比Spring AOP更细粒度的通知,例如拦截对象字段的修改,支持构造器链接点。

创建一个新切面(这是一个aspect):

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

JudgeAspect的职责是在表演结束后为表演发表评论,performance()切点匹配perform()方法,当它与after() returning()通知一起配合使用时,可以让该切面在表演结束时起作用。

并不是JudgeAspect本身发表评论,它与一个CriticismEngine对象相协作,调用该对象的getCriticism()方法发表一个评论,为了避免JudgeAspect和CriticismEngine产生耦合,通过setter依赖注入为JudgeAspect赋予CriticismEngine。

 

public interface CriticismEngine {
    String getCriticism();
}

public class CriticismEngineImpl implements CriticismEngine {

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

 

      <bean id="criticismEngine" class="cn.edu.stu.springidol.CriticismEngineImpl">
          <property name="criticisms">
              <list>
                  <value>I'm not being rude, but that was appalling.</value>
                  <value>You may be the least talented person in this show.</value>
                  <value>Do every a favor and keep your day job.</value>
              </list>
          </property>
      </bean>
      
      <bean class="cn.edu.stu.springidol.JudgeAspect" factory-method="aspectOf">
          <property name="criticismEngine" ref="criticismEngine" />
      </bean>

通常情况下,Spring Bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。因为Spring无法负责创建JudgeAspect,就不能简单地将JudgeAspect声明为一个Bean,相反,需要一种方式为所有的AspectJ切面提供一个静态的aspectOf()方法,该方法返回切面的一个单例。必须使用factory-method来调用aspectOf()方法来代替调用JudgeAspect的构造器方法。

posted @ 2016-08-24 17:41  没有梦想的小灰灰  阅读(562)  评论(0编辑  收藏  举报