CodeGym/Java Blog/Toto sisi/什麼是 AOP?面向方面編程的原則
John Squirrels
等級 41
San Francisco

什麼是 AOP?面向方面編程的原則

在 Toto sisi 群組發布
個成員
嗨,伙計們和姑娘們!如果不了解基本概念,就很難深入研究構建功能的框架和方法。所以今天我們將討論這樣一個概念——AOP,又名面向方面的編程什麼是 AOP? 面向方面編程的原則 - 1這個主題並不容易,也很少直接使用,但許多框架和技術在幕後使用它。當然,有時在採訪中,您可能會被要求籠統地描述這是什麼樣的野獸以及它可以應用到哪裡。那麼我們就來看看Java中AOP的基本概念和一些簡單的例子。那麼,AOP代表面向方面的編程,這是一種旨在通過分離橫切關注點來增加應用程序不同部分的模塊化的範例。為實現這一點,在不對原始代碼進行更改的情況下,將額外的行為添加到現有代碼中。換句話說,我們可以將其視為在不更改修改後的代碼的情況下,將附加功能附加到方法和類之上。為什麼這是必要的?遲早,我們會得出結論,典型的面向對象方法不能總是有效地解決某些問題。當那一刻到來時,AOP來拯救我們並為我們提供額外的工具來構建應用程序。額外的工具意味著軟件開發的靈活性增加,這意味著解決特定問題的更多選擇。

應用AOP

面向切面編程旨在執行橫切任務,可以是任何可能通過不同方法重複多次的代碼,不能完全結構化到一個單獨的模塊中。因此,AOP允許我們將它保留在主代碼之外並垂直聲明它。一個例子是在應用程序中使用安全策略。通常,安全貫穿應用程序的許多元素。更重要的是,應用程序的安全策略應該同樣適用於應用程序的所有現有部分和新部分。同時,正在使用的安全策略本身可以發展。這是使用AOP 的最佳場所。另外,另一個例子是記錄. 使用 AOP 方法進行日誌記錄而不是手動添加日誌記錄功能有幾個優點:
  1. 日誌記錄的代碼很容易添加和刪除:您需要做的就是添加或刪除某些方面的一些配置。

  2. 所有用於日誌記錄的源代碼都保存在一個地方,因此您無需手動搜索所有使用它的地方。

  3. 可以在任何地方添加日誌記錄代碼,無論是在已經編寫的方法和類中還是在新功能中。這減少了編碼錯誤的數量。

    此外,當從設計配置中刪除一個方面時,您可以確定所有跟踪代碼都已消失並且沒有遺漏任何內容。

  4. 方面是獨立的代碼,可以改進並一次又一次地使用。
什麼是 AOP? 面向方面編程的原則 - 2AOP 還用於異常處理、緩存和提取某些功能以使其可重用。

AOP的基本原理

為了進一步探討這個主題,讓我們首先了解一下 AOP 的主要概念。 建議——從連接點調用的附加邏輯或代碼。建議可以在連接點之前、之後或代替連接點執行(下面將詳細介紹)。可能的建議類型
  1. 之前——這種類型的建議在目標方法(即連接點)執行之前啟動。將方面用作類時,我們使用@Before註釋將通知標記為之前出現。將方面用作.aj文件時,這將是before()方法。

  2. After — 在方法(連接點)執行完成後執行的建議,無論是在正常執行還是在拋出異常時。

    將方面用作類時,我們可以使用@After註釋來指示這是之後的建議。

    將方面用作.aj文件時,這是after()方法。

  3. 返回後——只有當目標方法正常完成且沒有錯誤時才執行此建議。

    當方面表示為類時,我們可以使用@AfterReturning批註將通知標記為在成功完成後執行。

    將方面用作.aj文件時,這將是after() 返回 (Object obj)方法。

  4. 拋出後——此建議適用於方法(即連接點)拋出異常的情況。我們可以使用此建議來處理某些類型的失敗執行(例如,回滾整個事務或具有所需跟踪級別的日誌)。

    對於類方面,@AfterThrowing註釋用於指示在拋出異常後使用此通知。

    將方面用作.aj文件時,這將是after() throwing (Exception e)方法。

  5. 周圍——也許是最重要的建議類型之一。它包含一個方法,即一個連接點,我們可以使用它來選擇是否執行給定的連接點方法。

    您可以編寫在執行連接點方法之前和之後運行的建議代碼。

    around advice負責調用連接點方法和返回值(如果該方法有返回值)。換句話說,在這個通知中,你可以簡單地模擬目標方法的運行而不調用它,並返回你想要的任何結果作為返回結果。

    給定方面作為類,我們使用@Around註釋來創建包裝連接點的建議。當使用.aj文件形式的方面時,此方法將是around()方法。

連接點——運行程序(即方法調用、對象創建、變量訪問)中應該應用建議的點。換句話說,這是一種用於查找代碼注入位置(應該應用建議的地方)的正則表達式。 切入點——一組連接點。切入點確定給定的建議是否適用於給定的連接點。 方面——實現橫切功能的模塊或類。切面通過在某些切入點定義的連接點應用建議來更改其餘代碼的行為。換句話說,它是建議和連接點的組合。 介紹— 更改類的結構和/或更改繼承層次結構以將方面的功能添加到外部代碼。 目標— 建議將應用到的對象。 編織——將方面鏈接到其他對像以創建建議代理對象的過程。這可以在編譯時、加載時或運行時完成。織法分為三種:
  • 編譯時編織——如果你有方面的源代碼和使用方面的代碼,那麼你可以直接使用 AspectJ 編譯器編譯源代碼和方面;

  • 編譯後編織(二進制編織) ——如果您不能或不想使用源代碼轉換將方面編織到代碼中,您可以採用先前編譯的類或 jar 文件並將方面注入其中;

  • 加載時編織——這只是二進制編織,延遲到類加載器加載類文件並為 JVM 定義類。

    需要一個或多個編織類加載器來支持這一點。它們要么由運行時顯式提供,要么由“編織代理”激活。

AspectJ — AOP範例的特定實現,它實現了執行橫切任務的能力。文檔可以在這裡找到。

Java 中的示例

接下來,為了更好地理解AOP,我們將查看“Hello World”式的小示例。首先,我會注意到我們的示例將使用編譯時織入。首先,我們需要在pom.xml文件中添加以下依賴項:
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
通常,特殊的ajc編譯器是我們使用方面的方式。IntelliJ IDEA默認不包含它,因此在選擇它作為應用程序編譯器時,您必須指定 5168 75 AspectJ分發的路徑。這是第一種方式。第二種,也就是我使用的,是在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 clean compile。現在讓我們直接進入示例。

示例 1

讓我們創建一個類。在其中,我們將有一個入口點和一個在控制台上打印傳遞名稱的方法:
public class Main {

  public static void main(String[] args) {
  printName("Tanner");
  printName("Victor");
  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("Hi, ");
  }
}
這個文件有點像一個類。讓我們看看這裡發生了什麼: 切入點是一組連接點; greeting()是這個切入點的名稱; : execution指示在執行Main.printName(...)方法的所有 ( * )調用期間應用它。接下來是一個特定的建議 — before() — 在調用目標方法之前執行。: greeting()是這個通知響應的切點。好吧,下面我們看到了方法本身的主體,它是用我們理解的 Java 語言編寫的。當我們在這個方面存在的情況下運行main時,我們將得到這個控制台輸出:
嗨,Tanner 嗨,Victor Hi,Sasha
我們可以看到,由於一個方面,對printName方法的每次調用都已被修改。現在讓我們看一下作為帶有註解的 Java 類的切面是什麼樣的:
@Aspect
public class GreetingAspect{

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

  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
在.aj方面文件 之後,這裡的一切都變得更加明顯:
  • @Aspect表示這個類是一個切面;
  • @Pointcut("execution(* Main.printName(String))")是所有調用Main.printName時觸發的切點,輸入參數類型為String
  • @Before("greeting()")是在調用greeting()切點中指定的代碼之前應用的建議。
使用此方面運行main不會更改控制台輸出:
嗨,Tanner 嗨,Victor Hi,Sasha

示例 2

假設我們有一些方法可以為客戶端執行一些操作,我們從main調用這個方法:
public class Main {

  public static void main(String[] args) {
  performSomeOperation("Tanner");
  }

  public static void performSomeOperation(String clientName) {
     System.out.println("Performing some operations for Client " + clientName);
  }
}
讓我們使用@Around註解來創建一個“偽事務”:
@Aspect
public class TransactionAspect{

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

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Opening a transaction...");
     try {
        joinPoint.proceed();
        System.out.println("Closing a transaction...");
     }
     catch (Throwable throwable) {
        System.out.println("The operation failed. Rolling back the transaction...");
     }
  }
  }
通過ProceedingJoinPoint對象的proceed方法,我們調用 wrapping 方法來確定它在通知中的位置。因此,上述方法中的代碼joinPoint.proceed(); Before,而下面的代碼是After。如果我們運行main,我們會在控制台中看到:
開啟交易... 為客戶 Tanner 執行一些操作 關閉交易...
但是如果我們在我們的方法中拋出異常(模擬失敗的操作):
public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
然後我們得到這個控制台輸出:
正在打開交易... 正在為客戶 Tanner 執行一些操作操作失敗。回滾交易...
所以我們最終得到的是一種錯誤處理能力。

例 3

在我們的下一個示例中,讓我們做一些類似登錄到控制台的事情。首先,看一下Main,我們在其中添加了一些偽業務邏輯:
public class Main {
  private String value;

  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<some value>");
     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("Successful execution: method — %s method, class — %s class, return value — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Successful execution: method — %s, class — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }

  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Exception thrown: method — %s, class — %s, exception — %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
這裡發生了什麼? @Pointcut("execution(* *(..))")將加入所有方法的所有調用。 @AfterReturning(value = "methodExecuting()", returning = "returningValue")是將在成功執行目標方法後執行的通知。我們這裡有兩種情況:
  1. 當方法有返回值時——if (returningValue!= Null) {
  2. 當沒有返回值時——else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception")是在發生錯誤時觸發的通知,即當方法拋出異常時。因此,通過運行main,我們將獲得一種基於控制台的日誌記錄:
執行成功:method--setValue,class--Main 執行成功:method--getValue,class--Main,返回值--<some value> 拋出的異常:method--checkValue,class--Main 異常--java.lang.Exception 拋出的異常:method-- main, class — Main, exception — java.lang.Exception
由於我們沒有處理異常,我們仍然會得到堆棧跟踪:什麼是 AOP? 面向方面編程的原則 - 3您可以在這些文章中閱讀有關異常和異常處理的內容:Java 中的異常異常:捕獲和處理。今天對我來說就這些了。今天我們認識了AOP,你可以看到這隻野獸並不像某些人想像的那麼可怕。再見了,大家!
留言
  • 受歡迎
你必須登入才能留言
此頁面尚無留言