一、AOP

1、介紹

  AOP(Aspect Oriented Programming),面向切面程式設計。它利用一種稱為"橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未來的可操作性和可維護性。
  AOP是對所有物件或者是一類物件程式設計,核心是在不增加程式碼的基礎上,還增加新功能。實際上在開發框架本身用的多,在實際專案中,用的不多。

  切面(aspect):要實現的交叉功能,是系統模組化的一個切面或領域。如日誌記錄。
  連線點:應用程式執行過程中插入切面的地點,可以是方法呼叫,異常丟擲,或者要修改的欄位。
  通知(增強):切面的實際實現,他通知系統新的行為。如在日誌通知包含了實現日誌功能的程式碼,如向日志文件寫日誌。通知在連線點插入到應用系統中。
  切入點:定義了通知應該應用在哪些連線點,通知可以應用到AOP框架支援的任何連線點。連線點(靜態)-->切入點(動態)。
  引入:為類新增新方法和屬性。
  目標物件:被通知的物件(被代理的物件)。既可以是你編寫的類也可以是第三方類。
  代理物件:將通知應用到目標物件後建立的物件,應用系統的其他部分不用為了支援代理物件而改變。
  織入:將切面應用到目標物件從而建立一個新代理物件的過程。織入發生在目標物件生命週期的多個點上:
  ①編譯期:切面在目標物件編譯時織入,這需要一個特殊的編譯器。
  ②類裝載期:切面在目標物件被載入JVM時織入,這需要一個特殊的類載入器。
  ③執行期:切面在應用系統執行時織入。

  原理:AOP的核心思想為設計模式的動態代理模式。

