1. 程式人生 > >Spring學習(二十五)Spring AOP之增強介紹

Spring學習(二十五)Spring AOP之增強介紹

課程概要:
  • Spring AOP的基本概念
  • Spring AOP的增強型別
  • Spring AOP的前置增強
  • Spring AOP的後置增強
  • Spring AOP的環繞增強
  • Spring AOP的異常丟擲增強
  • Spring AOP的引介增強
一.Spring AOP增強的基本概念 Spring當中的專業術語-advice,翻譯成中文就是增強的意思。 所謂增強,其實就是向各個程式內部注入一些邏輯程式碼從而增強原有程式的功能。 二.Spring AOP的增強型別
首先先了解一下增強介面的繼承關係 如上圖所示: 其中帶Spring標誌的是Spring定義的擴充套件增強介面 其中帶aopalliance標誌的是AOP聯盟所定義的介面
按照增加在目標類方法連線點的位置可以將增強劃分為以下五類:
  • 前置增強   (org.springframework.aop.BeforeAdvice)   表示在目標方法執行前來實施增強
  • 後置增強   (org.springframework.aop.AfterReturningAdvice)   表示在目標方法執行後來實施增強
  • 環繞增強   (org.aopalliance.intercept.MethodInterceptor)   表示在目標方法執行前後同時實施增強
  • 異常丟擲增強   (org.springframework.aop.ThrowsAdvice)   表示在目標方法丟擲異常後來實施增強
  • 引介增強   (org.springframework.aop.introductioninterceptor)   表示在目標類中新增一些新的方法和屬性
其中,引介增強是一種特殊的增強。他可以在目標中新增屬性和方法,通過攔截定義一個介面,讓目標代理實現這個介面。他的連線點是級別的,而前面的幾種則是方法級別的。 其中,環繞增強是AOP聯盟定義的介面,其他四種增強介面則是Spring定義的介面。 其實,AOP增強很簡單: 通過實現這些增強介面,在實現這些介面的方法當中定義橫切邏輯,然後通過配置Spring的配置檔案就可以完成將增強織入到目標方法當中了。 補充:增強既包含了橫切邏輯同時又包含了部分連線點資訊。
三.Spring AOP的前置增強 1.通過程式碼實現增強 在Spring當中,僅支援方法級別的增強,利用MethodBeforeAdvice實現,表示在目標方法執行前實施增強。 示例演示: 對服務生的服務用語進行強制規範。我們假設服務生只需要幹兩件事情:1.歡迎顧客 2.對顧客提供服務 那麼我們建立的示例程式碼的主要步驟如下:
  1. 建立業務介面類:Waiter.java
  2. 建立業務實現類:NativeWaiter.java
  3. 建立業務增強類:GreetingBeforeAdvice.java
  4. 建立增強測試類:TestAdvice.java
接下來我們分別在IDEA中建立相應的類。 服務生介面Waiter.java
public interface Waiter {
    void greetTo(String name);
    void serverTo(String name);
}
服務生實現類NativeWaiter.java
public class NativeWaiter implements Waiter{
    public void greetTo(String name) {
        System.out.println("greet to"+name+"...");
    }

    public void serverTo(String name) {
        System.out.println("serving"+name+"...");
    }
}
服務生業務增強類GreetingBeforeAdvice.java
public class GreetingBeforeAdvice implements MethodBeforeAdvice{
    /**
    * 前置增強方法
    * 當該方法發生異常時,將阻止目標方法的執行
    * @param method 目標類方法
    * @param objects 目標類方法入參
    * @param o 目標類物件例項
    * @throws Throwable
    */
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        String clientName=(String)objects[0];
        System.out.println("How Are You! mr."+clientName);
    }
}

