面向切面的Spring

在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来说,这些横切关注点从概念上是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑相分离正是买了弦切面编程(AOP)所要解决的问题。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与他们所影响的对象之间解耦。

AOP的术语

切面(Aspect)

横切关注点可以被模块化为特殊的类,这些类可以称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更加简洁,因为它们主要关注业务代码,而次要关注的代码被移入切面中。

通知(Advice)

在AOP术语中,切面的工作就被称为通知。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,不关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用
  • 异常通知(After-throwing):在目标方法抛出异常之后调用
  • 环绕通知(Around):通知包裹被通知方法,在被通知的繁华调用之前和之后执行自定义的行为

连接点(Join point)

连接点是应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至修改一个字段时。切点代码可以利用这些点插入到应用的正常流程中,并添加新的行为。

切点(Poincut)

一个切点并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。切点的定义,我们需要使用明确的类和方法名称或者利用正则表达式来指定切点(切点表达式)

织入(Weaving)

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

  • 编译期:切面在目标类编译阶段被织入。这种方式需要特殊的编译器。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器。
  • 运行期:切面在应用运行的某个阶段被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。

AspectJ这三种方式方式都支持,Spring AOP只支持在运行期织入切面。

引入(introduction)

AOP的作用就在于增强目标对象现有的属性或方法,而引入允许我们向现有的类中添加新方法或属性。

Spring 对AOP的支持

并不是所有AOP框架都是相同的,它们在连接点模型上可能有强弱之分,它们织入切面的方式和时机也会有不同。但是无论如何,创建切点来定义切面所织入的连接点是所有AOP框架的基本功能。Spring AOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截,这是Spring作为AOP框架的局限性。

如果AOP的需求超过了简单的方法调用(如构造器或属性拦截),那么就需要考虑使用AspectJ来实现切面。

Spring只支持方法级别的连接点

Spring基于动态代理,所以Spring只支持方法级别的连接点,缺少对字段连接点的支持,无法让我们创建细颗粒度的通知,例如拦截对象字段的修改;而且它不支持构造器连接点,我们无法在bean创建的时候应用通知。

虽然方法拦截可以满足大部分的需求,但要拦截其他,就需要利用AspectJ来补充Spring AOP的功能。

通过切点来选择连接点

在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。AspectJ的切点指示器只有execution是实际用于执行匹配的,其他的只是限制匹配的。execution指示器是我们在编写切点表达式时最主要使用的。

编写切点

execution(返回类型 全限定的类.方法(参数))

例如: execution(* cn.lynu.Performance.perform(..))

表达式以"*"号开始,表明可以返回任意类型,然后我们使用全限定的类名和方法名,对于方法参数列表,我们使用两个点号(..)表明该方法可以使用任意入参

我们还可以使用限制的指示器来匹配,例如使用execution()和within()限制切点。使用的是“&&”操作符进行连接:

 execution(* cn.lynu.Performance.perform(..) && within(cn.*))

类似的还可以使用“||”运算符表示或的关系,“!”运算符表示非的关系。

但是因为“&&”在XML中有特殊含义,Spring的XML配置里面使用切点可以使用and替代“&&”,or和not分别替代“||” 和“!”.

使用Java代码定义切面

使用AspectJ的@Aspect注解表明一个Jav类作为切面,这个类中的方法都可以使用注解来定义切面的具体行为。AspectJ使用5个注解来对应5中通知方式:

  • @After 后置通知
  • @Before 前置通知
  • @AfterReturning 返回通知
  • @AfterThrowing 异常通知
  • @Around 环绕通知

所有的这些通知注解都可以使用一个切点表达式作为它的值。

@Aspect
public class Audience {
    
    @Before("execution(* test04.Performance.perform(..))")
    public void silenceCellPhone() {
        System.out.println("将手机调置静音");
    }
    
    @Before("execution(* test04.Performance.perform(..))")
    public void taskSeats() {
        System.out.println("观众就坐");
    }
    