2、案例

  注意:接下來介紹的案例,更像是AOP的靜態代理實現,個人理解。
  定義目標物件(被代理的物件)

  1. 1 // 定義一個介面
  2. 2 public interface ITeacher {
  3. 3 void teach();
  4. 4 int add(int i, int j);
  5. 5 }
  6. 6
  7. 7 // 定義目標物件
  8. 8 public class Teacher implements ITeacher {
  9. 9 @Override
  10. 10 public void teach() {
  11. 11 System.out.println("老師正在上課");
  12. 12 }
  13. 13
  14. 14 @Override
  15. 15 public int add(int i, int j) {
  16. 16 int add = i + j;
  17. 17 System.out.println("執行目標方法:老師正在做加法,結果為:" + add);
  18. 18 // int throwable = 10 / 0; 測試異常通知
  19. 19 return add;
  20. 20 }
  21. 21
  22. 22 // 目標物件自己的方法,此方法不是介面所以無法代理
  23. 23 public void sayHello() {
  24. 24 System.out.println("老師會說hello");
  25. 25 }
  26. 26
  27. 27 }

  編寫一個通知(前置通知)

  1. 1 // 前置通知
  2. 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
  3. 3
  4. 4 // method:被 代理物件 呼叫的方法(目標方法)
  5. 5 // objects:被 代理物件 呼叫的方法入參(引數)
  6. 6 // target:目標物件
  7. 7 @Override
  8. 8 public void before(Method method, Object[] objects, Object target) throws Throwable {
  9. 9 System.out.println("前置通知=====1======函式名:" + method.getName());
  10. 10 System.out.println("前置通知=====2======引數值:" + JSON.toJSONString(objects));
  11. 11 System.out.println("前置通知=====3======物件值:" + JSON.toJSONString(target));
  12. 12 }
  13. 13
  14. 14 }

  配置 application.xml

  1. 1 <!-- 配置目標物件 -->
  2. 2 <bean id="teacher" class="com.lx.spring.day1.Teacher"/>
  3. 3
  4. 4 <!-- 配置前置通知 -->
  5. 5 <bean id="myMethodBeforeAdvice" class="com.lx.spring.day1.MyMethodBeforeAdvice"/>
  6. 6
  7. 7 <!-- 配置代理物件 -->
  8. 8 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
  9. 9 <!-- 1.指定要代理的目標物件 -->
  10. 10 <property name="target" ref="teacher"/>
  11. 11
  12. 12 <!-- 2.指定要代理的介面集 -->
  13. 13 <property name="proxyInterfaces">
  14. 14 <list>
  15. 15 <value>com.lx.spring.day1.ITeacher</value>
  16. 16 </list>
  17. 17 </property>
  18. 18
  19. 19 <!-- 3.指定要織入的通知 -->
  20. 20 <property name="interceptorNames">
  21. 21 <list>
  22. 22 <value>myMethodBeforeAdvice</value>
  23. 23 </list>
  24. 24 </property>
  25. 25 </bean>
  1. 1 // 測試類
  2. 2 public class Main {
  3. 3 public static void main(String[] args) {
  4. 4 ApplicationContext app = new ClassPathXmlApplicationContext("app1.xml");
  5. 5 // 獲取代理物件
  6. 6 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean");
  7. 7 // 通過代理物件執行目標物件的方法
  8. 8 int add = iTeacher.add(1, 2);
  9. 9 }
  10. 10 }
  11. 11
  12. 12 // 前置通知=====1======函式名:add
  13. 13 // 前置通知=====2======引數值:[1,2]
  14. 14 // 前置通知=====3======物件值:{}
  15. 15 // 執行目標方法:老師正在做加法,結果為:3

  原理剖析:案例中,重點在於理解ProxyFactoryBean這個Bean到底做了什麼?有一個前置通知,在目標方法前呼叫(從列印結果也能看出)。
  那麼,用靜態代理模式不難理解,簡單理解上面的實現如下,詳細的請檢視原始碼。

  1. 1 // 代理物件,靜態代理
  2. 2 public class ProxyFactoryBean implements ITeacher { // 2.指定要代理的介面集,即要實現哪些介面
  3. 3 // 目標物件,通過介面來聚合
  4. 4 private Teacher target;
  5. 5 // 前置通知
  6. 6 private MethodBeforeAdvice methodBeforeAdvice;
  7. 7
  8. 8 // 3.指定要織入的通知,即在執行目標物件方法要執行什麼程式碼
  9. 9 public void setMethodBeforeAdvice(MethodBeforeAdvice methodBeforeAdvice) {
  10. 10 this.methodBeforeAdvice = methodBeforeAdvice;
  11. 11 }
  12. 12
  13. 13 public ProxyFactoryBean(Teacher teacher) { // 1.指定要代理的目標物件
  14. 14 this.target = teacher;
  15. 15 }
  16. 16
  17. 17 @Override
  18. 18 public void teach() {
  19. 19
  20. 20 }
  21. 21
  22. 22 // 代理方法的編寫
  23. 23 @Override
  24. 24 public int add(int i, int j) {
  25. 25 try {
  26. 26 // 1.如果設定了前置通知,執行前置通知
  27. 27 if (methodBeforeAdvice != null) {
  28. 28 Method method = ITeacher.class.getMethod("add", int.class, int.class);
  29. 29 List<Object> objectList = new ArrayList<>();
  30. 30 objectList.add(i);
  31. 31 objectList.add(j);
  32. 32 Object[] objects = objectList.toArray();
  33. 33
  34. 34 methodBeforeAdvice.before(method, objects, target);
  35. 35 }
  36. 36
  37. 37 // 2.執行目標方法
  38. 38 return target.add(i, j);
  39. 39
  40. 40 // 3.如果設定了後置通知,執行後置通知
  41. 41 } catch (Throwable e) {
  42. 42 e.printStackTrace();
  43. 43 }
  44. 44 return 0;
  45. 45 }
  46. 46 }
  1. 1 // 測試類
  2. 2 public class Main {
  3. 3 public static void main(String[] args) {
  4. 4 // 建立代理物件,同時將建立的目標物件作為引數傳遞,即為誰代理.
  5. 5 ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean(new Teacher());
  6. 6 // 設定一個前置通知
  7. 7 proxyFactoryBean.setMethodBeforeAdvice(new MyMethodBeforeAdvice());
  8. 8 // 通過代理物件執行目標物件的方法.
  9. 9 proxyFactoryBean.add(1, 4);
  10. 10 }
  11. 11 }
  12. 12
  13. 13 // 前置通知=====1======函式名:add
  14. 14 // 前置通知=====2======引數值:[1,3]
  15. 15 // 前置通知=====3======物件值:{}
  16. 16 // 執行目標方法:老師正在做加法,結果為:4

3、通知類別

  除了案例中使用的前置通知,Spring中還提供瞭如下幾種通知:

通知型別
介面
描述
前置通知
org.springframework.aop.MethodBeforeAdvice
在目標方法前呼叫
後置通知
org.springframework.aop.AfterReturningAdvice
在目標方法後呼叫
環繞通知
org.aopalliance.intercept.MethodInterceptor
攔截對目標方法呼叫
異常通知
org.springframework.aop.ThrowsAdvice
目標方法丟擲異常時呼叫
引入通知
org.springframework.aop.support.NameMatchMethodPointcutAdvisor
可以指定切入點

  前置通知:介面提供了獲得目標方法,引數和目標物件的機會。該介面唯一能阻止目標方法被呼叫的途徑是丟擲異常或(System.exit())。
  後置通知:同前置通知類似。
  環繞通知:該通知能夠控制目標方法是否真的被呼叫。通過invocation.proceed()方法來呼叫。該通知可以控制返回的物件。可以返回一個與proceed()方法返回物件完全不同的物件。但要謹慎使用。特別注意:配置通知時,指定要織入的通知,環繞順序不同,會影響執行順序。
  異常通知:該介面為標識性(tag)介面,沒有任何方法,但實現該介面的類必須要有如下形式的方法:

  void afterThrowing(Throwable throwable);
  void afterThrowing(Method m,Object[] os,Object target,Exception e);
  第一個方法只接受一個引數:需要丟擲的異常。
  第二個方法接受異常、被呼叫的方法、引數以及目標物件

  引入通知:以前定義的通知型別是在目標物件的方法被呼叫的周圍織入。引入通知給目標物件新增新的方法和屬性。

  幾種通知:

  1. 1 // 前置通知(上面已介紹過)
  2. 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
  3. 3 // 目標方法、引數、目標物件
  4. 4 @Override
  5. 5 public void before(Method method, Object[] objects, Object target) throws Throwable {
  6. 6 System.out.println("前置通知=====1======函式名:" + method.getName());
  7. 7 System.out.println("前置通知=====2======引數值:" + JSON.toJSONString(objects));
  8. 8 System.out.println("前置通知=====3======物件值:" + JSON.toJSONString(target));
  9. 9 }
  10. 10 }
  11. 11
  12. 12 // 後置通知
  13. 13 public class MyAfterReturningAdvice implements AfterReturningAdvice {
  14. 14 // object:方法的返回值
  15. 15 // method:目標方法
  16. 16 // objects:引數
  17. 17 // target:目標物件.注:該物件由前置通知傳入的 target
  18. 18 @Override
  19. 19 public void afterReturning(Object object, Method method, Object[] objects, Object target) throws Throwable {
  20. 20 System.out.println("後置通知=====0======返回值:" + object);
  21. 21 System.out.println("後置通知=====1======函式名:" + method.getName());
  22. 22 System.out.println("後置通知=====2======引數值:" + JSON.toJSONString(objects));
  23. 23 System.out.println("後置通知=====3======物件值:" + JSON.toJSONString(target));
  24. 24 }
  25. 25 }
  26. 26
  27. 27 // 環繞通知
  28. 28 public class MyMethodInterceptor implements MethodInterceptor {
  29. 29 // object:目標方法的返回值
  30. 30 @Override
  31. 31 public Object invoke(MethodInvocation invocation) throws Throwable {
  32. 32 System.out.println("============環繞前==============");
  33. 33 Object object = invocation.proceed(); // 這裡會切入目標方法
  34. 34 System.out.println("============環繞後==============");
  35. 35 return object;
  36. 36 }
  37. 37 }
  38. 38
  39. 39 // 異常通知
  40. 40 public class MyThrowsAdvice implements ThrowsAdvice {
  41. 41
  42. 42 public void afterThrowing(Method method, Object[] objects, Object target, Exception e) {
  43. 43 System.out.println("異常通知=====1======函式名:" + method.getName());
  44. 44 System.out.println("異常通知=====2======引數值:" + JSON.toJSONString(objects));
  45. 45 System.out.println("異常通知=====3======物件值:" + JSON.toJSONString(target));
  46. 46 System.out.println("異常通知=====4======異常值:" + JSON.toJSONString(e.getMessage()));
  47. 47 }
  48. 48 }

  經過上面案例及四種通知的說明,可以看到,ProxyFactoryBean是一個在BeanFactory中顯式建立代理物件的中心類,可以給它一個要代理的目標物件、一個要代理的介面集、一個要織入的通知,他將建立一個嶄新的代理物件。

  引入通知(切點):前四種通知已經指明瞭在目標方法前,還是後,還是環繞呼叫。如果不能表達在什麼地方應用通知的話,通知將毫無用處,這就是切入點的用處。
  切入點決定了一個特定的類的特定方法是否滿足一定的規則。若符合,通知就應用到該方法上。引入通知可以指定切入點(即指定在哪些方法上織入通知)。
  注:引入通知並不像前4個介紹的那樣是一個通知。而重點思想在於:①在哪裡(切點,或者說方法)引入?②引入一個什麼樣的通知?
  規則如下:

符號
描述
示例
匹配
不匹配
.
匹配任何單個字元
setFoo.
setFooB
setFooBar setFooB
+
匹配前一個字元一次或多次
setFoo.+
setFooBar setFooB
setFoo
*
匹配前一個字元0次或多次
setFoo.*
setFoosetFooB, setFooBar
 