增強測試類TestBeforeAdvice.java
public class TestBeforeAdvice {
    public static void main(String[] args){
        //建立目標物件
        Waiter target=new NativeWaiter();
        //建立增強類物件
        BeforeAdvice advice=new GreetingBeforeAdvice();
        //建立代理工廠物件
        ProxyFactory factory=new ProxyFactory();
        //設定代理類
        factory.setTarget(target);
        //新增增強類
        factory.addAdvice(advice);
        //獲取代理類
        Waiter proxy=(Waiter)factory.getProxy();
        //呼叫目標類方法
        proxy.greetTo("icarus");
        proxy.serverTo("icarus");

    }
}
程式執行結果: How Are You! mr.icarus greet toicarus... How Are You! mr.icarus servingicarus... 2.ProxyFactory介紹 其實ProxyFactory代理技術就是利用jdk代理或者cglib代理的技術,將增強應用到目標類當中。 Spring定義的AOP Proxy類具有兩個final型別的實現類,如下圖所示:

其中: Cglib2AopProxy是使用cglib代理技術來建立代理 JdkDynamicAopProxy是使用jdk代理技術來建立代理 那麼使用JDK代理來實現上面的程式碼則為:
//建立代理工廠物件
ProxyFactory factory=new ProxyFactory();
//設定代理介面
factory.setInterfaces(target.getClass().getInterfaces());
//設定代理類
factory.setTarget(target);
//設定增強類
factory.addAdvice(advice);

使用CGLib代理則為:
ProxyFactory factory=new ProxyFactory();
//設定代理介面
factory.setInterfaces(target.getClass().getInterfaces());
//啟用cglib代理方式
factory.setOptimize(true);
//設定代理類
factory.setTarget(target);
//新增增強類
factory.addAdvice(advice);

可以觀察到,ProxyFactory通過addAdvice來增加一個增強。 使用者可以使用該方法增加多個增強,通過增強形成一個增強鏈,他們的呼叫順序和新增順序是一致的 3.通過配置檔案實現增強 我們也可以通過配置檔案來實現Spring的前置增強,並且大多數情況下都是使用配置檔案方式。 首先我們介紹下ProxyFactory Bean配置檔案當中常用的屬性:
  • target:我們需要代理的目標物件
  • proxyInterfaces:代理所要實現的介面,可以是多個介面
  • interceptorNames:需要織入的目標物件的Bean的列表(增強類的Bean列表),使用Bean的名稱來指定。
  • singleton:確定返回的代理是不是單例項的,系統預設返回的是單例項的。
  • optimize:當值為true時,強制使用cglib代理。當是singleton的例項時我們推薦使用cglib代理,當是其他作用域的時候,推薦使用JDK的代理。原因是cglib建立代理速度比較慢,但是執行效率高。JDK代理則剛好相反。
  • proxyTargetClass:是否對進行代理而不是對介面進行代理,當值為true的時候使用cglib代理
接下來我們使用配置檔案對上面的示例程式碼進行配置:
<?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:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="gerrtingBefore" class="cn.lovepi.chapter07.aop.advice.GreetingBeforeAdvice"/>
    <bean id="target" class="cn.lovepi.chapter07.aop.advice.NativeWaiter"/>
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:proxyInterfaces="cn.lovepi.chapter07.aop.advice.Waiter"
          p:interceptorNames="gerrtingBefore"
          p:target-ref="target"
          />

</beans>

接下來我們建立對應的測試檔案
public class TestBeforeAdviceByXml {
    public static void main(String[] args){
        String path="src/conf/conf-advice.xml";
        ApplicationContext context=new FileSystemXmlApplicationContext(path);
        Waiter waiter=context.getBean("waiter",Waiter.class);
        waiter.greetTo("icarus");
        waiter.serverTo("icarus");

    }
}

可以看到輸出結果為: How Are You! mr.icarus greet toicarus... How Are You! mr.icarus servingicarus... 和我們通過程式碼實現增強的結果相同 四.Spring AOP的後置增強 後置增強在目標方法呼叫後執行,例如上面的例子中,在服務生每次服務後,也需要向客人問候,可以通過後置增強來實施這一要求,步驟如下:
  1. 建立業務介面類:Waiter.java
  2. 建立業務實現類:NativeWaiter.java
  3. 建立業務增強類:GreetingAfterAdvice.java
  4. 建立配置檔案:conf-advice.xml
  5. 建立增強測試類:TestAdvice.java
