1. 程式人生 > >Spring/Boot/Cloud系列知識(3)——代理模式(中)

Spring/Boot/Cloud系列知識(3)——代理模式(中)

2.2、代理模式在Spring中的應用

那麼java中基於java.lang.reflect.Proxy的動態代理模式和Spring生態有什麼關係呢?Spring中的所有Bean例項儲存在一個名叫IOC容器(Inversion of Control 控制反轉容器)中。這個容器中存在著介面和實現類的對應關係(也可以直接儲存類的例項,無需這個類有任何介面的實現),而其中Bean例項的儲存方式都預設採用單例儲存。

一般情況下我們可以通過BeanFactory(Spring IOC容器工廠)介面中getBean()方法,直接獲取到這個介面在IOC容器中對應的例項。但當我們需要為這個Bean例項附加AOP切面操作時,這個例項就會被代理——視這個例項實現介面的情況和Spring的配置情況,又可以區別為使用Proxy動態代理還是使用Cglib代理。如下圖所示:

  • 通常情況下當我們使用IOC容器中的Bean例項時,IOC容器將向使用者返回這個介面的具體實現,並不會代理這個Bean例項。如下圖所示:

這裡寫圖片描述

  • 當我們為這個Bean設定了AOP切面功能後,這個IOC容器中的Bean例項就會被代理。以下設定了一個AOP切面攔截(AOP切面的詳細講解在後續文章中進行,這裡我們只看被代理的結果就好):
// 建立一個AOP切面攔截器
@Aspect
@Component
public class MyInterceptor {
  /** 
   * 攔截yinwenjie.test.proxy.service包下面的所有類中的所有方法
   * 後續文章還會詳細講解Spring EL和AOP攔截規則 
   */
@Pointcut("execution(* yinwenjie.test.proxy.service..*.*(..))") public void executeService() { System.out.println("攔截器作用!"); } // 前置事件 @Before("executeService()") public void doBeforeAdvice(JoinPoint joinPoint){ System.out.println("前置事件!"); } }
  • 接著我們重新執行單元測試(重新執行程式碼),就會發現“MyService”介面的實現——MyServiceImpl被代理了。代理者是Spring基礎設施中aop模組的JdkDynamicAopProxy類。

這裡寫圖片描述

org.springframework.aop.framework.JdkDynamicAopProxy類非常重要,它實現了Java對動態代理模式的支援,既實現了java.lang.reflect.InvocationHandler介面(當然不止是實現了這個介面)。注意:需要設定您的Spring主配置檔案(這裡是Spring Boot)中spring.aop.proxy-target-class=false,否則就算您依賴注入的Bean存在介面,也會使用Cglib代理。

3. Cglib代理(動態)

要使用java對動態代理的原生支援,就需要被代理的類至少實現了一個介面。但如果某個需要被代理的類沒有實現任何介面,又怎麼辦呢?這時一個比較好的選擇就是使用Cglib代理,Cglib代理並不是java的原生代理模式,而是由第三方提供的一種代理方式。Cglib代理元件是Mockito Framework的一部分,其內部封裝了一個java位元組碼生成框架ASM:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or dynamically generate classes, directly in binary form. Provided common transformations and analysis algorithms allow to easily assemble custom complex transformations and code analysis tools.

以上引用自OW2組織對ASM的描述(http://asm.ow2.org)。簡單來說就是ASM位元組碼框架可在執行時動態生成Class位元組碼內容,或者擴充套件已有的某個Class。實際上Spring生態中預設使用Cglib動態代理作為主要的動態代理方式——不是java原生的java.lang.reflect.Proxy動態代理。無論被代理的Bean例項是否實現了任何介面,Cglib都能更好勝任代理工作任務。

3.1 Cglib元件基本使用

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
</dependency>

在本小節的示例講解中,我們需要在工程的maven檔案中匯入Cglib元件的引用(1.10.19版本)。接下來我們可以使用一個示例,來看看Cglib元件的基本使用。Cglib元件中的基本角色包括:

  • 代理器:代理器實現了org.mockito.cglib.proxy.MethodInterceptor介面,當代理者被外部使用者呼叫時,就會觸發其中的intercept方法。在本示例中,我們建立了兩個代理器DefaultProxy和CglibProxy:

    • DefaultProxy代理器
    // 預設的代理器,什麼都不執行
    public class DefaultProxy implements MethodInterceptor {
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 這裡不做任何處理(也可以直接使用proxy執行父級程式碼)
        return null;
      }
    }
    • CglibProxy代理器
    /**
     * 這是一個基於Cglib的代理器
     * 它實現了org.mockito.cglib.proxy.MethodInterceptor介面
     * @author yinwenjie
     */
    public class CglibProxy implements MethodInterceptor {
      // 日誌
      private static final Logger LOG = LoggerFactory.getLogger(CglibProxy.class);
      /**
       * 該方法在代理者被呼叫時觸發
       * @param obj 這是代理者,按照本示例就是使用Enhancer生成的子級類
       * @param method 被呼叫的方法
       * @param args 呼叫方法時可能傳入的引數
       * @param proxy cglib內部的方法代理,可以用它來執行真正的業務過程
       */
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        LOG.info("通過debug觀察Enhancer生成的子級類,是否有似曾相識的感覺?");
        LOG.info("被呼叫的方法:" + method.getName());
        LOG.info("可以在正式呼叫前執行一些額外的過程");
        // 這裡返回真正的業務呼叫過程
        Object result = null;
        try {
          result = proxy.invokeSuper(obj, args);
        } catch (Exception e) {
          LOG.error(e.getMessage() , e);
          LOG.info("在呼叫執行異常後,執行一些額外的過程");
          return null;
        }
        LOG.info("在呼叫執行成功後,執行一些額外的過程");
        return result;
      }
    }
  • 被代理者:被代理者就是負責真正業務過程處理的類。本示例中我們建立了一個TargetClass類,作為真正業務的處理者