    @AfterReturning("execution(* test04.Performance.perform(..))")
    public void applause() {
        System.out.println("鼓掌");
    }
    
    @AfterThrowing("execution(* test04.Performance.perform(..))")
    public void demandRefund() {
        System.out.println("表演失败,观众要求退款");
    }

}

如果所有的这些切点表达式都是相同的,我们可以使用@Pointcut注解定义个可重用的切点:

    @Pointcut("execution(* test04.Performance.perform(..))")
    public void performance() {}
    
    @Before("performance()")
    public void silenceCellPhone() {
        System.out.println("将手机调置静音");
    }

performance方法是一个空方法,其本身只是作为一个标识,供@Pointcut注解依附。其实这个已经是切面的Audience,我们依然可以像其他Java类那样使用它的方法,它的方法也可以独立地进行测试,这与其他Java类并没有什么不同。只是使用了@Aspect注解,并不会被视为切面,这些注解也不会解析,也不会转换为切面的代理,还需要启动自动代理功能。

如果使用的是JavaConfig的话,可以在配置类的类级别上使用@EnableAspectJAutoProxy注解启用:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackageClasses= {Performance.class})
public class Config {
    @Bean
    public Audience audience() {
        return new Audience();
    }
}    

如果使用XML来装配bean,就需要使用aop命名空间的<aop:aspect-autoproxy>元素

<!--启用AspectJ自动代理-->
<aop:aspectj-autoproxy />

<bean class="cn.Audience"/>

不要忘了将切面声明为一个Spring bean,不论是用JavaConfig还是XML。

虽然我们使用了AspectJ的注解来创建切面,但是这个切面依然是基于代理的,它依然是Spring基于代理的切面,仍然受限于代理方法的调用,不能利用AspectJ所有的能力。

接下来,我们可以测试这个切面的效果了:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {Config.class})
public class Test {

    @Autowired
    private Performance performance;
    
    @org.junit.Test
    public void test01() {
        performance.perform();
    }
}

创建环绕通知

环绕通知是最为强大的通知类型,它能够让所编写的逻辑将被通知的目标方法完全包装起来,事实上就像在一个通知方法中同时编写前置和后置通知。

@Aspect
public class Audience {
    
    @Pointcut("execution(* test04.Performance.perform(..))")
    public void performance() {}
    
    //环绕通知
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint pj) {
        try {
            System.out.println("关闭手机");
            System.out.println("就坐");
            //调用被通知方法
            pj.proceed();
            System.out.println("鼓掌");
        } catch (Throwable e) {
            System.out.println("表演失败,观众要求退款");
        } 
    }
}

注意环绕通知方法的参数是ProceedingJoinPoint作为入参的,这个对象是必须的,因为需要在环绕通知方法中通过它来调用目标方法,使用的是它的proceed()方法,不要忘记调用这个方法,如果不调这个方法,则会阻塞被通知方法的调用。

处理通知中的参数

切面所通知的法拉伐确实有参数该怎么办?如何在切面中访问和使用传递给被通知方法的参数?

我们换一个有参数的切点,并改造切点表达式:

@Pointcut("execution(* cn.lynu.CompactDisc.playTrack(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber){}

@Before("trackPlayed(trackNumber)")
public void before(int trackNumber){
  system.out.print(trackNumber);  
}

被通知的方法入参是int类型,并使用args限制器,参数的名称是与切点方法签名中的参数名相匹配的。这样一来,就可以在通知方法中使用传递给切点方法的参数了

通过注解引入新功能

之前,我们一直是为目标对象以拥有的方法添加新功能,实际上,利用引入的概念,AOP可以为对象添加新的方法。在Spring中,切面只是实现了它们所包裹bean相同接口的代理,。如果这些代理可以暴露新的接口,那么目标类看起来也实现了新的接口,即使底层实现类并没有实现这些接口。但调用这个新引入的方法时,代理会把调用胃痛给实现了新接口的某个其他对象。

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value="test04.Performance+",defaultImpl=DefaultEncorable.class)
    public static Encoreable encoreable;
}

通过@DeclareParents注解,将Encoredable接口引入到Performance bean中。这个注解有三个部分组成:value属性指定了哪种类型bean要引入该接口,标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。defaultImpl属性指定为引入功能提供实现的类。注解所标注的静态属性指明了要引入的接口。

接下来,我们可以测试调用这个引入的新方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {Config.class})
public class Test {

    @Autowired
    private Performance performance;
    
    @org.junit.Test
    public void test01() {
        //需要先强转为引入的接口类型,再调用新方法
        Encoreable encoreable=(Encoreable) performance;
        encoreable.performEncore();
    }
    
}

运行之后,可以正常使用这个新方法,如果不通过引入的方法,直接强转会出现ClassCaseException。

使用注解的方式真的太方便了,但是这种方式由一个明显的缺点:必须可以看到和修改源码。如果没有源码,或不想将AspectJ的注解放在代码中,我们就需要使用XML的方式。

在XML中声明切面

在Spring aop命名空间中,提供了多个元素用在XML中声明切面:

AOP配置元素 用途
<aop:advisor> 定义AOp通知器
<aop:after> 定义AOP后置通知
<aop:after-returning> 定义AOP返回通知
<aop:after-throwing> 定义AOP异常通知
<aop:around> 定义环绕通知
<aop:aspect> 定义切面
<aop:before> 定义前置通知
<aop:aspectj-autoproxy> 这个之前就见过,是为启用@Aspec注解
<aop:config> 顶层到的AOP配置,大多数<aop:*>元素必须包裹在<aop:config>元素内
<aop:declare-parents> 引入
<aop:pointcut> 定义切点
<bean id="audience" class="test04.Audience"></bean>    

<aop:config>
     <aop:aspect ref="audience">
         <aop:pointcut expression="execution(* test04.Performance.perform(..))" id="pointcut"/>
         <aop:before pointcut-ref="pointcut" method="silenceCellPhone"/>
         <aop:before pointcut-ref="pointcut" method="taskSeats"/>
         <aop:after-returning pointcut-ref="pointcut" method="applause"/>
         <aop:after-throwing pointcut-ref="pointcut" method="demandRefund"/>
         <aop:around pointcut-ref="pointcut" method="watchPerformance"/>
     </aop:aspect>
    </aop:config>

关于Spring AOP配置元素,注意的是大多数AOP配置元素必须在<aop:config>元素上下文内使用.这里使用<aop:pointcut>将相同的切点抽取出来,如果通知的切点不一致,在通知中使用pointcut属性而不是pointcut-ref。<aop:pointcut>元素还可以放在<aop:config>元素范围内,提供其他切面使用。

为通知传递参数

在AspectJ注解的方式中,我们可以获得目标方法的参数,使用XML的方式也可以:

<aop:pointcut expression="execution(* test04.CompactDisc.playTrack(int)) and args(trackNumber)" id="pointcut"/>

只不过在XML中用and or not表示与或非,而不是&& || !

在XML中引入新功能

AspectJ中使用的是@DeclareParents注解,在XML中对应的就是Spring aop命名空间中的<aop:declare-parents>元素:

    <bean id="audience" class="test04.Audience"></bean>
    <aop:config>
     <aop:aspect ref="audience">
         <aop:declare-parents types-matching="test04.Performance+" implement-interface="test04.Encoreable" delegate-ref="myPerformance"/>
     </aop:aspect>
    </aop:config>

最后再说一点,相比较AspectJ,SpringAOP只局限与对方法的增强,AOP的功能较弱,如果需要对对于构造器,属性等类型的切点,就需要直接使用AspectJ。

猜你喜欢

转载自www.cnblogs.com/lz2017/p/8975421.html
今日推荐