接下來我們在IDEA中建立相應的程式碼: 我們繼續使用上面的例子,由於Waiter.java和NativeWaiter.java已經建立好了 我們只需建立GreetingAfterAdvice.java
public class GreetingAfterAdvice  implements AfterReturningAdvice{
    /**
    * 後置增強程式碼實現
    * @param o 代理返回物件
    * @param method 目標物件方法
    * @param objects 目標物件方法引數
    * @param o1 目標物件
    * @throws Throwable
    */
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("please enjoy youself!");
    }
}

接下來我們修改對應的配置檔案 首先得將後者增強類作為bean配置到檔案當中 <bean id="gerrtingAfter" class="cn.lovepi.chapter07.aop.advice.GreetingAfterAdvice"/>
接下來得在ProxyFactory Bean當中新增織入的bean
p:interceptorNames="gerrtingBefore,gerrtingAfter"
完整的配置檔案如下:
<?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:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="gerrtingBefore" class="cn.lovepi.chapter07.aop.advice.GreetingBeforeAdvice"/>
    <bean id="gerrtingAfter" class="cn.lovepi.chapter07.aop.advice.GreetingAfterAdvice"/>
    <bean id="target" class="cn.lovepi.chapter07.aop.advice.NativeWaiter"/>
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:proxyInterfaces="cn.lovepi.chapter07.aop.advice.Waiter"
          p:interceptorNames="gerrtingBefore,gerrtingAfter"
          p:target-ref="target"
          />

</beans>

測試檔案和上面的保持不變,執行測試類,測試結果為: How Are You! mr.icarus greet toicarus... please enjoy youself! How Are You! mr.icarus servingicarus... please enjoy youself! 五.Spring AOP的環繞增強 環繞增強允許在目標類方法呼叫前後織入橫切邏輯,它綜合實現了前置,後置增強兩者的功能,下面是我們用環繞增強同時實現上面的我們的示例。步驟如下:
  1. 建立業務介面類:Waiter.java
  2. 建立業務實現類:NativeWaiter.java
  3. 建立業務增強類:GreetingInterceptor.java
  4. 建立配置檔案:conf-advice.xml
  5. 建立增強測試類:TestAdvice.java
接下來我們在IDEA中來實現。 首先建立GreetingInterceptor.java
public class GreetingInterceptor implements MethodInterceptor{
    /**
    * 業務邏輯實現類
    * @param methodInvocation 封裝了目標方法和入引數組以及目標方法所帶的例項物件
    * @return 代理物件
    * @throws Throwable
    */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        //獲取目標方法的入參
        Object[] args=methodInvocation.getArguments();
        //獲取方法名稱
        String clickName= (String) args[0];
        System.out.println("GreetingInterceptor:How are you!");
        //利用反射機制來呼叫目標方法
        Object object=methodInvocation.proceed();
        System.out.println("GreetingInterceptor: please enjoy youself!");
        return object;
    }
}

接下來在配置檔案中對其進行配置:
<?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:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="gerrtingBefore" class="cn.lovepi.chapter07.aop.advice.GreetingBeforeAdvice"/>
    <bean id="gerrtingAfter" class="cn.lovepi.chapter07.aop.advice.GreetingAfterAdvice"/>
    <bean id="gerrtingAround" class="cn.lovepi.chapter07.aop.advice.GreetingInterceptor"/>
    <bean id="target" class="cn.lovepi.chapter07.aop.advice.NativeWaiter"/>
    <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:proxyInterfaces="cn.lovepi.chapter07.aop.advice.Waiter"
          p:interceptorNames="gerrtingBefore,gerrtingAfter,gerrtingAround"
          p:target-ref="target"
          />

</beans>