/**
 * 這是要被代理的業務處理類<br>
 * 這個類既沒有任何父類,也沒有實現任何介面
 * @author yinwenjie
 */
public class TargetClass {
  // 日誌
  private static final Logger LOG = LoggerFactory.getLogger(TargetClass.class);
  public void handleSomething(String param) {
    LOG.info("傳入了一個引數:" + param + ",做了一些業務處理");
  }
}
  • 位元組碼增強操作器Enhancer

    Enhancer是Cglib中用於動態生成java位元組碼的最重要工具,其內部是對ASM框架的封裝。以下示例是Enhancer的基本使用:

public class Run {
  static {
    BasicConfigurator.configure();
  }
  public static void main(String[] args) {
    /*
     * Cglib的核心思路,是依靠一個java位元組碼操作框架ASM
     * 在程式碼執行時,為被代理的Class生成擴充套件程式碼(子級程式碼)
     * 然後通過執行子級程式碼,實現代理
     * 
     * Enhancer是其中用來進行程式碼生成的主要類,請看如下執行示例
     * */

    Enhancer enhancer = new Enhancer();
    // 基於被代理的TargetClass,為它建立一個子類進行代理執行
    enhancer.setSuperclass(TargetClass.class);
    // 設定代理器,這裡設定了兩個代理器DefaultProxy和CglibProxy(這裡也可以傳null)
    enhancer.setCallbacks(new Callback[]{new DefaultProxy() , new CglibProxy()});
    // 設定過濾器(很多資料都寫錯了用法,請參見本人MethodFilter中的註釋)
    enhancer.setCallbackFilter(new MethodFilter());

    // 在執行時動態建立代理類(TargetClass類的子類)
    TargetClass proxy = (TargetClass)enhancer.create();

    // 以下方法將被DefaultProxy代理
    proxy.toString();
    // 以下方法將被CglibProxy代理
    proxy.handleSomething("做一些對社會有益的事情!");
  }
  ……
}
  • 其它的角色——代理過濾器
    代理過濾器CallbackFilter的主要工作,是根據當前外部使用者所呼叫的代理者的方法,來決定使用哪一個代理器:
public class Run {
  ……
  static class MethodFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
      /*
       * 在main方法中,我們一共設定了兩個代理器,DefaultProxy和CglibProxy。
       * 當被執行的方法是Object類中的,類似clone、toString、hashCode這樣的方法,則使用DefaultProxy進行代理
       * 其他的方法,就是我們真正需要被代理的業務方法了,使用CglibProxy這個代理器進行代理
       * 
       * 這裡返回的結果,就是我們在main方法中使用enhancer.setCallbacks設定的代理器順序
       * 返回0,表示使用DefaultProxy代理器;返回1,表示使用CglibProxy代理器
       * 
       * 程式碼就不解釋了,很容易看懂。
       * 大家還可以參見org.mockito.cglib.proxy.Proxy.getProxyClass的原始碼,後文中我們還會提到這個Proxy類
       */
      if (method.getDeclaringClass().getName().equals("java.lang.Object")) {
        return 0;
      }
      return 1;
    }
  }
}
  • 下圖展示了debug過程中,觀察到的obj物件的結構效果。是否有似曾相似的感覺?

這裡寫圖片描述

以下是示例程式碼的執行結果:

09:27:36.939 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 通過debug觀察Enhancer生成的子級類,是否有似曾相識的感覺?
09:27:36.964 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 被呼叫的方法:handleSomething
09:27:36.964 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 在正式呼叫前執行一些額外的過程
09:27:36.998 [main] INFO yinwenjie.test.proxy.cglib.target.TargetClass - 傳入了一個引數:做一些對社會有益的事情!,做了一些業務處理
09:27:36.998 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 在呼叫執行成功後,執行一些額外的過程

3.2 Cglib替代Java原生動態代理

Cglib元件中有類似java原生動態代理的實現方式,可以完整替代java中原生動態代理的支援。這使得Spring預設使用Cglib動態代理作為主要的動態代理方式有了基礎支撐。本小節我們來看看Cglib如何提供類似java原生動態代理的支援(我們使用的Cglib版本還是1.10.19)。

  • 以下是介面和介面的實現類,也是我們需要進行進行代理的業務處理類。這裡的程式碼示例已經在之前的文章中出現過,這裡就不再贅述了:
// 被代理的介面
public interface TargetOneInterface {
  // 這是一個方法
  public void doSomething();
  // 這是第二個方法
  public void handleSomething();
}

......

// 介面實現,也是真正的業務處理類
public class TargetOneImpl implements TargetOneInterface {
  // 日誌
  private static final Logger LOG = LoggerFactory.getLogger(TargetOneImpl.class);
  @Override
  public void doSomething() {
    LOG.info("doSomething 方法被執行");
  }
  @Override
  public void handleSomething() {
    LOG.info("handleSomething 方法被執行");
  }
}
  • 以下我們基於Cglib提供的InvocationHandler介面,實現的代理器。注意,在Java的原生動態代理支援中,也有這個名稱相同的介面——包名不一樣:
......
import org.mockito.cglib.proxy.InvocationHandler;
......

// 代理者處理器
public class ProxyInvocationHandler implements InvocationHandler {
  /**
   * 模擬一個簡單的IOC容器<br>
   * 當然實際情況沒有這麼簡單,IOC內部有一個組織結構的
   */
  private static Map<Class<?>, Object> simulatorContainer = new HashMap<>();

  static {
    // 這是TargetOneInterface的實現
    simulatorContainer.put(TargetOneInterface.class, new TargetOneImpl());
  }

  /**
   * @param proxy 代理物件,注意是代理者,不是被代理者
   * @param method 被代理的方法
   * @param args 被執行的代理方法中傳入的引數
   */
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 被代理的介面
    Class<?> targetInterface = method.getDeclaringClass();
    // 從容器中取出執行類
    Object target = simulatorContainer.get(targetInterface);
    if(target == null) {
      return null;
    }
    // 開始呼叫
    return method.invoke(target, args);
  }
}

在以上這個代理器示例中,我們模擬了一個簡易的IOC容器,並根據被代理的介面從這個簡易的容器中取出介面對應的真實業務處理類,最後執行。

  • 以下是呼叫程式碼和執行結果