\
匹配任何正則表示式符號
\.setFoo.
bar.setFoo
setFoo

  下面給出一份完整的配置檔案 application.xml

  1. 1 <!-- 配置目標物件 -->
  2. 2 <bean id="teacher" class="com.lx.test.Teacher"/>
  3. 3
  4. 4 <!-- 配置前置通知 -->
  5. 5 <bean id="myMethodBeforeAdvice" class="com.lx.advice.MyMethodBeforeAdvice"/>
  6. 6 <!-- 配置後置通知 -->
  7. 7 <bean id="myAfterReturningAdvice" class="com.lx.advice.MyAfterReturningAdvice"/>
  8. 8 <!-- 配置環繞通知 -->
  9. 9 <bean id="myMethodInterceptor" class="com.lx.advice.MyMethodInterceptor"/>
  10. 10 <!-- 配置異常通知 -->
  11. 11 <bean id="myThrowsAdvice" class="com.lx.advice.MyThrowsAdvice"/>
  12. 12
  13. 13 <!-- 配置引入通知 -->
  14. 14 <bean id="pointcutAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
  15. 15 <property name="advice" ref="myMethodBeforeAdvice"/>
  16. 16 <property name="mappedNames">
  17. 17 <list>
  18. 18 <value>add*</value> <!-- 對以add開頭的函式織入前置通知 -->
  19. 19 <value>del*</value>
  20. 20 <value>teach*</value>
  21. 21 </list>
  22. 22 </property>
  23. 23 </bean>
  24. 24
  25. 25 <!-- 配置代理物件 -->
  26. 26 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
  27. 27 <!-- 指定要代理的目標物件 -->
  28. 28 <property name="target" ref="teacher"/>
  29. 29
  30. 30 <!-- 指定要代理的介面集 -->
  31. 31 <property name="proxyInterfaces">
  32. 32 <list>
  33. 33 <value>com.lx.test.ITeacher</value>
  34. 34 </list>
  35. 35 </property>
  36. 36
  37. 37 <!-- 指定要織入的通知 -->
  38. 38 <property name="interceptorNames">
  39. 39 <list>
  40. 40 <value>myMethodBeforeAdvice</value>
  41. 41 <value>myAfterReturningAdvice</value>
  42. 42 <value>myMethodInterceptor</value>
  43. 43 <value>myThrowsAdvice</value>
  44. 44
  45. 45 <value>pointcutAdvisor</value>
  46. 46 </list>
  47. 47 </property>
  48. 48 </bean>
  1. 1 // 測試類
  2. 2 public class Main {
  3. 3 public static void main(String[] args) {
  4. 4 ApplicationContext app = new ClassPathXmlApplicationContext("beans.xml");
  5. 5 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean");
  6. 6 int add = iTeacher.add(11, 22);
  7. 7 }
  8. 8 }
  9. 9
  10. 10 // 前置通知=====1======函式名:add
  11. 11 // 前置通知=====2======引數值:[11,22]
  12. 12 // 前置通知=====3======物件值:{}
  13. 13 // ============環繞前==============
  14. 14 // 前置通知=====1======函式名:add
  15. 15 // 前置通知=====2======引數值:[11,22]
  16. 16 // 前置通知=====3======物件值:{}
  17. 17 // 執行目標方法:老師正在做加法,結果為:33
  18. 18 // ============環繞後==============
  19. 19 // 後置通知=====0======返回值:33
  20. 20 // 後置通知=====1======函式名:add
  21. 21 // 後置通知=====2======引數值:[11,22]
  22. 22 // 後置通知=====3======物件值:{}
  23. 23
  24. 24 // row14、15、16是由於配置了引入通知又執行了一次前置通知

4、小結

  Spring在執行時通知物件,在執行期建立代理,不需要特殊的編譯器。Spring有兩種代理方式:
  若目標物件實現了若干介面:Spring使用JDK的java.lang.reflect.Proxy類代理。該類讓Spring動態產生一個新類,它實現了所需的介面,織入了通知和代理對目標物件的所有請求。
  若目標物件沒有實現任何介面:Spring使用CGLIB庫生成目標物件的子類。使用該方式時需要注意:
  ①對介面建立代理優於對類建立代理,因為會產生更加鬆耦合的系統。對類代理是讓遺留系統或無法實現介面的第三方類庫同樣可以得到通知,這種方式應該是備用方案。
  ②標記為final的方法不能夠被通知。Spring是為目標類產生子類,任何需要被通知的方法都需要被複寫,將通知織入。final方法是不允許重寫的。
  Spring實現了aop聯盟介面。Spring只支援方法連線點,不提供屬性接入點。Spring的觀點是屬性攔截破壞了封裝。面向物件的概念是物件自己處理工作,其他物件只能通過方法呼叫的得到的結果。
  問:說Spring的aop中,當你通過代理物件去實現aop的時候,獲取的ProxyFactoryBean是什麼型別?
  答:返回的是一個代理物件,如果目標物件實現了介面,則Spring使用jdk動態代理技術;如果目標物件沒有實現介面,則Spring使用CGLIB技術。