乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      [AOP] 2. AOP的兩種實現(xiàn)-Spring AOP以及AspectJ

       沙門空海 2017-12-13

      在接觸Spring以及種類繁多的Java框架時,很多開發(fā)人員(至少包括我)都會覺得注解是個很奇妙的存在,為什么加上了@Transactional之后,方法會在一個事務(wù)的上下文中被執(zhí)行呢?為什么加上了@Cacheable之后,方法的返回值會被記錄到緩存中,從而讓下次的重復(fù)調(diào)用能夠直接利用緩存的結(jié)果呢?

      隨著對AOP的逐漸應(yīng)用和了解,才明白注解只是一個表象,在幕后Spring AOP/AspectJ做了大量的工作才得以實現(xiàn)這些神奇的功能。

      那么,本文就來聊一聊Spring AOP和AspectJ的那些事,它們究竟有什么魔力才讓這一切成為現(xiàn)實。

      Spring AOP

      基于代理(Proxy)的AOP實現(xiàn)

      首先,這是一種基于代理(Proxy)的實現(xiàn)方式。下面這張圖很好地表達(dá)了這層關(guān)系:

      這張圖反映了參與到AOP過程中的幾個關(guān)鍵組件(以@Before Advice為例):

      1. 調(diào)用者Beans - 即調(diào)用發(fā)起者,它只知道目標(biāo)方法所在Bean,并不清楚代理以及Advice的存在
      2. 目標(biāo)方法所在Bean - 被調(diào)用的目標(biāo)方法
      3. 生成的代理 - 由Spring AOP為目標(biāo)方法所在Bean生成的一個代理對象
      4. Advice - 切面的執(zhí)行邏輯

      它們之間的調(diào)用先后次序反映在上圖的序號中:

      1. 調(diào)用者Bean嘗試調(diào)用目標(biāo)方法,但是被生成的代理截了胡
      2. 代理根據(jù)Advice的種類(本例中是@Before Advice),對Advice首先進(jìn)行調(diào)用
      3. 代理調(diào)用目標(biāo)方法
      4. 返回調(diào)用結(jié)果給調(diào)用者Bean(由代理返回,沒有體現(xiàn)在圖中)

      為了理解清楚這張圖的意思和代理在中間扮演的角色,不妨看看下面的代碼:

      @Component
      public class SampleBean {
      
        public void advicedMethod() {
      
        }
      
        public void invokeAdvicedMethod() {
          advicedMethod();
        }
      
      }
      
      @Aspect
      @Component
      public class SampleAspect {
      
        @Before("execution(void advicedMethod())")
        public void logException() {
          System.out.println("Aspect被調(diào)用了");
        }
      
      }
      
      sampleBean.invokeAdvicedMethod(); // 會打印出 "Aspect被調(diào)用了" 嗎?
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25

      SampleBean扮演的就是目標(biāo)方法所在Bean的角色,而SampleAspect扮演的則是Advice的角色。很顯然,被AOP修飾過的方法是advicedMethod(),而非invokeAdvicedMethod()。然而,invokeAdvicedMethod()方法在內(nèi)部調(diào)用了advicedMethod()。那么會打印出來Advice中的輸出嗎?

      答案是不會

      如果想不通為什么會這樣,不妨再去仔細(xì)看看上面的示意圖。

      這是在使用Spring AOP的時候可能會遇到的一個問題。類似這種間接調(diào)用不會觸發(fā)Advice的原因在于調(diào)用發(fā)生在目標(biāo)方法所在Bean的內(nèi)部,和外面的代理對象可是沒有半毛錢的關(guān)系哦。我們可以把這個代理想象成一個中介,只有它知道Advice的存在,調(diào)用者Bean和目標(biāo)方法所在Bean知道彼此的存在,但是對于代理或者是Advice卻是一無所知的。因此,沒有通過代理的調(diào)用是絕無可能觸發(fā)Advice的邏輯的。如下圖所示:

      Spring AOP的兩種實現(xiàn)方式

      Spring AOP有兩種實現(xiàn)方式:

      • 基于接口的動態(tài)代理(Dynamic Proxy)
      • 基于子類化的CGLIB代理

      我們在使用Spring AOP的時候,一般是不需要選擇具體的實現(xiàn)方式的。Spring AOP能根據(jù)上下文環(huán)境幫助我們選擇一種合適的。那么是不是每次都能夠這么”智能”地選擇出來呢?也不盡然,下面的例子就反映了這個問題:

      @Component
      public class SampleBean implements SampleInterface {
      
        public void advicedMethod() {
      
        }
      
        public void invokeAdvicedMethod() {
          advicedMethod();
        }
      
      }
      
      public interface SampleInterface {}
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

      在上述代碼中,我們?yōu)樵瓉淼腂ean實現(xiàn)了一個新的接口SampleInterface,這個接口中并沒有定義任何方法。這個時候,再次運行相關(guān)測試代碼的時候就會出現(xiàn)異常(摘錄了部分異常信息):

      org.springframework.beans.factory.BeanCreationException: 
      Error creating bean with name 'com.destiny1020.SampleBeanTest': 
      Injection of autowired dependencies failedCaused by: 
      org.springframework.beans.factory.NoSuchBeanDefinitionException: 
      No qualifying bean of type [com.destiny1020.SampleBean] found for dependency: 
      expected at least 1 bean which qualifies as autowire candidate for this dependency. 
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      也就是說在Test類中對于Bean的Autowiring失敗了,原因是創(chuàng)建SampleBeanTest Bean的時候發(fā)生了異常。那么為什么會出現(xiàn)創(chuàng)建Bean的異常呢?從異常信息來看并不明顯,實際上這個問題的根源在于Spring AOP在創(chuàng)建代理的時候出現(xiàn)了問題。

      這個問題的根源可以在這里得到一些線索:

      Spring AOP Reference - AOP Proxies

      文檔中是這樣描述的(每段后加上了翻譯):

      Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

      Spring AOP默認(rèn)使用標(biāo)準(zhǔn)的JDK動態(tài)代理來實現(xiàn)AOP代理。這能使任何借口(或者一組接口)被代理。

      Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.

      Spring AOP也使用CGLIB代理。對于代理classes而非接口這是必要的。如果一個業(yè)務(wù)對象沒有實現(xiàn)任何接口,那么默認(rèn)會使用CGLIB。由于面向接口而非面向classes編程是一個良好的實踐;業(yè)務(wù)對象通常都會實現(xiàn)一個或者多個業(yè)務(wù)接口。強制使用CGLIB也是可能的(希望這種情況很少),此時你需要advise的方法沒有被定義在接口中,或者你需要向方法中傳入一個具體的對象作為代理對象。

      因此,上面異常的原因在于:

      強制使用CGLIB也是可能的(希望這種情況很少),此時你需要advise的方法沒有被定義在接口中。

      我們需要advise的方法是SampleBean中的advicedMethod方法。而在添加接口后,這個方法并沒有被定義在該接口中。所以正如文檔所言,我們需要強制使用CGLIB來避免這個問題。

      強制使用CGLIB很簡單:

      @Configuration
      @EnableAspectJAutoProxy(proxyTargetClass = true)
      @ComponentScan(basePackages = "com.destiny1020")
      public class CommonConfiguration {}
      • 1
      • 2
      • 3
      • 4

      @EnableAspectJAutoProxy注解中添加屬性proxyTargetClass = true即可。
      CGLIB實現(xiàn)AOP代理的原理是通過動態(tài)地創(chuàng)建一個目標(biāo)Bean的子類來實現(xiàn)的,該子類的實例就是AOP代理,它建立起了目標(biāo)Bean到Advice的聯(lián)系。

      當(dāng)然還有另外一種解決方案,那就是將方法定義聲明在新創(chuàng)建的接口中并且去掉之前添加的proxyTargetClass = true

      @Component
      public class SampleBean implements SampleInterface {
      
        @Override
        public void advicedMethod() {
      
        }
      
        @Override
        public void invokeAdvicedMethod() {
          advicedMethod();
        }
      
      }
      
      public interface SampleInterface {
      
        void invokeAdvicedMethod();
      
        void advicedMethod();
      
      }
      
      @Configuration
      @EnableAspectJAutoProxy
      @ComponentScan(basePackages = "com.destiny1020")
      public class CommonConfiguration {}
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

      這樣就讓業(yè)務(wù)對象實現(xiàn)了一個接口,從而能夠使用基于標(biāo)準(zhǔn)JDK的動態(tài)代理來完成Spring AOP代理對象的創(chuàng)建。

      從Debug Stacktrace的角度也可以看出這兩種AOP實現(xiàn)方式上的區(qū)別:

      • JDK動態(tài)代理

      • CGLIB

      關(guān)于動態(tài)代理和CGLIB這兩種方式的簡要總結(jié)如下:

      • JDK動態(tài)代理(Dynamic Proxy)

        • 基于標(biāo)準(zhǔn)JDK的動態(tài)代理功能
        • 只針對實現(xiàn)了接口的業(yè)務(wù)對象
      • CGLIB

        • 通過動態(tài)地對目標(biāo)對象進(jìn)行子類化來實現(xiàn)AOP代理,上面截圖中的SampleBean$$EnhancerByCGLIB$$1767dd4b即為動態(tài)創(chuàng)建的一個子類
        • 需要指定@EnableAspectJAutoProxy(proxyTargetClass = true)來強制使用
        • 當(dāng)業(yè)務(wù)對象沒有實現(xiàn)任何接口的時候默認(rèn)會選擇CGLIB

      AspectJ

      AspectJ是Eclipse旗下的一個項目。至于它和Spring AOP的關(guān)系,不妨可將Spring AOP看成是Spring這個龐大的集成框架為了集成AspectJ而出現(xiàn)的一個模塊。

      畢竟很多地方都是直接用到AspectJ里面的代碼。典型的比如@Aspect,@Around,@Pointcut注解等等。而且從相關(guān)概念以及語法結(jié)構(gòu)上而言,兩者其實非常非常相似。比如Pointcut的表達(dá)式語法以及Advice的種類,都是一樣一樣的。

      那么,它們的區(qū)別在哪里呢?

      最大的區(qū)別在于兩者實現(xiàn)AOP的底層原理不太一樣:

      • Spring AOP: 基于代理(Proxying)
      • AspectJ: 基于字節(jié)碼操作(Bytecode Manipulation)

      用一張圖來表示AspectJ使用的字節(jié)碼操作,就一目了然了:

      通過編織階段(Weaving Phase),對目標(biāo)Java類型的字節(jié)碼進(jìn)行操作,將需要的Advice邏輯給編織進(jìn)去,形成新的字節(jié)碼。畢竟JVM執(zhí)行的都是Java源代碼編譯后得到的字節(jié)碼,所以AspectJ相當(dāng)于在這個過程中做了一點手腳,讓Advice能夠參與進(jìn)來。

      而編織階段可以有兩個選擇,分別是加載時編織(也可以成為運行時編織)和編譯時編織:

      加載時編織(Load-Time Weaving)

      顧名思義,這種編織方式是在JVM加載類的時候完成的。

      使用它需要進(jìn)行相關(guān)的配置,舉例如下:

      在類路徑的META-INF目錄下創(chuàng)建一個文件名為aop.xml:

      <aspectj>
        <weaver>
          <include within="com.destiny1020..*" />
        </weaver>
        <aspects>
          <aspect name="com.destiny1020.SampleAspect" />
        </aspects>
      </aspectj>
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      然后添加啟動參數(shù),直接使用AspectJ提供的或者使用Spring提供的工具:

      # AspectJ
      -javaagent:path_to/aspectjweaver-{version}.jar
      
      # Spring
      -javaagent:path_to/org.springframework.instrument-{version}.jar
      • 1
      • 2
      • 3
      • 4
      • 5

      當(dāng)使用Spring提供的工具時,還需要進(jìn)行一些配置,以JavaConfig為例:

      @Configuration
      @EnableLoadTimeWeaving
      @ComponentScan(basePackages = "com.destiny1020")
      public class CommonConfiguration {}
      • 1
      • 2
      • 3
      • 4

      重點就是上述的@EnableLoadTimeWeaving

      編譯時編織(Compile-Time Weaving)

      需要使用AspectJ的編譯器來替換JDK的編譯器??梢越柚鶰aven AspectJ來實現(xiàn),下面是一例:

      <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>aspectj-maven-plugin</artifactId>
          <version>1.4</version>
          <dependencies>
              <dependency>
                  <groupId>org.aspectj</groupId>
                  <artifactId>aspectjrt</artifactId>
                  <version>${aspectj.version}</version>
              </dependency>
              <dependency>
                  <groupId>org.aspectj</groupId>
                  <artifactId>aspectjtools</artifactId>
                  <version>${aspectj.version}</version>
              </dependency>
          </dependencies>
          <executions>
              <execution>
                  <phase>process-sources</phase>
                  <goals>
                      <goal>compile</goal>
                      <goal>test-compile</goal>
                  </goals>
              </execution>
          </executions>
          <configuration>
              <outxml>true</outxml>
              <source>${java.version}</source>
              <target>${java.version}</target>
          </configuration>
      </plugin>
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31

      然后直接通過mvn test進(jìn)行測試:

      自定義的編譯錯誤/警告

      舉個例子,有兩個Service1和Service2分別位于兩個包Package1和Package2下,只能在Package2中調(diào)用來自本包內(nèi)部的方法,在Service1中調(diào)用Service2中提供的方法會導(dǎo)致編譯錯誤(能夠用訪問控制符解決的問題強行用這種方式來解決,當(dāng)然只是為了說明問題:)):

      @Aspect
      public class EmitCompilationErrorAspect {
      
        @DeclareError("call (* com.destiny1020.biz.package2..*.*(..))"
            + "&& !within(com.destiny1020.biz.package2..*)")
        public static final String notInBizPackage2 = "只能在Package2中調(diào)用來自Package2的方法";
      
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      package com.destiny1020.biz.package1;
      
      import com.destiny1020.biz.package2.ServiceInPackage2;
      
      public class ServiceInPackage1 {
      
        ServiceInPackage2 service2 = new ServiceInPackage2();
      
        public void invokeMethodInPackage2() {
          service2.doBizInPackage2();  // 這里理應(yīng)會出現(xiàn)編譯錯誤
        }
      
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

      實際情況也正式如此:

      在聲明編譯錯誤Pointcut的時候,出現(xiàn)了兩個新概念:

      • call
      • within

      這兩個新出現(xiàn)的Pointcut原語只能在使用AspectJ作為AOP實現(xiàn)的時候才可用。它們表達(dá)的是什么意思呢:

      • call:針對所有的調(diào)用者(caller),即哪里調(diào)用了Pointcut表達(dá)式匹配的方法,在該方法被執(zhí)行之前就會被匹配到;而我們經(jīng)常使用的execution則是針對所有的被調(diào)用方法,而不會care是誰調(diào)用的該方法
      • within:這個很好理解,它的Pointcut表達(dá)式是一個用來匹配完整限定類名的表達(dá)式,比如上例中的!within(com.destiny1020.biz.package2..*)意味不在包com.destiny1020.biz.package2中的類。

      在使用AspectJ的編譯時編織功能時,由于使用了AspectJ Compiler來完成代碼的編譯,因此可以根據(jù)編碼規(guī)范添加相應(yīng)的編譯錯誤/警告,來進(jìn)一步地讓代碼更加規(guī)范。這個特性對于輔助實現(xiàn)大型項目的編碼規(guī)范還是很有益處的。

      哪種方式更好

      先下結(jié)論:It Depends.

      得根據(jù)具體需求,不過我個人認(rèn)為在對AOP的需求不那么深入和迫切的時候,使用Spring AOP足矣。

      畢竟Spring作為一個以集成起家的框架,在設(shè)計Spring AOP的時候也是為了減輕開發(fā)人員負(fù)擔(dān)而做了不少努力的。它提供的開箱即用(Out-of-the-box)的眾多AOP功能讓很多開發(fā)人員甚至都不知道什么是AOP,就算知道了AOP是Spring的一大基石或者@Transactional和@Cacheable等等常用注解是借助了AOP的力量,但是再深入恐怕就有點勉為其難了。這是優(yōu)點也是缺點,當(dāng)需要對AOP的實現(xiàn)做出精細(xì)化調(diào)整的時候,就會有力不從心的感覺。

      這個時候,就可以考慮使用AspectJ。AspectJ的功能更加全面和強大。支持全部的Pointcut類型。

      這里進(jìn)行了一個簡單的比較,摘錄并簡單翻譯(括號內(nèi)是我添加的補充)如下:

      Spring-AOP Pros

      • 比AspectJ更簡單,不需要使用Load-Time Weaving以及AspectJ編譯器(為了Compile-Time Weaving)
      • 當(dāng)使用@Aspect注解時可以很方便的遷移到AspectJ AOP實現(xiàn)
      • 使用代理模式和裝飾模式

      Spring-AOP Cons

      • 由于是基于代理的AOP,所以基本上只能選擇方法execution這一個Pointcut原語
      • 在類本身中調(diào)用另一個方法的時候Aspects不會生效
      • 有一點運行時的額外開銷
      • 無法為不是從Spring Factory中創(chuàng)建的對象添加Aspect(只對Spring Bean有效)

      AspectJ Pros

      • 支持所有的Pointcut原語,這意味著你可以做任何事情
      • 運行時開銷比Spring AOP少
      • 能夠添加各種編譯錯誤來保障代碼質(zhì)量(這一條是我自己添加的)

      AspectJ Cons

      • 當(dāng)心。檢查是否發(fā)生了意料之外的Weaving操作
      • 使用Compile-Time Weaving時需要額外的構(gòu)建步驟(使用AspectJ Compiler),在使用Load-Time Weaving時需要一些配置(-javaassist)

      參考資料

      Spring AOP Reference

      AspectJ Quick Reference

        本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多