JavaRush /Java Blog /Random-TW /什麼是AOP?面向方面的程式設計基礎知識

什麼是AOP?面向方面的程式設計基礎知識

在 Random-TW 群組發布
大家好!如果不理解基本概念,就很難深入研究建構功能的框架和方法。所以今天我們要討論其中一個概念──AOP,也就是面向方面程式設計什麼是AOP? 面向方面程式設計基礎 - 1這不是一個簡單的話題,也不經常直接使用,但許多框架和技術都在幕後使用它。當然,有時在面試過程中,您可能會被要求籠統地告訴您這是什麼動物以及它可以在哪裡使用。那我們來看看Java中AOP的基本概念和一些簡單的例子。什麼是AOP? 面向方面程式設計的基礎 - 2因此,AOP面向方面​​的程式設計)是一種範式,旨在透過分離橫切關注點來提高應用程式各個部分的模組化程度。為此,需要在現有程式碼中添加額外的行為,而無需更改原始程式碼。換句話說,我們似乎在方法和類別之上掛了額外的功能,而不對修改後的程式碼進行修改。為什麼這是必要的?我們遲早會得出這樣的結論:通常的物件導向方法並不總是有效地解決某些問題。這時, AOP就派上用場了,它為我們提供了建立應用程式的額外工具。額外的工具意味著開發的靈活性增加,因此解決特定問題有更多選擇。

AOP的應用

面向方面的程式設計旨在解決橫切問題,橫切問題可以是任何以不同方式重複多次的程式碼,這些程式碼不能完全結構化為單獨的模組。因此,透過AOP,我們可以將其保留在主代碼之外並垂直定義它。一個例子是在應用程式中應用安全性策略。通常,安全性涉及應用程式的許多元素。此外,應用程式安全性策略必須同等地應用於應用程式的所有現有部分和新部分。同時,所使用的安全策略本身也可以發展。這就是AOP可以派上用場的地方。另一個例子是日誌記錄。與手動插入日誌記錄相比,使用AOP方法進行日誌記錄有幾個優點:
  1. 日誌代碼很容易實現和刪除:您只需要新增或刪除某些方面的幾個配置。
  2. 所有日誌記錄的源代碼都儲存在一個地方,無需手動查找所有使用的地方。
  3. 用於日誌記錄的程式碼可以添加到任何地方,無論是已經編寫的方法和類別還是新功能。這減少了開發人員錯誤的數量。
    此外,當您從設計配置中刪除某個方面時,您可以絕對確定所有追蹤程式碼都已刪除並且沒有遺失任何內容。
  4. 方面是可以重複重複使用和改進的獨立程式碼。
什麼是AOP? 面向方面程式設計的基礎 - 3AOP 也用於異常處理、快取和刪除某些功能以使其可重複使用。

AOP的基本概念

為了進一步分析這個主題,我們先來了解AOP的主要概念。 建議是從連接點呼叫的附加邏輯、程式碼。此建議可以在連接點之前、之後或代替連接點執行(更多資訊請參閱下文)。可能的建議類型
  1. Before - 這種類型的通知在目標方法 - 連接點執行之前啟動。當使用方面作為類別時,我們使用@Before註解來將建議類型標記為先前的類型。當使用方面作為.aj檔時,這將是before()方法。
  2. 之後(After) ——方法執行完成後執行的通知——連接點,無論是在正常情況下還是在拋出異常時。
    當使用方面作為類別時,我們可以使用@After註解來指示這是後面的提示。
    當使用方面作為.aj檔時,這將是after()方法。
  3. 返回後- 僅當目標方法正常工作且沒有錯誤時才會執行這些提示。
    當方面表示為類別時,我們可以使用@AfterReturning註解將建議標記為在成功完成後執行。
    當使用方面作為 .aj 檔案時,這將是after()方法傳回 (Object obj) 。
  4. 拋出後- 此類建議適用於方法(即連接點)拋出異常的情況。我們可以使用此建議對失敗的執行進行某些處理(例如,回滾整個交易或使用所需的追蹤等級進行日誌記錄)。
    對於切面類,@AfterThrowing註解用於指示在拋出異常後使用此建議。當以.aj
    檔案 形式使用切面時,這將是方法 - after() 拋出(異常 e)
  5. around可能是圍繞方法(即連接點)的最重要的建議類型之一,例如,我們可以使用它來選擇是否執行給定的連接點方法。
    您可以編寫在連接點方法執行之前和之後執行的建議程式碼。around通知的
    職責包括呼叫連接點方法以及如果該方法傳回某些內容則傳回值。也就是說,在本技巧中,您可以簡單地模仿目標方法的操作而不呼叫它,並返回您自己的結果作為結果。 對於類別形式的方面,我們使用@Around註解來建立包裝連接點的提示。當使用方面作為.aj檔時,這將是around()方法。
