AOP 你看這一篇就夠了

網上不少人在介紹AOP時都這樣說:面向切面編程,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。我的認爲這句話是錯誤。AOP和OOP同樣,是一種程序設計思想,而非技術手段。spring

程序設計有六大原則,其中第一原則就是 單一職責原則 。意思就是一個類只負責一件事情。這與OOP的封裝特性相得益彰。在這個條件下,咱們的程序會被分散到不一樣的類、不一樣的方法中去。這樣作的好處是下降了類的複雜性,提升了程序的可維護性。可是同時,它也使代碼變得囉嗦了。例如,咱們要爲方法添加調用日誌,那就必須爲全部類的全部方法添加日誌調用,儘管它們都是相同的。爲了解決上述問題,AOP應運而生了。express

AOP旨在將 橫切關注點 與業務主體進行分類,從而提升程序代碼的模塊化程度。橫切關注點是一個抽象的概念,它是指那些在項目中貫穿多個模塊的業務。上個例子中日誌功能就是一個典型的橫切關注點。編程

AOP的幾種實現方式設計模式

動態代理安全

動態代理是一種設計模式。它有如下特徵:性能優化

  • 咱們不須要本身寫代理類。
  • 運行期經過接口直接生成代理對象。
  • 運行期間才肯定代理哪一個對象。

如下面這個例子爲例,咱們看一下動態代理的類圖結構。bash

一般咱們的APP都有一部分功能要求用戶登陸以後才能訪問。如修改密碼、修改用戶名等功能。當用戶打算使用這些功能時,咱們通常要對用戶的登陸狀態進行判斷,只有用戶登陸了,才能正常使用這些功能。而若是用戶未登陸,咱們的APP要跳轉到登陸頁。就以修改密碼爲例咱們看一下動態代理的類圖。架構

AOP 你看這一篇就夠了

InvocationHandler是Java JDK提供的動態代理的入口,用來對被代理對象的方法作處理。併發

代碼以下:app

public static class LoginCheckHandler implements InvocationHandler { private static <S, T extends S> T proxy(S source, Class<T> tClass) { return (T) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{tClass}, new LoginCheckHandler(source)); }  private Object mSource; LoginCheckHandler(Object source) { this.mSource = source; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(!checkLogin()){ jumpToLoginActivity(); return null; } return method.invoke(mSource, args); } private boolean checkLogin(){ System.out.println("用戶未登陸"); return false; } private void jumpToLoginActivity(){ System.out.println("跳轉到登陸頁"); } } public class Client {  public static void main(String[] args) { IUserSetting source = new UserSetting(); IUserSetting iUserSetting = LoginCheckHandler.proxy(source,IUserSetting.class); iUserSetting.changePwd("new Password"); } }複製代碼複製代碼

通過這樣封裝以後,檢查登陸跳轉登陸頁的邏輯做爲 橫切關注點 就和業務主體進行了分離。當有新的需求須要登陸檢查時,咱們只須要經過LoginCheckHandler生成新的代理對象便可。

APT

APT(Annotation Processing Tool)是一種編譯期註解處理技術。它經過定義註解和處理器來實現編譯期生成代碼的功能,而且將生成的代碼和源代碼一塊兒編譯成.class文件。經過APT技術,咱們將 橫切關注點 封裝到註解處理器中,從而實現 橫切關注點 與業務主體的分離。更詳細的介紹請移步

Android編譯期插樁,讓程序本身寫代碼(一)

AspectJ

AspectJ就是一種編譯器,它在Java編譯器的基礎上增長了關鍵字識別和編譯方法。所以,AspectJ能夠編譯Java代碼。它還提供了Aspect程序。在編譯期間,將開發者編寫的Aspect程序織入到目標程序中,擴展目標程序的功能。開發者經過編寫AspectJ程序實現AOP功能。更詳細的介紹請移步

Android編譯期插樁,讓程序本身寫代碼(二)

Transform + Javassist/ASM

Transform是Android Gradle提供的,能夠操做字節碼的一種方式。App編譯時,源代碼首先會被編譯成class,而後再被編譯成dex。在class編譯成dex的過程當中,會通過一系列 Transform 處理。 Javassist/ASM 是一個可以很是方便操做字節碼的庫。咱們經過它們能夠修改編譯的.class文件。

