Spring实战——面向切面的Spring

面向切面是Spring又一大核心,本章我们就来详细了解下:面向切面编程的基本原理以及创建使用切面的常用方法,如何为POJO创建切面,使用@AspectJ注释,为AspectJ切面注入依赖。

为什么要AOP
日志、安全和事物管理是软件中很重要的组成部分,但是如果每个应用对象都需要自己处理这些问题,不仅开发者会十分厌烦,而且也不利于维护、复用以及扩展。这些分布于应用多处的功能被称为 横切关注点

依赖注入(DI)是为了应用对象之间的解耦,面向切面(AOP)则是为了横切关注点和应用对象之间的解耦。

1. 什么是面向切面(继承、委托和切面)
AOP也是一种复用的手段,那些横切关注点都是应该与核心功能分离的职责。就复用来说还有继承和委托。
如果使用继承,整个应用系统很多地方都得使用相同的基类,比如在Android开发中Activity和Service它们都有可能用到缓存处理(举个例子,缓存可以和网络请求处理一起放在数据处理层和控制层分离),如果把缓存放到基类中,那么activity和service都要分别创建一个包含该功能的基类,因为很难修改activity和service的基类,它在framework中。因此基于继承的复用在系统级别是很难实现的,应为应用范围越大,系统中可能包含多个角度的划分,你要修改基类会造成很大影响。另外使用继承本身还会造成类数量”爆炸“,使得系统庞大臃肿。

而是用委托可以很大程度上解决,但是委托一般使用组合的方式对接受委托对象进行调用,被委托的服务对委托者来说仍旧是可见的,有时可能需要对委托对象进行复杂的调用,使得类与类之间的联系变得不是很清晰。

切面提供了另一种选择,仍然是在集中的地方定义通用功能,横切关注点可以被模块化为特殊的类,首先每个关注点关注一个问题集中到一处不是分散到多处代码。其次,服务模块更加简洁,只需要关心核心功能,因为如果调用切面服务模块不用关心,交给AOP容器来完成(运行期是这样,编译期和类加载期稍后讨论),很类似依赖倒转,但它解决是行为之间的耦合。

1.1 AOP术语

通知(advice):通知决定切面何时使用,是方法前还是方法后,spring提供了5种选择,Before,After,After-returning(成功后调用),After-throwing(异常后调用),Around(包含方法,前后执行自定义行为);
连接点(join point):在应用中会有很多可以应用通知的时机,调用方法时,抛出异常时,修改字段时,这些都是连接点,切面代码可以利用这些连接点插入添加新的行为;
切点(pointcut):通知定义了“什么”和“何时”,切点就定义了“何处”,通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名来指定切点,定义切点就是选择那些连接点可以织入;
切面(Aspect):切面就是完成的横切关注点的功能,它是切点和通知的结合,最后通知和切点共同决定了是什么、在何时和何处完成其功能;
引入(introduction):向现有类添加新的方法和属性,无需修直接改现有类;
织入(Weaving):将切面应用到目标对象来创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中:
     编译期:AspectJ提供的织入编译器织入切面;
     类加载器:AspectJ提供LTW类加载器织入切面;
     运行期:AOP容器会为目标对象动态地创建一个代理对象织入切面(动态代理,可以参看《Java编程思想》);

1.2 Spring对AOP的支持
Spring提供4种AOP支持(前三种都是基于代理的AOP变体):
(1)基于代理的经典AOP;
(2)@AspectJ注解驱动的切面;
(3)纯POJO切面;
(4)注入式AspectJ切面;

Spring是运行期AOP通知对象的,在调用目标bean方法时,才会创建代理对象。
Spring支持方法连接点,因为Spring基于动态代理所以只支持方法连接点,而AspectJ和Jboss则不同。

2. 使用切点选择连接点
介绍完了aop和spring中的aop后,首先看一看如何编写切点:
Spring借助AspectJ的切点表达式来定义切面,只能使用AspectJ中基于代理的部分,否则会抛出异常。