連接點- 執行程式中應套用建議的點(呼叫方法、建立物件、存取變數)。換句話說,這是某種正規表示式,借助它可以找到引入程式碼的地方(應用提示的地方)。 切入點是一組連接點。切割確定給定的連接點是否適合給定的尖端。 方面是實現端到端功能的模組或類別。方面透過在某個slice定義的連接點應用建議來修改程式碼其餘部分的行為。換句話說,它是尖端和連接點的組合。 簡介- 更改類別的結構和/或更改繼承層次結構以向外部程式碼新增方面功能。 目標是將建議應用到的物件。 編織是將方面與其他物件連結起來以創建推薦的代理物件的過程。這可以在編譯時、載入時或執行時完成。編織方式可分為三種:
  • 編譯時織入- 如果您有切面的原始程式碼以及使用切面的程式碼,則可以直接使用 AspectJ 編譯器編譯原始程式碼和切面;
  • 編譯後編織(二進位編織) - 如果您不能或不想使用原始程式碼轉換將方面編織到程式碼中,您可以採用已編譯的類別或 jar 並注入方面;
  • 載入時編織只是二進位編織,延遲到類別載入器載入類別檔案並為 JVM 定義類別為止。
    為了支援這一點,需要一個或多個「weave 類別載入器」。它們要么由運行時顯式提供,要么由“編織代理”激活。
AspectJ是AOP範式的具體實現,實現了解決橫切問題的能力。可以在此處找到文件。

Java 中的範例

接下來,為了更好地理解AOP,我們將看一下 Hello World 等級的小範例。什麼是AOP? 面向方面程式設計的基礎 - 4讓我立即指出,在我們的範例中,我們將使用編譯時編織。首先,我們需要將以下依賴項新增到pom.xml中:
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
通常,使用特殊的Ajs編譯器來使用方面。IntelliJ IDEA預設沒有它,因此在選擇它作為應用程式編譯器時,您需要指定AspectJ發行版的路徑。您可以在本頁閱讀更多有關選擇Ajs作為編譯器的方法。這是第一種方法,第二種方法(我使用的)是將以下插件新增到pom.xml
<build>
  <plugins>
     <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.7</version>
        <configuration>
           <complianceLevel>1.8</complianceLevel>
           <source>1.8</source>
           <target>1.8</target>
           <showWeaveInfo>true</showWeaveInfo>
           <verbose>true</verbose>
           <Xlint>ignore</Xlint>
           <encoding>UTF-8</encoding>
        </configuration>
        <executions>
           <execution>
              <goals>
                 <goal>compile</goal>
                 <goal>test-compile</goal>
              </goals>
           </execution>
        </executions>
     </plugin>
  </plugins>
</build>
之後,建議從Maven重新匯入並執行mvn cleancompile。現在讓我們繼續看範例。

範例1

讓我們建立一個Main類別。在其中我們將有一個啟動點和一個在控制台中列印傳遞給它的名稱的方法:
public class Main {

  public static void main(String[] args) {
  printName("Толя");
  printName("Вова");
  printName("Sasha");
  }

  public static void printName(String name) {
     System.out.println(name);
  }
}
沒什麼複雜的:他們傳遞了名稱並將其顯示在控制台中。如果我們現在運行它,控制台將顯示:
托利亞·沃瓦·薩莎
好吧,是時候利用 AOP 的力量了。現在我們需要建立一個文件方面。它們有兩種類型:第一種是副檔名為.aj的文件,第二種是使用註解實作AOP功能的常規類別。我們先來看一個副檔名為.aj的檔:
public aspect GreetingAspect {

  pointcut greeting() : execution(* Main.printName(..));

  before() : greeting() {
     System.out.print("Привет ");
  }
}
這個文件有點類似一個類別。讓我們弄清楚這裡發生了什麼: 切入點- 一個切點或一組連接點; greeting() — 該切片的名稱; : 執行- 執行時* - 全部,呼叫 - Main.printName(..) - 這個方法。接下來是具體的建議 - before() - 在呼叫目標方法之前執行:greeting() - 該建議所反應的切片,下面我們看到方法本身的主體,它是用 Java 編寫的我們理解的語言。當我們在存在此方面的情況下運行main時,我們將在控制台中得到以下輸出:
你好托利亞你好沃瓦你好薩莎
我們可以看到printName方法的每次呼叫都被一個面向修改了。現在讓我們看看切面是什麼樣子,但它是一個帶有註解的 Java 類別:
@Aspect
public class GreetingAspect{

  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }

  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Привет ");
  }
}
在.aj方面文件 之後,一切都更加明顯:
  • @Aspect表示給定的類別是一個方面;
  • @Pointcut("execution(* Main.printName(String))")是一個切入點,在所有使用String類型的傳入參數對Main.printName的呼叫上觸發;
  • @Before("greeting()") - 在呼叫在greeting()切入點描述的程式碼之前應用的建議。