public class Run {

  static {
    BasicConfigurator.configure();
  }

  public static void main(String[] args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    //這是Cglib代理處理器
    ProxyInvocationHandler invocationHandler = new ProxyInvocationHandler();
    Object proxy = Proxy.newProxyInstance(classLoader,new Class<?>[]{TargetOneInterface.class}, invocationHandler);

    // 開始呼叫(測試第一個介面)
    TargetOneInterface targetOne = (TargetOneInterface)proxy;
    targetOne.doSomething();
    targetOne.handleSomething();
  }
}

對以上程式碼進行debug操作,會發現類似如下的除錯資訊:

這裡寫圖片描述

從除錯資訊可以看出,當我們呼叫org.mockito.cglib.proxy.Proxy.newProxyInstance這個靜態方法時,一旦執行成功,該方法就將返回一個由Cglib元件動態生成的類。上圖中表示為:這裡寫圖片描述。後文內容中我們會閱讀org.mockito.cglib.proxy.Proxy.newProxyInstance中的原始碼,來講解其中的工作內容。以下是示例的執行結果:

[main] INFO yinwenjie.test.proxy.cglib.dproxy.target.TargetOneImpl - doSomething 方法被執行
[main] INFO yinwenjie.test.proxy.cglib.dproxy.target.TargetOneImpl - handleSomething 方法被執行

=========================
(接後文,Spring中對Cglib的封裝、Cglib中newProxyInstance實現過程等內容)

相關推薦

Spring/Boot/Cloud系列知識3——代理模式