橫切關注點

影響應用多處的功能(日誌、事務、安全)

加強(Advice)

加強定義了切面要完成的功能以及何時執行這個功能。

Spring 切面能夠應用 5 種類型的加強:

  • 前置加強(Before) 在目標方法被調用前調用加強功能
  • 後置加強(After) 在目標方法完成以後調用加強, 不關注方法輸出是什麼
  • 返回加強(After-returning) 在目標方法成功執行以後調用加強
  • 異常加強(After-throwing) 在目標方法拋出異常後調用加強
  • 環繞加強(Around) 在被加強的方法調用以前和調用以後執行自定義行爲,即包括前置加強和後置加強。

鏈接點(Join Point)

應用中每個有可能會被加強的點被稱爲鏈接點。

切點(Pointcut)

切點是規則匹配出來的鏈接點。

切面(Aspect)

切面是加強和切點的結合,定義了在什麼時候和何處完成其功能。

引入(Introduction)

引入容許咱們向現有的類中添加新方法和屬性。能夠在不修改現有的類的狀況下,讓類具備新的行爲和狀態。

織入(Weaving)

織入是把切面應用到目標對象中並建立新的代理對象的過程。在目標對象的生命週期裏有多個點能夠進行織入:

  • 編譯器:切面在目標類編譯時織入。這種方式須要特殊的編譯器。AspectJ 的織入編譯器就是以這種方式織入切面的。
  • 類加載器:切面在目標類加載到 JVM 時被織入。這種方式須要特殊的類加載器(ClassLoader),它能夠在目標類被引入應用以前加強該目標類的字節碼。AspectJ5 的加載時織入(LTW)支持以這種方式織入。
  • 運行期:切面在應用運行時的某個時刻被織入。通常狀況下,在織入切面時,AOP 容器會爲目標對象動態地建立一個代理對象。Spring AOP 就是以這種方式織入切面的。

Spring 對 AOP 的支持

Spring 對 AOP 的支持在不少方面借鑑了 AspectJ 項目。目前 Spring 提供了 4 種類型的 AOP 支持:

  • 基於代理的經典 AOP
  • 純 POJO 切面
  • @AspectJ 註解驅動的切面
  • 注入式 AspectJ 切面

Spring AOP 構建在動態代理基礎之上,所以 Spring 對 AOP 的支持侷限於方法攔截。

運行時加強

經過在代理中包裹切面,Spring 在運行期把切面織入到 Spring 管理的 bean 中。代理類封裝了目標類,並攔截被加強方法的調用,再把調用轉發給真正的目標 bean。在代理攔截到方法調用時,在調用目標 bean 方法以前,會執行切面邏輯。

直到應用須要代理的 bean 時,Spring 才建立代理對象。若是使用 ApplicationContext 的話,在 ApplicationContext 從 BeanFactory 中加載全部 bean 的時候,Spring 纔會建立被代理的對象。

方法級別的鏈接點

Spring 基於動態代理實現 AOP,因此 Spring 只支持方法鏈接點。其餘的 AOP 框架好比 AspectJ 與 JBoss,都提供了字段和構造器接入點,容許建立細粒度的加強。

切點表達式

Spring AOP 中,使用 AspectJ 的切點表達式來定義切點。Spring 只支持 AspectJ 切點指示器(pointcut designator)的一個子集。

指示器

AspectJ 指示器描述arg( )限制鏈接點匹配參數爲指定類型的執行方法execution( )用於匹配鏈接點this指定匹配 AOP 代理的 bean 引用的類型target指定匹配對象爲特定的類within( )指定鏈接點匹配的類型@annotation匹配帶有指定註解的鏈接點

編寫切點

package concert;public interface Performance { public void perform();}複製代碼複製代碼

Performance 類能夠表明任何類型的現場表演,好比電影、舞臺劇等。如今編寫一個切點表達式來限定 perform() 方法執行時觸發的加強。

execution(* concert.Performance.perform(..))複製代碼複製代碼

每一個部分的意義以下圖所示:

AOP 你看這一篇就夠了

也能夠引入其餘註解對匹配規則作進一步限制。好比