啟動測試類,觀察列印結果: How Are You! mr.icarus GreetingInterceptor:How are you! greet toicarus... GreetingInterceptor: please enjoy youself! please enjoy youself! How Are You! mr.icarus GreetingInterceptor:How are you! servingicarus... GreetingInterceptor: please enjoy youself! please enjoy youself! 可以看到,我們成功在示例中實現了前置增強,後者增強以及環繞增強。 六.Spring AOP的異常丟擲增強 異常丟擲增強表示在目標方法丟擲異常後實施增強,最適合的場景是事務管理,比如當參與事事務的方法丟擲異常後需要回滾事務。 異常丟擲增強類需要實現ThrowsAdvice介面,ThrowsAdvice介面並沒有定義任何的方法,他只是一個標誌介面。 在執行期,Spring採用反射的機制來進行判斷。我們必須採用以下的形式來定義異常丟擲的方法 public void afterThrowing(Method method,Object[] args,Object target,Throwable t)
其中: 方法名必須為afterThrowing,方法入參中前三個入參是可選的,即要麼同時存在,要麼都沒有 最後一個入參是Throwable及其子類,必須得有。 也可以在異常增強類中定義多個方法,Spring會自動選擇匹配的方法來進行呼叫。 在類的繼承樹上,兩個類的距離越近,則兩個類的相似度越高 那麼當方法丟擲異常時,會優先選取異常入參和丟擲的異常相似度最高的afterThrowing方法。 接下來我們建立示例來演示一下,步驟如下:
  1. 建立業務實現類:ForumService.java
  2. 建立業務增強類:TransactionManager.java
  3. 建立配置檔案:conf-advice.xml
  4. 建立增強測試類:TestAdvice.java
接下來我們在IDEA上分別建立對應的程式碼: 首先,我們建立業務邏輯類ForumService
public class ForumService {
    public void removeForum(){
        //進行相應的資料庫操作,但這裡只為演示丟擲異常
        throw new RuntimeException("removeForum:Exception...");
    }
    public void updateForum(){
        //進行相應的資料庫操作,但這裡只為演示丟擲異常
        throw new RuntimeException("updateForum:Exception...");
    }
}
接下來我們建立增強類TransactionManager
public class TransactionManager implements ThrowsAdvice{
    /**
    * 捕獲異常並列印異常名稱
    * @param method 目標物件對應方法
    * @param args 方法入參
    * @param target 目標物件
    * @param ex 執行方法所捕獲的異常
    * @throws Throwable
    */
    public void afterThrowing(Method method,Object[] args,Object target,Exception ex)throws Throwable{
        System.out.println("method:"+method.getName());
        System.out.println("丟擲異常:"+ex.getMessage());
        System.out.println("成功回滾事務");
    }
}
接下來我們編寫對應的配置檔案
<?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:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="forumServiceTarget" class="cn.lovepi.chapter07.aop.advice.ForumService"/>
    <bean id="transactionManager" class="cn.lovepi.chapter07.aop.advice.TransactionManager"/>
    <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:proxyTargetClass="true"
          p:target-ref="forumServiceTarget"
          p:interceptorNames="transactionManager"
          />
</beans>
建立相應的測試類進行測試
public static void testThrowAdvice(){
    String path="src/conf/conf-advice.xml";
    ApplicationContext context=new FileSystemXmlApplicationContext(path);
    ForumService forumService=context.getBean("forumService",ForumService.class);
    try {
        forumService.removeForum();
    }catch (Exception e){}
    try {
        forumService.updateForum();
    }catch (Exception e){}
}
執行結果為: method:removeForum 丟擲異常:removeForum:Exception... 成功回滾事務 method:updateForum 丟擲異常:updateForum:Exception... 成功回滾事務 七.Spring AOP的引介增強 引介增強是一種比較特殊的增強型別,他不是在目標方法周圍織入增強,而是為目標建立新的方法和屬性,所以他的連線點是類級別的而非方法級別的。通過引介增強我們可以為目標類新增一個介面的實現即原來目標類未實現某個介面,那麼通過引介增強可以為目標類建立實現某介面的代理。 接下來我們建立一個示例來演示下,步驟如下:
  • 建立介面類:Monitorable.java
  • 建立業務類:PerformanceMonitor.java
  • 建立增強類:ControllablePerformanceMonitor.java
  • 建立配置檔案:conf-advice-introduce.xml
  • 建立增強測試類:TestIntroduce.java