2.2、代理模式在Spring中的應用 那麼java中基於java.lang.reflect.Proxy的動態代理模式和Spring生態有什麼關係呢?Spring中的所有Bean例項儲存在一個名叫IOC容器(Inversion of Control 控制反

23種設計模式代理模式python_c++實現

23種設計模式之(六)代理模式(Proxy) 本文主要介紹23種設計模式之原型模式,附詳細python/c++示例程式碼。 - 概念 - 應用場景 - 注意事項 - 程式碼示例 - 總結 - 程式碼連結 代理模式(Proxy) 概念

spring boot學習系列

web服務器 應用程序 spring 控制器 做什麽 spring boot開發第一個應用程序1、spring boot是什麽?2、spring boot容易上手嗎?寫這篇文章技術文章,主要是記錄日常的學習以及理解。我們重新認識一下spring假設你受命使用spring開發一個簡單的hel

Spring Boot入門系列資源文件屬性配置

response mage 註意 site spa website 圖片 process ram   Spring Boot 資源文件屬性配置     配置文件是指在resources根目錄下的application.properties或application.yml配置

Spring Boot入門系列FreeMarker模板的使用

前端 start marker pin 用戶 需要 類庫 準備 control FreeMarker模板的使用   FreeMarker是一款模板引擎: 即一種基於模板和要改變的數據, 並用來生成輸出文本(HTML網頁、電子郵件、配置文件、源代碼等)的通用工具。 它不是面向

Spring Cloud Spring Boot mybatis分布式微服務雲架構使用IntellijSpring Initializr來快速構建Spring Boot/Cloud工程

follow 體驗 alt initial ali roo 進行 依賴管理 img 在之前的所有Spring Boot和Spring Cloud相關博文中,都會涉及Spring Boot工程的創建。而創建的方式多種多樣,我們可以通過Maven來手工構建或是通過腳手架等方式快

Spring Boot 學習系列05—自定義檢視解析規則

此文已由作者易國強授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 自定義檢視解析 在預設情況下Spring Boot 的MVC框架使用的檢視解析ViewResolver類是ContentNegotiatingViewResolver,這個解析器比較智慧,它會根據你的請求型別(一

Spring Boot 學習系列07—properties檔案讀取

此文已由作者易國強授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 傳統的properties讀取方式 一般的,我們都可以自定義一個xxx.properties檔案,然後在工程的xml配置檔案中注入相關的配置bean,示例如下:<context:property-placeh

Spring Boot 學習系列03—jar or war,做出你的選擇

此文已由作者易國強授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 兩種打包方式 採用Spring Boot框架來構建專案,我們對專案的打包有兩種方式可供選擇,一種仍保持原有的方式不變,package一個war包放置到外接的應用容器中;另一種則是直接打包成一個

Spring Boot 學習系列01—從0到1,只需兩分鐘

此文已由作者易國強授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 快速構建 如果我們想基於spring mvc 提供一個簡單的API查詢服務,傳統的方式,首先需要我們引入spring-mvc/spring-context等等各項依賴包,然後配置相關的引數

Spring Boot 學習系列08—自定義servlet、filter及listener

此文已由作者易國強授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 傳統的filter及listener配置 在傳統的Java web專案中,servlet、filter和listener的配置很簡單,直接在web.xml中按順序配置好即可,程式啟動時,就會按照你配置的順序依次載入

Spring Boot 學習系列09—自定義Bean的順序加載

rri 學習 內容安全 master sys const nco 單純 分享圖片 此文已由作者易國強授權網易雲社區發布。歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。Bean 的順序加載有些場景中,我們希望編寫的Bean能夠按照指定的順序進行加載。比如,有UserSer

Spring Boot 基礎系列教程 | 第十二篇:使用Spring-data-jpa簡化資料訪問層推薦

推薦 Spring Boot/Cloud 視訊: Spring Boot中使用Spring-data-jpa讓資料訪問更簡單、更優雅 在上一篇Spring中使用JdbcTemplate訪問資料庫 中介紹了一種基本的資料訪問方式,結合構建RESTful API和

Spring Boot乾貨系列預設日誌logback配置解析

前言 今天來介紹下Spring Boot如何配置日誌logback,我剛學習的時候,是帶著下面幾個問題來查資料的 如何引入日誌? 日誌輸出格式以及輸出方式如何配置? 程式碼中如何使用? 正文       Spring Boot在所有

Spring Boot乾貨系列靜態資源和攔截器處理

正文     前面章節我們也有簡單介紹過SpringBoot中對靜態資源的預設支援,今天詳細的來介紹下預設的支援,以及自定義擴充套件如何實現。 預設資源對映 Spring Boot 預設為我們提供了靜態資源處理,使用 WebMvcAutoConfiguration 中

Spring Boot乾貨系列開發Web應用之JSP篇

前言     上一篇介紹了Spring Boot中使用Thymeleaf模板引擎,今天來介紹一下如何使用SpringBoot官方不推薦的jsp,雖然難度有點大,但是玩起來還是蠻有意思的。 正文      先來看看整體的框架結構,跟前面介紹

Spring Boot乾貨系列開發Web應用之Thymeleaf篇

 前言       Web開發是我們平時開發中至關重要的,這裡就來介紹一下Spring Boot對Web開發的支援。 正文      Spring Boot提供了spring-boot-starter-web為Web開

Spring Boot乾貨系列啟動原理解析

前言       前面幾章我們見識了SpringBoot為我們做的自動配置,確實方便快捷,但是對於新手來說,如果不大懂SpringBoot內部啟動原理,以後難免會吃虧。所以這次博主就跟你們一起一步步揭開SpringBoot的神祕面紗,讓它不在神祕。 正文 我

Spring Boot乾貨系列優雅的入門篇

        首先宣告,Spring Boot不是一門新技術,所以不用緊張。從本質上來說,Spring Boot就是Spring,它做了那些沒有它你也會去做的Spring Bean配置。它使用“習慣優於配置”(專案中存在大量的配置,此外還內建了一個習慣性的配

【直播預告】:Java Spring Boot實戰系列課程第十講Spring Boot 2.0實戰高併發分散式快取

內容概要:Redis作為開源分散式高併發快取,在網際網路公司高併發系統中廣泛使 用,本次課程講解如何使用最新的Java Spring Data實戰Redis,以及底層API的實現原始碼。主講人:徐雷(阿里雲棲特邀Java專家)直播時間:2019年1月1日 週二 今晚20:00直播地點:【阿里Java技術進階】