2.1 编写切点
定义pointcut:execution(* com.springection.springdol.Instrument.play(..));
*表示任意返回类型,play(..)表示任意参数;

execution是主要使用的AspectJ表达式,其他的可以用来限定pointcut的范围。

2.2 bean()指示器
execution(* com.springinaction.springdol.Instrument.play()) and !bean(eddie)
可以指定切点位于ID不为eddie的Instrument类型的Bean上。

3. 在XML中声明切面
通过如下的标签可以定义基于POJO的不同位置的切面声明:
我们可以定义一个简单的Java类
public class Audience {
     public void takeSeats() {...}
     public void turnOffCellPhones(){}
     public void applaud() {...}
     public void demandRefund() {...}
}

接着我们可以定义xml来设置切面的配置
<aop:config>
     <aop:aspect ref="audience">
          <aop:pointcut id="performance" expresstion="【aspectJ表达式】"
          <aop:before pointcut-ref='performance' method="takeSeats" />
          <aop:before pointcut-ref="performance" method="turnOffCellPhones" />
          ...
     </aop:aspect>
</aop:config>
通知的逻辑是如下组织的:
声明环绕通知
你可能想如果前置通知和后置通知如何来共享一些必要信息呢?Audience类是单例的,因此成员变量的方式不能保证线程安全。因此环绕的方式就很适合解决这类问题。
public void watchPerformance(ProceedingJ joinpoint) {
     try {
          //前置功能
          joinpoint.proceed();
          //后置功能
     } catch(Throwable t) {
          //失败后
     }
}
再定义一个<aop:around />就可以实现了,可以看到,切点是通过参数的方式传入,同样可以实现前置、后置和异常时的切面功能,也不用担心信息共享的问题。


为通知传递参数
之前几种方式你可能会发现没没传入额外的参数,当然spring提供传参的功能:
<aop:config>
     <aop:aspect ref="nagician">
          <aop:pointcut id="thinking" expression="...  and args(thougths)" />

          <aop:before pointcut-ref="thinking" method="interceptThoughts"  arg-names="thoughts" />
     </aop:aspect>
</aop:config>

通过切面引入新功能
虽然java不是一个开放式的语言,但我们知道sprin是通过代理的方式实现aop,因此可以通过为代理定义新的方法的方式来增加新的功能。
你可能会问什么要这么做,有什么意义,设想需求更改有时你可能需要需要为每一类及其子类添加新的功能,但是不同的子类可能有不同的实现,因此你不能直接基类,那么你可能会想到增加一些抽象类,这种方式增加了类的数量,另外使用接口,这就要你手动修改很多类,设置扩展jar包中的类的时候,难道不断的继承吗,显示spring作为开发框架就解决这类问题的。
例:
为Performer添加Contestant接口:
public interface Contestant {
     void receiveAward();
}

方式一:
<aop:aspect>
     <aop:declare parents
          types-matching="com.springinaction.springidol.performer+"
          implement-interface="xxx.Contestant"
           default-impl="xxx.ConcreteContestant" /> 具体的实现
</aop:aspect>
方式二:
<aop:aspect>
     <aop:declare parents
          types-matching="com.springinaction.springidol.performer+"
          implement-interface="xxx.Contestant"
           default-refs="contestantDelegate" /> 这里是Bean的ID
</aop:aspect>

这样就可以不修改原有类的代码了,减少了入侵式编程,实现了“开发-封闭”;


4. 注解切面
使用注解可以代替XML定义切面,直接在POJO上添加注解可以实现切面的定义:
@Aspect
public  class  Audiance {
         @Pointcut ( "execution(* com.yjh.example.Performer.performance())"  )
         public  void  performance() {
       }
       
         @Before (  "performance()" )
         public  void  takeSeats() {
              System.  out .println( "[before]take seats"  );
       }
       
         @Before (  "performance()" )
         public  void  turnOffLights() {
              System.  out .println( "[before]turn off lights"  );
       }
}
这样可以通过注解方便的定义切面,不需要再XML中配置aop了。我们已经知道Spring的aop是基于动态代理的,因此需要在使用时为Bean创建代理。
Spring通过创建一个AnnotationAwareAspecJProxyCreator类的Bean,因此要将它注册为Bean,通过它来创建自动代理类。为了简化注册的过程,你只要在配置xml中添加, <aop:aspectj-autoproxy />Spring就知道注册该Bean了。

使用AspectJ注解的优缺点
优点:可以减少XML的量,方便使用;
缺点:对于无法修改源码的类不能应用切面,还得用<aop:aspect>

4.1 注解环绕通知
使用@around可以实现环绕通知:
@Around ( "performance()"  )
public   void  watchPerformance(ProceedingJoinPoint  proccedJoinPoint ) {
                //定义环绕通知,比如统计处理过程的耗时
                try  {
                     System.  out  .println( "start proceeding..."  );
                       long   startTime  = System.currentTimeMillis ();
                     
                       proccedJoinPoint  .proceed();
                     
                       long   endTime  = System.currentTimeMillis ();
                     
                     System.  out  .println( "proceed used: "  + ( endTime  startTime  ) +  "ms in total."  );
              }  catch  (Throwable  e  ) {
                     System.  out  .println( "[ERROR]There is a error caughted in proceeding." );
              }
       }
这个例子展示了计算程序处理过程时间统计的功能,如果不用切面,这样的功能写到业务逻辑中,再反复删掉想想都是一件很麻烦的事。


4.2 注解传递参数给通知
@Aspect
public  class  MindReader {
         private  String  thoughts  ;
       
         @Pointcut ( "execution(* com.yjh.example.Thinker.thinkAboutSomething(String)) && args(thoughts)" )
         public  void  thinkAboutSomething(String  toughts ) {
        }
       
         @Before (  "thinkAboutSomething(thoughts)"  )
         public  void  interceptThought(String  thoughts ) {
                this . thoughts  =  thoughts  ;
        }

         public  String getThoughts() {
                return  thoughts  ;
       }
       
}

4.3 标注引入
之前已经提到过,引入是在不修改源码的情况下给Bean添加新的行为,Spring已经提供了<aop:declared-parents>来实现xml引入。
通过@DeclareParents注解我们也可以实现为Bean添加新行为的过程。
@Aspect
public  class  ContestantIntroducer {
         @DeclareParents (
                     value=  "com.yjh.example.Performer+" ,
                     defaultImpl=ConcreteContestant.  class
                     )
         public  static  Contestant  contestant ;
}

注册 ContestantIntroducer为Bean,<bean class="xxx. ContestantIntroducer " />

注意:使用@DeclareParents并没有和declare-ref属性相对应的注解。因此如果要委托的对象是Spring Bean的话,还是要使用<aop:declare-parents>

5. 注入AspectJ切面
Spring只能提供基于方法的动态代理的AOP,如果你需要更强大的切面功能,比如在创建通知时应用通知,Spring就无能为力了。而你可以使用AspectJ的其他切面功能来完成。

在使用AspectJ切面的时候,Spring可以做什么呢?切面本身也是一个类,它可能也有一些依赖关系,因此使用Spring的依赖注入来解决切面的属性注入问题。

public aspect JudgeAspect {
     public JudgeAspect() {}

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

     after() returning() : performance() {...}
     
     //injected
         private CriticismEngine criticismEngine;
         public void setCriticismEngine(CriticismEngine criticismEngine) {     
               this.criticismEngine = criticismEngine;
          }
}
这就是一个AspectJ切面,接下来可以使用spring来注入依赖:
<bean class="com.springinaction.springdol.JudgeAspect"
     factory-method="aspectOf">
     <property name="critismEngine" ref="criticismEngine" />
</bean>

注意这里使用了factory-method,AspectJ切面是AspectJ在运行期创建的,Spring无法创建,因此应该使用AspectJ切面提供的aspectOf()静态方法作为工厂方法。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值