2017年12月24日 星期日

Java Spring - Spring AOP advice & AspectJ


  • 建立annotated aspects
    • AspectJ5的特性
    • 簡便的通過少量的annotation把任意class轉變為aspect
  • 定義aspect
    • 一場表演,觀眾很重要,但對演出功能本身而言,觀眾並不是必要。因此,在Performance上,觀眾是aspect,並將其應用到Performance
    • @Aspect
      public class Audience {
          @Before("execution(** concert.Performance.perform(..))") //perform()之前
          public void silenceCellPhones() {
              System.out.println("Silencing cell phones");
          }
      
          @Before("execution(** concert.Performance.perform(..))") //perform()之前
          public void takeSeats() {
              System.out.println("Taking seats");
          }
      
          @AfterReturning("execution(** concert.Performance.perform(..))") //perform()成功執行後
          public void applause() {
              System.out.println("CLAP CLAP CLAP!!!");
          }
      
          @AfterThrowing("execution(** concert.Performance.perform(..))") //表演失敗之後
      
          public void demandRefund() {
              System.out.println("Demanding a refund");
          }
      }
      
    • 以上的方法表示Audience不只是一個POJO,也是一個aspect
    • 使用annotation定義aspect的具體行為
      • 上例有個缺點,"execution(** concert.Performance.perform(..))"使用了四次。因此可以使用@Pointcut避免頻繁地使用aspect expression
      • @Aspect
        public class Audience {
            @Pointcut("execution(** concert.Performance.perform(..))") //使用@Pointcut定義重複的aspect expression
            public void performance() {} //接著就可以使用這個縮寫,簡略那些更長的aspect expression,method內容通常為空
        
            @Before("performance()") // declare advice method
            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");
            }
        }
        
      • 上面的Audience除了annotation及空的method,依然是一個一般的POJO。也就是Audience只是一個Java class,只不過透過annotation表明會作為一個aspect使用而已
      • 也可以像是bean一樣被wired
      • @Bean
        public Audience audience() {
            return new Audience();
        }
        
    • 使用JavaConfig開啟auto-proxy
    • @Configuration
      @EnableAspectJAutoProxy
      @ComponentScan
      public class ConcertConfig {
          @Bean
          public Audience audience() {
              return new Audience();
          }
      }
      
    • 使用XML開啟auto-proxy
    • <context:component-scan base-package="concert" />
      <aop:aspectj-autoproxy />
      <bean class="concert.Audience" />
      
    • 不論使用JavaConfig或是XML,AspectJ auto-proxy都會為使用@Aspect annotation的bean建立proxy
    • 在Spring內使用@AspectJ annotation,如果想要使用AspectJ的所有功能,則需要在runtime的時候使用AspectJ而且不依賴於Spring create 基於agent的aspect
  • Around advice
    • 就像是同時寫了before advice&after advice
    • 使用一個@Around來代替上述的多個不同before advice&after advice
    • @Aspect
      public class Audience {
          @Pointcut("execution(** concert.Performance.perform(..))")
          public void performance() {}
      
          @Around("performance()")
          public void watchPerformance(ProceedingJoinPoint jp) {
              try {
                  System.out.println("Silencing cell phones");
                  System.out.println("Taking seats");
                  jp.proceed();
                  System.out.println("CLAP CLAP CLAP!!!");
              } catch (Throwable e) {
                  System.out.println("Demanding a refund");
              }
          }
      }
      
    • 可以注意到,此@Around method接受ProceedingJoinPoint作為parameter
    • 在執行被advice的method的時候,可以invoke proceed() method
    • 可以多次invoke proceed(),也可以不invoke
  • 在advice中處理parameters
    • 問題:如果aspect所通知的method有parameters時,是否能夠取得此parameter?
    • 範例:假設需要紀錄track被播放的次數如下:
    • @Aspect
      public class TrackCounter {
          private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
      
          @Pointcut("execution(* soundsystem.CompactDisc.playTrack(int)) " + "&& args(trackNumber)")
          public void trackPlayed(int trackNumber) {}
      
          @Before("trackPlayed(trackNumber)") // 播放之前紀錄次數
          public void countTrack(int trackNumber) { // 擷取傳至trackPlayed的trackNumber
              int currentCount = getPlayCount(trackNumber);
              trackCounts.put(trackNumber, currentCount + 1);
          }
      
        public int getPlayCount(int trackNumber) {
          return trackCounts.containsKey(trackNumber)
        }
      }
      
    • args(trackNumber): 指定parameters,這個parameter將傳到method中
    • 表示傳至playTrack()的parameter也會傳到advice中
  • 透過annotation引入新的功能
    • 使用AOP觀念(稱為introduction,引入),aspect可以為Spring bean添加新的功能
    • 假設現有的interface: Performance
    • public interface Performance {
        public void perform();
      }
      
    • 以及現有一新的interface: Encoreable
    • package concert;
      public interface Encoreable {
          void performEncore();
      }
      
    • 假設能夠visit所有implements Performance的class並且能夠修改他們讓他們也能夠implements Encoreable。但這不是個好方法,因為不是所有implements Performance的class都具有Encoreable的特性。也有可能並無法修改所有Performance的 implementation classes(比如使用第三方package)
    • 解法:使用AOP的intrudoction,並不需要在設計上使用新入性的改變現有implementation classes,而只需要create一個新的aspect
    • @Aspect
      public class EncoreableIntroducer {
        @DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
        public static Encoreable encoreable;
      }
      
      • 透過@DeclareParents能夠將Encoreable interface introduce to Performance bean
      • @DeclareParents
        1. value=哪種bean要introduce至此interface (此為concert.Performance的所有subclass(+),不含Performance本身)
        2. defualtImpl: 為了introduce新功能提供implementation,此為DefaultEncoreable
        3. static property指明了要introduce的interface
    • 與其他aspect相同,EncoreableIntroducer也需要被宣告為bean
    • 使用這種方法的缺點是,必須要能夠在原本的class修改,增加annotatino。若不能則可以考慮使用XML
  • 在XML中宣告aspects
    • Spring的aop namespace提供了很多element在XML中宣告aspect
    • aspect namespace能夠直接在Spring config中聲明@aspect,而不需要使用annotation
      • <aop:advisor>
      • <aop:after>
      • <aop:after-returning>
      • <aop:after-throwing>
      • <aop:around>
      • <aop:aspectj-autoporxy>
      • <aop:before>
      • <aop:config>
      • <aop:declare-parents>
      • <aop:pointcut>
    • 假設要使用XML配置,則原本的Audience可以改成如下:
    • 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");
          }
      }
      
    • 使用Spring aop namespace將沒有annotation的Audience class轉回aspect
    • 將audience先註冊為bean,之後..
    •   <aop:config>
          <aop:aspect ref="audience"> // 使用了audience Bean,declare一個aspect
            <aop:before pointcut="execution(** concert.Performance.perform(..))"
              method="silenceCellPhones" />
            <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>
      
      • 必須在<aop:config>之間
      • <aop:config>中可以配置多個advice、pointcut、aspects
      • Audience aspect包含四個advices,將advice logic weave(織入)匹配aspect's pointcut的method中
      • pointcut定義advice什麼時候會被執行
      • pointcut內的值就是使用AspectJ的expression syntax
      • 也可以使用<aop:pointcut>將重複的pointcut提出,這樣就可以在其他advice element中使用
      •   <aop:config>
            <aop:aspect ref="audience">
              <aop:pointcut id="performance"
                expression="execution(** concert.Performance.perform(..))" /> // 定義pointcut
              <aop:before pointcut-ref="performance" method="silenceCellPhones" />
              <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>
        
      • <aop:pointcut>定義一個id為performance的pointcut
      • 下面即可以使用pointcut-ref引用這個named pointcut
    • 宣告around-advice
      •   <aop:config>
            <aop:aspect ref="audience">
              <aop:pointcut id="performance"
                expression="execution(** concert.Performance.perform(..))" />
              <aop:around pointcut-ref="performance" method="watchPerformance" /> // 在watchPerformance內定義around-advice
            </aop:aspect>
          </aop:config>
        
    • Passing parameters to advice
      • 以下範例,可看到多了args(trackNumber),這個參數會傳到advice method中,和上面使用annotation使用的expression幾乎相等。除了XML使用的是and而不是&&
      •   <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>
                <!-- ...other tracks omitted for brevity... -->
              </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>
        
      • 以下為搭配的POJO
      • public class TrackCounter {
            private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
        
            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;
            }
        }
        
    • Introducing new functionality to aspects
      • 可以使用@DeclareParents annotation introduce新的方法
      • 若要在XML中,可以使用<aop:declare-parents> element
      • <aop:aspect>
          <aop:declare-parents
            types-matching="concert.Performance+"
            implement-interface="concert.Encoreable" // 增加concert.Encoreable interface功能
            default-impl="concert.DefaultEncoreable"
            />
        </aop:aspect>
        
      • introduce interface的implemtation除了使用default-imp指定implement Encoreable的class(要寫完整的class名字),也可以使用delegate-ref attribute
      •   <aop:aspect>
            <aop:declare-parents types-matching="concert.Performance+"
              implement-interface="concert.Encoreable" delegate-ref="encoreableDelegate" />
          </aop:aspect>
        
      • delegate-ref使用了Spring bean的ref,這需要在Spring上的context存在一個ID是encoreableDelegate的bean
      •  <bean id="encoreableDelegate" class="concert.DefaultEncoreable" />
        
  • Injecting AspectJ aspects
    • 和AspectJ相比,Spring AOP是一個功能比較弱的AOP解決方案
    • 如果在執行advice時,aspcet依賴一個或多個class,可以在aspect內部時體會這些對象。但更好的方式是,使用Spring的DI把bean wire進入AspectJ的aspect中
    • 範例:使用AspectJ implement a performance評論員
      • package concert;
        public aspect CriticAspect {
         public CriticAspect() {}
        
         pointcut performance(): execution( * perform(..)); //定義pointcut
        
         afterReturning(): performance() { //表演結束後執行getCriticism()
          System.out.println(criticismEngine.getCriticism());
         }
         private CriticismEngine criticismEngine; //injects criticismEngine,為了避免CriticAspect與CriticismEngine有coupling
         public void setCriticismEngine(CriticismEngine criticismEngine) {
          this.criticismEngine = criticismEngine;
         }
        }
        
      • 主要功能:在表演結束後為表演發表評論
      • aspect也需要injects。就像其他bean依樣,Spring可以為AspectJ aspect injects dependency 
      • criticismEngine的implementation:
      • package com.springinaction.springidol;
        
        public class CriticismEngineImpl implements CriticismEngine {
            public CriticismEngineImpl() {}
        
            public String getCriticism() { //隨機從criticismPool選擇一個評論並傳回
                int i = (int) (Math.random() * criticismPool.length);
                return criticismPool[i];
            }
        
            // 從XML injected
            private String[] criticismPool;
        
            public void setCriticismPool(String[] criticismPool) {
                this.criticismPool = criticismPool;
            }
        }
        
      • XML:
      •   <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>
        
      • AspectJ不需要Spring就可以weave到程式中。如果想要使用Spring配置將依賴注入於AspectJ aspect中,則需要將aspect declare為bean
      • <bean class="com.springinaction.springidol.CriticAspect" factory-method="aspectOf">
          <property name="criticismEngine" ref="criticismEngine" />
         </bean>
        
      • 一般來說,Spring bean由Spring container initialize,但是AspectJ aspect是由AspectJ在runtime created。等到Spring有機會為CriticAspect injects CriticismEngine時,CriticAspect 已經被initial了
      • 因為Spring不是那個負責建立CriticAspect的人,也因此不能夠把CriticAspect宣告為一個bean。相反的,我們需要一個方式替Spring獲得已經由AspectJ建立的CriticAspect  instance,從而injects CriticismEngine 
      • 所有的AspectJ aspect其實都提供一個static aspectOf() method,可以return aspect的singleton
      • 因此為了取得aspect的instance,必須使用factory-method="aspectOf",而不是試著呼叫CriticAspect的constructor
      • 簡而言之,Spring不能像之前使用<bean>來創造一個CriticAspect 的instance,因為在runtime的時候已經被AspectJ建立完成了。Spring只需要透過factory-method="aspectOf"來取得aspect ref,然後像<bean>規定的那樣在object上執行DI


沒有留言:

張貼留言