execution(* concert.Performance.perform(..)) && within(concert.*)複製代碼複製代碼

within() 指示器限制了切點僅匹配 concert 包。

Spring 還有一個 bean() 指示器,容許咱們在切點表達式中使用 bean 的 ID 表示 bean。

execution(* concert.Performance.perform(..)) && bean('woodstock')複製代碼複製代碼

以上的切點就表示限定切點的 bean 的 ID 爲 woodstock 。

給本身的Java技術交流羣打波廣告吧,想要學習Java架構技術的朋友能夠加個人羣: 710373545,羣內每晚都會有阿里技術大牛講解的最新Java架構技術。並會錄製錄播視頻分享在羣公告中,做爲給廣大朋友的加羣的福利——分佈式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高併發、高可用架構)/微服務(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優化(JVM、TomCat、MySQL)

使用註解建立切面

定義切面

在一場演出以前,咱們須要讓觀衆將手機靜音且就座,觀衆在表演以後鼓掌,在表演失敗以後能夠退票。在觀衆類中定義這些功能。

@Aspectpublic class Audience {  @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Before("performance()") 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"); }}複製代碼複製代碼

@AspectJ 註解表名了該類是一個切面。 @Pointcut 定義了一個類中可重用的切點,寫切點表達式時,若是切點相同,能夠重用該切點。 其他方法上的註解定義了加強被調用的時間,根據註解名能夠知道具體調用時間。

到目前爲止, Audience 仍然只是 Spring 容器中的一個 bean。即便使用了 AspectJ 註解,可是這些註解仍然不會解析,由於目前還缺少代理的相關配置。

若是使用 JavaConfig,在配置類的類級別上使用 @EnableAspectJAutoProxy 註解啓用自動代理功能。

@Configuration@EnableAspectJAutoProxy@ComponentScanpublic class ConcertConfig { @Bean public Audience audience() { return new Audience(); } }複製代碼複製代碼

若是使用 xml ,那麼須要引入 <aop:aspectj-autoproxy> 元素。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="concert"/> <aop:aspectj-autoproxy/> <bean class="concert.Audience"/></beans>複製代碼複製代碼

環繞加強

環繞加強就像在一個加強方法中同時編寫了前置加強和後置加強。

@Aspectpublic class Audience { @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}複製代碼複製代碼

能夠看到,這個加強達到的效果與分開寫前置加強與後置加強是同樣的,可是如今全部的功能都位於同一個方法內。 注意該方法接收 ProceedingJoinPoint 做爲參數,這個對象必需要有,由於須要經過它來調用被加強的方法。 注意,在這個方法中,咱們能夠控制不調用 proceed() 方法,從而阻塞對加強方法的訪問。一樣,咱們也能夠在加強方法失敗後,屢次調用 proceed() 進行重試。

加強方法參數

修改 Perform#perform() 方法,添加參數

package concert;public interface Performance { public void perform(int audienceNumbers);}複製代碼複製代碼

咱們能夠經過切點表達式來獲取被加強方法中的參數。

@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))") public void performance(int audienceNumbers){}複製代碼複製代碼

注意,此時方法接收的參數爲 int 型, args(audienceNumbers) 指定參數名爲 audienceNumbers ,與切點方法簽名中的參數匹配,該參數不必定與加強方法的參數名一致。

引入加強

切面不只僅可以加強現有方法,也能爲對象新增新的方法。 咱們能夠在代理中暴露新的接口,當引入接口的方法被調用時,代理會把此調用委託給實現了新接口的某個其餘對象。實際上,就是一個 bean 的實現被拆分到多個類中了。 定義 Encoreable 接口,將其引入到 Performance 的實現類中。

public interface Encoreable { void performEncore();}複製代碼複製代碼

建立一個新的切面

@Aspectpublic class EncoreableIntroducer { @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable;}複製代碼複製代碼

咱們使用了 @Aspect 將 EncoreableIntroducer 標記爲一個切面,可是它沒有提供前置、後置或環繞加強。經過 @DeclareParents 註解將 Encoreable 接口引入到了 Performance bean 中。

@DeclareParents 註解由三部分組成:

  • value 屬性指定了哪一種類型的 bean 要引入該接口。在上述代碼中,類名後面的 + 號表示是 Performance 的全部子類型,而不是它自己。
  • defaultImpl 屬性指定了爲引入功能提供實現的類。
  • @DeclareParents 註解所標註的靜態屬性指明瞭要引入的接口。

一樣地,咱們在 Spring 應用中將該類聲明爲一個 bean:

<bean class="concert.EncoreableIntroducer" />複製代碼複製代碼

Spring 的自動代理機制將會獲取到它的聲明,並建立相應的代理。而後將調用委託給被代理的 bean 或者被引入的實現,具體取決於調用的方法屬於被代理的 bean 仍是屬於被引入的接口。

在 XML 中聲明切面

更新一下 Audience 類,將它的 AspectJ 註解所有移除。

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"); }}複製代碼複製代碼

聲明前置與後置加強

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/> <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></beans>複製代碼複製代碼

如上所示,就將一個普通方法變爲了加強。 大多數的 AOP 配置元素都必須在 <aop:config>元素的上下文內使用。元素名基本上都與註解名相對應。 這裏,咱們一樣將同一個切點表達式寫了四遍,將它提取出來。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:before pointcut-ref="performance" method="silenceCellPhone"/> <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></beans>複製代碼複製代碼

注意,此時 <aop:pointcut> 標籤位於 <aop:aspect> 下層,故只能在該切面中引用。若是想要一個切點可以被多個切面引用,能夠將 <aop:aspect> 元素放在 <aop:config> 下第一層。

環繞加強

定義環繞加強方法

public class Audience { public void performance(int audienceNumbers){} public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}複製代碼複製代碼

在 xml 中使用 <aop:around> 指定方法名與切點便可。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:around pointcut-ref="performance" method="watchPerformance"/> </aop:aspect> </aop:config></beans>複製代碼複製代碼

爲加強傳遞參數

獲取參數主要就在於切點表達式。

<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/>複製代碼複製代碼

這樣能在 xml 中定位到一個參數類型爲 int ,參數名爲 audienceNumbers 的切點。 注意在 xml 中使用了 and 代替 && (在 XML 中, & 符號會被解析爲實體的開始)。

引入加強

<aop:declare-parents types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/>複製代碼複製代碼

types-matching 指定了要匹配的類型,與註解中的 value 值功能相同。

注入 AspectJ 切面

AspectJ 切面提供了 Spring AOP 所不能支持的許多類型的切點。 切面頗有可能依賴其餘類來完成它們的工做。咱們能夠藉助 Spring 的依賴注入把 bean 裝配進 AspectJ 切面中。

建立一個新切面。

public aspect CriticAspect { private CriticismEngine criticismEngine; public CriticAspect() { } pointcut performance():execution(* perform(..)); afterReturning() : performance() { System.out.println(criticismEngine.getCriticism()); } public void setCriticismEngine(CriticismEngine criticismEngine) { this.criticismEngine = criticismEngine; }}複製代碼複製代碼

注入的 CritismEngine 的實現類

public class CriticismEngineImple implements CriticismEngine { public CriticismEngineImple() { } public String getCriticism() { int i = (int) (Math.random() * criticismPool.length); return criticismPool[i]; }  private String[] criticismPool; public void setCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; }}複製代碼複製代碼

CriticAspect 主要做用是在表演結束後爲表演發表評論。 實際上, CriticAspect 是調用了 CriticismEngine 的方法來發表評論。經過 setter 依賴注入爲 CriticAspect 設置 CriticismEngine 。

AOP 你看這一篇就夠了

在配置文件中將 CriticismEngine bean 注入到 CriticAspect 中。

<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf"> <property name="criticismEngine" ref="criticismEngine"/> </bean>複製代碼複製代碼

通常狀況下,Spring bean 由 Spring 容器初始化,可是 AspectJ 切面是由 AspectJ 在運行期建立的。因此在運行期間,AspectJ 建立好了 CriticAspect 實例,每一個 AspectJ 都會提供一個靜態的 aspectOf() 方法,返回切面的的單例。 使用 factory-method 調用 aspectOf() 方法向 CriticAspect 中注入 CriticismEngine 。

相關文章
相關標籤/搜索