在這方面運行main不會改變控制台輸出:
你好托利亞你好沃瓦你好薩莎

例子2

假設我們有一些為客戶端執行某些操作的方法,並從main呼叫此方法:
public class Main {

  public static void main(String[] args) {
  makeSomeOperation("Толя");
  }

  public static void makeSomeOperation(String clientName) {
     System.out.println("Выполнение некоторых операций для клиента - " + clientName);
  }
}
使用@Around註釋,讓我們做一些類似「偽交易」的事情:
@Aspect
public class TransactionAspect{

  @Pointcut("execution(* Main.makeSomeOperation(String))")
  public void executeOperation() {
  }

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Открытие транзакции...");
     try {
        joinPoint.proceed();
        System.out.println("Закрытие транзакции....");
     }
     catch (Throwable throwable) {
        System.out.println("Операция не удалась, откат транзакции...");
     }
  }
  }
使用ProceedingJoinPoint物件的proceed方法,我們呼叫包裝器的方法來確定它在板中的位置,並相應地呼叫上面方法中的程式碼joinPoint.proceed(); - 這是之前,下面是 -之後。如果我們運行main我們將進入控制台:
開啟交易...為客戶端執行一些操作 - Tolya 關閉交易....
如果我們向我們的方法添加一個異常拋出(突然操作失敗):
public static void makeSomeOperation(String clientName)throws Exception {
  System.out.println("Выполнение некоторых операций для клиента - " + clientName);
  throw new Exception();
}
然後我們將在控制台中得到輸出:
正在開啟交易...正在為客戶端執行一些操作 - Tolya 操作失敗,事務已回滾...
原來是對失敗的偽處理。

例子3

作為下一個範例,讓我們執行一些類似登入控制台的操作。首先,讓我們看看Main,我們的偽業務邏輯發生在其中:
public class Main {
  private String value;

  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<некоторое meaning>");
     String valueForCheck = main.getValue();
     main.checkValue(valueForCheck);
  }

  public void setValue(String value) {
     this.value = value;
  }

  public String getValue() {
     return this.value;
  }

  public void checkValue(String value) throws Exception {
     if (value.length() > 10) {
        throw new Exception();
     }
  }
}
main中,使用setValue我們將設定內部變數 value 的值然後使用getValue我們將取得該值,在checkValue中我們將檢查該值是否超過 10 個字元。如果是,則會拋出異常。現在讓我們來看看記錄方法操作的方面:
@Aspect
public class LogAspect {

  @Pointcut("execution(* *(..))")
  public void methodExecuting() {
  }

  @AfterReturning(value = "methodExecuting()", returning = "returningValue")
  public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
     if (returningValue != null) {
        System.out.printf("Успешно выполнен метод - %s, класса- %s, с результатом выполнения - %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Успешно выполнен метод - %s, класса- %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }

  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Метод - %s, класса- %s, был аварийно завершен с исключением - %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
這裡發生了什麼事? @Pointcut("execution(* *(..))") - 將連接到所有方法的所有呼叫; @AfterReturning(value = "methodExecuting()", returned = "returningValue") - 目標方法成功完成後將執行的建議。我們這裡有兩個案例:
  1. 當方法有回傳值時if (returningValue != null) {
  2. 當沒有回傳值時else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") - 當發生錯誤時(即從方法拋出例外)將觸發的建議。因此,透過運行main,我們將在控制台中獲得一種日誌記錄:
Main 類別的 setValue 方法執行成功 Main 類別的 getValue 方法執行成功,執行結果 <some value> Main 類別的 checkValue 方法執行成功,因異常異常終止- java.lang.Exception 方法- main、class-Main、因異常崩潰- java.lang.Exception
好吧,由於我們沒有處理異常,所以我們還將獲得它的堆疊追蹤:什麼是AOP? 面向方面程式設計的基礎知識 - 5您可以在以下文章中閱讀有關異常及其處理的資訊:Java 中的異常異常及其處理。這就是我今天的全部內容。今天我們認識了AOP,你會發現這個野獸並不像它所描繪的那麼可怕。 再見了,大家!什麼是AOP? 面向方面程式設計的基礎知識 - 6
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION