1. 程式人生 > >【JavaWeb_Part09】面向介面程式設計?NO!我拒絕,我喜歡面向切面程式設計(AOP)。

【JavaWeb_Part09】面向介面程式設計?NO!我拒絕,我喜歡面向切面程式設計(AOP)。

開篇

很久沒有寫文章了,記得上篇文章中說了一些 Spring 的入門以及 IOC 容器和 DI,今天難得有時間,就打算把剩下的一點 Spring 內容講了。不能再拖了,雖然我是重度拖延症晚期患者。

Spring 測試

先說一下 Spring 中的測試功能吧,說起測試,肯定有很多朋友會想到單元測試(JUnit4),嗯。能夠想到單元測試,這很棒。但是我們今天所講的並不是單元測試,而是 Spring 與 單元測試整合後的測試功能,話不多說,直接幹。

1. 匯入 Spring 中的測試包

既然是 Spring 和單元測試整合,自然是會有一個整合包的,至於包嘛,在 Spring 的依賴檔案件中。
這裡寫圖片描述


就是這個目錄下,將 jar 包直接拷貝到工程的lib 目錄中。

2. 編寫測試類 SpringTest.java

先看看之前我們進行測試的方式:

@Test
public void run(){
    //獲取 applicationContext 物件
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    //根據配置的 bean 標籤中的 id 找到 UserService 的物件
    UserService userService = (UserService) applicationContext.getBean("userService"
); //呼叫 findAll 方法 List<User> userList = userService.findAll(); for (User user : userList) { System.out.println(user); } }

以上就是我們以前的寫法,如果我們要寫 100 個測試方法,我們是不是就需要寫下面的程式碼 100 次,顯然不符合我們程式設計師“懶惰”的性格。

ClassPathXmlApplicationContext applicationContext = new             ClassPathXmlApplicationContext("applicationContext.xml"
); UserService userService = (UserService) applicationContext.getBean("userService");

正是因為在 Spring 中單獨利用 Junit4 做單元測試太複雜,所以 Spring 給我們提供了一個更加簡便的測試方法。

如下

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringTest {
    //Spring 和 JUnit4 整合後的測試
    @Autowired
    private UserService userService;
    @Test
    public void run2(){
        List<User> userList = userService.findAll();
        for (User user :
                userList) {
            System.out.println(user);
        }
    }
}

怎麼樣,是不是比上面的程式碼簡單多了,配合上註解,幾乎我們的測試方式已經簡單到極致了。僅僅只需要兩行註解,就可以搞定全部的單元測試,何樂而不為呢?

注意

這裡我沒有給出 UserService 以及 UserServiceImpl 中的程式碼,這兩個 java 檔案中的程式碼實在是太簡單了,和上一篇文章中的程式碼一樣,所以這裡我就沒有給出來,如果不明白的話,去 07 和 08 兩篇文章中看一下。

Spring 中的 AOP

說到 AOP,可能有人一臉懵逼,就像下面這樣。
這裡寫圖片描述
what the fuck!AOP 又是個什麼玩意?那就先來解釋一下 AOP 是什麼玩意吧!

1. 什麼是AOP的技術?

  1. 在軟體業,AOP 為 Aspect Oriented Programming 的縮寫,意為:面向切面程式設計。
  2. AOP 是一種程式設計正規化,隸屬於軟工範疇,指導開發者如何組織程式結構。
  3. AOP 最早由 AOP 聯盟的組織提出的,制定了一套規範。Spring 將 AOP 思想引入到框架中,必須遵守 AOP 聯盟的規範。
  4. 通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。
  5. AOP 是 OOP 的延續,是軟體開發中的一個熱點,也是 Spring 框架中的一個重要內容,是函數語言程式設計的一種衍生範型。
  6. 利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
  7. AOP 採取橫向抽取機制,取代了傳統縱向繼承體系重複性程式碼(效能監視、事務管理、安全檢查、快取)。

上面都是一些概念性的東西,理解不了就算了。舉個最通俗的例子吧,現在有兩個類 UserDao 和 CustomerDao,如果我想要在執行 dao 層中的 save 方法之前進行一個列印日誌的操作,聰明的你們可以動腦筋想一想,該怎麼做?肯定有人想到了繼承。嗯,繼承確實是可以達到我們的目的,但是耦合度太高。況且這樣也需要修改原始碼。這種方法並不適合後期的維護,也不利於程式的健壯性。如果我們想要程式的耦合性不那麼高,那麼利用 aop 正好可以達到我們的要求。

看下面一張圖。
這裡寫圖片描述

圖畫的有點醜,以前我們的做法是 service 層操作 dao 層,而 aop 的思想就是在所有的 aop 層添加了一個切面,這個切面不依賴任何類。並且可以在所有的 dao 中的 save 方法執行之前,搞一些事情,這也正好可以滿足我們的條件,也是面向切面程式設計的由來,也就是所謂的對程式進行了增強。至於我們為什麼要學習 aop,主要還是因為 aop 可以在不修改原始碼的前提下,對程式進行增強!

2. AOP 開發

2.1 新增 jar 包

既然是要學習 aop 的思想,基本的搭建環境這部分我就不做過多說明了。主要是講解 aop。學習 spring 的核心功能之一,首要的還是要匯入 spring 的 aop 相關的 jar 包。那麼 aop 需要哪些 jar 包呢?
我們需要匯入的 jar 包主要有下面四個,這四個 jar 包都是和 aop 相關的。

spring-aspects-4.2.4.RELEASE.jar 
spring-aop-4.2.4.RELEASE.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
com.springsource.org.aopalliance-1.0.0.jar

2.2 編寫切面類 MyAspectJ.java

public class MyAspectJ {
    public void log(){
        System.out.println("儲存日誌");
    }
}

2.3 配置切面類以及切面切入點

<!-- 配置切面類-->
<bean id="myAspectJ" class="com.student.aop.MyAspectJ"/>

<!-- 配置切入點-->
<aop:config>
     <aop:aspect ref="myAspectJ">
         <aop:before method="log" pointcut="execution(public void com.student.mapper.UserDaoImpl.save(..))"/>
     </aop:aspect>
 </aop:config>

2.4 執行結果演示

這裡寫圖片描述

可能有人對第三步有點不理解,先別懵逼。我們看一下結果,很明顯是在儲存使用者之前呼叫了切面類中的儲存日誌方法,而切面類不依賴任何一個類。所以這即達到了我們的要求,同時也降低了程式的耦合性。

2.5 配置詳解

對初學者來說,2.3 中的配置可能有點難以理解,下面我們對 2.3 中的配置做一個說明。反正又是一大堆名詞。前面的 bean 標籤我就不說了,都知道。不理解的估計也就是這段。

<!-- 配置通知型別以及切入點-->
<aop:before method="log" pointcut="execution(public void com.student.mapper.UserDaoImpl.save(..))"/>

一個一個來,先說 pointcut,這個是表示切入點,那什麼叫切入點呢?比如說我們想在 save 方法執行之前,執行儲存日誌的功能,那麼 save 方法就是切入點。至於後面的一大坨的表示式,這個就厲害了。

  1. execution() 這個是必須的,必須這麼寫,別跟老子講為什麼,老子也不知道為什麼,你記住必須要這麼寫就對了。
  2. public 表示方法的修飾符,可以省略。
  3. void 表示返回值。
  4. com.student.mapper.UserDaoImpl.save() 表示方法的全路徑名稱。
  5. save(..) .. 表示任意引數。
    上面的寫法可能有點複雜,那我們就可以簡寫成下面的。
execution(* *..*.*DaoImpl.save*(..)) //這個寫法就厲害了,肯定會有人一臉懵逼

先執行一把看看結果再來解釋。
這裡寫圖片描述

切入點的表示式如下:

execution([修飾符] 返回值型別 包名.類名.方法名(引數))

結果是一樣的,

這裡我們省略了 public。訪問修飾符是可以不寫的。
第一個 * 代表的是返回值。*表示任意返回值。返回值不能不寫,必須存在。
*..* 是簡寫了前面的包名 com.student.mapper,簡寫了這段。
*DaoImpl 表示任何以 DaoImpl 結尾的,比如UserDaoImplCustomerDaoImpl等等。
save* 表示以 save 開頭的方法。
(..) 表示任意引數。

誒,理解起來也不是那麼困難嘛。上面的一段就這麼容易就解決了。既然可以在 xml中配置,那麼是不是同樣也可以用註解來簡化切面的配置呢?咦,你怎麼這麼聰明,肯定是可以的呀。

2.6 使用註解簡化 AOP 的配置

首先在 applicationContext.xml 中定義切面類。

<bean id="myAspectJ" class="com.student.aop.MyAspectJ"/>

然後在切面類上加上註解 AspectJ,同時在增強方法上面加上 @Before 通知註解。

@Aspect
public class MyAspectJ {

    @Before(value = "execution(* *..*.*DaoImpl.save*(..))")
    public void log(){
        System.out.println("儲存日誌");
    }
}

通知註解中的 value = “內容”,這裡“內容”部分就是切入點的表示式。
切記別忘記了在 applicationContext.xml 中開啟自動代理。

<!-- 開啟 aop 的自動代理-->
<aop:aspectj-autoproxy/>

到這裡,AOP 的註解開發就完成了。最後還有一個需要講。可能有人不明白為什麼要用 Before 這個註解,這個註解是幹嘛用的?這就要說到通知型別了。Before 是我們的一種通知型別。

2.7 通知型別

為了便於測試,我又另外新添加了一個方法 update()。
注意,所有的註解配置我已經都在截圖中給出來了,不要再問我為什麼不貼出來配置檔案。
前置通知(Before)

在目標類的方法執行之前執行

執行截圖

這裡寫圖片描述
可以看到“儲存日誌” 這個列印結果在 save 方法執行之前輸出的。

環繞通知(Around)

方法的執行前後執行。
要注意:目標的方法預設不執行,需要藉助 ProceedingJoinPoint 對來讓目標物件的方法執行。

不信我們就看看圖吧。

這裡寫圖片描述

save 方法中的儲存使用者並沒有輸出,所以我們需要手動執行我們的目標方法。自己手動新增一個引數,然後看執行結果。

這裡寫圖片描述

ProceedingJoinPoint  //這個物件的例項就可以幫我們呼叫目標方法去執行。因為要演示環繞通知,所以我就加了一行列印語句。

最終通知(After)

在目標類的方法執行之後執行,如果程式出現了異常,最終通知也會執行。

//自己在 save 方法裡面造一個異常
@Override
public void save() {
   int i = 1 / 0;
   System.out.println("儲存使用者...");
}

執行截圖

這裡寫圖片描述
異常資訊出來了,但是我們的儲存日誌也打印出來了。如果你換成其他的通知型別,儲存日誌是不會被列印的,如果不信邪,自己去嘗試,我是嘗試過了才敢這麼說的。

後置通知(AfterReturning)

方法正常執行後的通知。

執行截圖

這裡寫圖片描述

在 save 方法執行完成後呼叫了“儲存日誌”的方法。

異常丟擲通知(AfterThrowing)

在丟擲異常後通知
這種異常丟擲通知用的不多,所以我就不做過多的解釋了。如果你們有興趣,可以自己去嘗試一下,自己動手,豐衣足食。

2.8 自定義切入點

除了像上面那樣配置切入點外,我們還可以自己去定義一個通用的切入點。比如像下面這樣

@Aspect
public class MyAspectJ {

    @Before(value = "MyAspectJ.fn()")
    public void log(){
        System.out.println("儲存日誌");
        System.out.println("儲存日誌2");
    }

    @Pointcut(value = "execution(public void com.student.mapper.UserDaoImpl.save())")
    public void fn(){

    }
}

其輸出結果是一樣的。
這裡寫圖片描述

注意:如果自定義的切入點和通知方法在同一個類中,Before 註解裡面的值可以是

MyAspectJ.fn()
fn()

這兩者均可。

結尾

唉,說了準備把 Spring 中的內容全部講完的,但是一個 AOP 講了太多了,只能留到下篇文章了。(不是我想拖,主要是在事務估計也有 AOP 這麼多,再寫下去就如滔滔江水,一發不可收拾了)。所以還是下篇文章再講吧。反正這篇文章什麼都沒有講,就是講了 aop,你們看起來也不用那麼累。
最後注意一點,本人能力有限,寫出來的東西純屬是個人對於 Spring 這個框架的理解。所以在記錄和理解的過程中難免會出現錯誤,如果出現了錯誤,望指正。同時也歡迎小夥伴們留言與我交流。
看到這裡的朋友可以回顧一下什麼是 Spring IOC 容器以及依賴注入。不記得的自己去面壁。