接下來我們在IDEA上分別建立對應的程式碼: 首先建立效能監視介面Monitorable
public interface Monitorable {
    void setMonitorActive(boolean active);
}
建立測試介面Testable
public interface Testable {
  void test();
}
接下來建立業務類
public class PerformanceMonitor {
  private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>();
  public static void begin(String method) {
      System.out.println("begin monitor...");
      MethodPerformace mp = performaceRecord.get();
      if(mp == null){
        mp = new MethodPerformace(method);
        performaceRecord.set(mp);
      }else{
          mp.reset(method);
      }
  }
  public static void end() {
      System.out.println("end monitor...");
      MethodPerformace mp = performaceRecord.get();
      mp.printPerformace();
  }
}

接下來建立增強類ControllablePerformanceMonitor
public class ControllablePerformaceMonitor
      extends
        DelegatingIntroductionInterceptor implements Monitorable, Testable {
  private ThreadLocal<Boolean> MonitorStatusMap = new ThreadLocal<Boolean>();
  public void setMonitorActive(boolean active) {
      MonitorStatusMap.set(active);
  }
  public Object invoke(MethodInvocation mi) throws Throwable {
      Object obj = null;
      if (MonitorStatusMap.get() != null && MonitorStatusMap.get()) {
        PerformanceMonitor.begin(mi.getClass().getName() + "."
              + mi.getMethod().getName());
        obj = super.invoke(mi);
        PerformanceMonitor.end();
      } else {
        obj = super.invoke(mi);
      }
      return obj;
  }
  public void test() {
      // TODO Auto-generated method stub
      System.out.println("dd");
  }
}
接下來建立所要增強的方法類
public class ForumService {

  public void removeTopic(int topicId) {
      System.out.println("模擬刪除Topic記錄:"+topicId);
      try {
        Thread.currentThread().sleep(20);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }

  }

  public void removeForum(int forumId) {
      System.out.println("模擬刪除Forum記錄:"+forumId);
      try {
        Thread.currentThread().sleep(40);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
  }
}

public class MethodPerformace {
  private long begin;
  private long end;
  private String serviceMethod;
    public MethodPerformace(String serviceMethod){
      reset(serviceMethod);
    }
    public void printPerformace(){
        end = System.currentTimeMillis();
        long elapse = end - begin;
        System.out.println(serviceMethod+"花費"+elapse+"毫秒。");
    }
    public void reset(String serviceMethod){
      this.serviceMethod = serviceMethod;
      this.begin = System.currentTimeMillis();
    }
}
建立配置檔案來將所設定的程式碼組合起來:
<?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:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="pmonitor" class="cn.lovepi.chapter07.aop.intorduce.ControllablePerformaceMonitor" />
  <bean id="forumServiceTarget" class="cn.lovepi.chapter07.aop.intorduce.ForumService" />
  <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interfaces="cn.lovepi.chapter07.aop.intorduce.Monitorable"
      p:target-ref="forumServiceTarget"
      p:interceptorNames="pmonitor" p:proxyTargetClass="true" />

</beans>
建立對應的測試類
public class TestIntroduce {
  public static void main(String[] args) {
      testBeforeAdviceByCode();
  }

  private static void testBeforeAdviceByCode() {
      String configPath = "src/conf/conf-advice-introduce.xml";
      ApplicationContext ctx = new FileSystemXmlApplicationContext(configPath);
        ForumService forumService = (ForumService)ctx.getBean("forumService");
        forumService.removeForum(10);
        forumService.removeTopic(1022);
        Monitorable moniterable = (Monitorable)forumService;
        moniterable.setMonitorActive(true);
        forumService.removeForum(10);
        forumService.removeTopic(1022);
  }

}
程式執行結果為: 模擬刪除Forum記錄:10 模擬刪除Topic記錄:1022 begin monitor... 模擬刪除Forum記錄:10 end monitor... org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.removeForum花費40毫秒。 begin monitor... 模擬刪除Topic記錄:1022 end monitor... org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.removeTopic花費20毫秒。 總結 增強其實就是對原有的方法或類動態增加功能,可為方法執行前後以及所丟擲的異常進行邏輯處理。實現增強的方式有兩種:程式碼方式和XML配置檔案方式,建議在以後開發中使用後者,這樣可以避免程式碼的耦合度,方便後期維護。