1. 程式人生 > >從代理模式再出發!ClassLoader初探

從代理模式再出發!ClassLoader初探

前面幾篇文章或多或少涉及到了代理模式的方方面面,從靜態代理到動態代理的介紹使用,到mybaties對代理模式的實際運用,我們會想到代理模式再深一層是什麼?我們只會用jdkproxy,但是在java語言的視角中,代理模式又是怎麼執行的? 這個系列文章將從代理模式再出發!探討跟代理相關的classloader,jvm位元組碼等概念,最終的目標是實現一個自己的代理框架。這些文章不是面面俱到的大談jvm,classloader等,只是一個總結,給的是一個研究的方向,自己也是剛剛入jvm的小白,多多總結,多多學習。

一.代理模式的原理

在我們寫java程式碼的時候,ide幫我們將java檔案編譯成class檔案,然後class檔案會交給jvm虛擬機器去執行,把java檔案編譯成class檔案這一過程中就有許多學問。我們可以在提交給jvm虛擬機器執行前修改class檔案(通過asm框架等方法),這個靈活的特性可以幫我們實現很多功能,比如我們的代理模式也是因為插手了這一步,修改了class檔案(對class檔案動了手術),然後虛擬機器接收到的檔案已經是修改後的class檔案,從而實現動態增強類功能,達到代理的目的。當然,在jdkproxy中,不是直接對被代理類動手術,而是建立了一個新類,這個新類實現了被代理類的介面,然後通過InvocationHandler物件實現動態代理功能。這樣做既對被代理類達到無損的效果,又實現了功能。

二.ClassLoader初探

前面說到“java檔案->class檔案->class檔案提交虛擬機器”,ClassLoader 在這個過程中扮演者很重要的角色,它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行,java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個Java類,即java.lang.Class類的一個例項。在java系統中,ClassLoader不是隻有一個,你可以自定義ClassLoader,只需要繼承ClassLoader即可,為什麼java設計允許多個ClassLoader呢?比如之前代理的需求,在你需要對某個class檔案特殊處理時,你渴望擁有自己的ClassLoader,在一些程式碼加密,熱部署之類的領域,可以寫自己特定需求的ClassLoader就顯得極為方便。後面我們會寫自己的ClassLoader,先理解完概念。

  • 引導類載入器(bootstrap class loader):它用來載入 Java 的核心庫,是用原生程式碼來實現的,並不繼承自
    java.lang.ClassLoader。
  • 擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java
    虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。
  • 系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入 Java
    類。一般來說,Java 應用的類都是由它來完成載入的。可以通過
    ClassLoader.getSystemClassLoader()來獲取它。

這裡寫圖片描述

在上圖中,可以看出java系統也有好幾層ClassLoader,它們似乎有聯絡,這就引出了“雙親委派模型”

雙親委派模型

每個ClassLoader有自己的父類,這個父類不是繼承概念上的父類,而是類似於責任鏈模式的關係,每一個ClassLoader有自己的上級(ext 和 bootstrap classloader 除外,它是最頂級),雙親委派的工作原理就是 當一個類載入器收到類載入請求,它會先看看自己是否以前載入過該類,如果有,會直接返回上次載入過的,如果沒有,則會給自己的上級類載入器載入,一級一級往上,直到父載入器說我找不到這個類,也就是載入不到這個類,這個類就會交給子載入器載入。

使用雙親委派模型有個很好的好處就是 不會不同的類載入器都載入同一個類,不然會造成程式混亂,因為對一個物件是否一樣,除了它的類名和包名是否相等,還要考慮是否是同一個類載入器載入的。

剛剛都是通過概念瞭解classloader,現在通過程式碼示例加深對classloader的直觀印象。

三.自己寫一個ClassLoader

在寫之前,我們先看一下classloader的入口方法,loadClass方法,如果你沒有複寫這個方法,預設就是雙親委派模型的形式,看原始碼:

synchronized (getClassLoadingLock(name)) {
            // 首先看看這個類是不是載入過,這裡有個快取機制
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    // 如果有父載入器,就先呼叫父載入器的loadClass方法
                        c = parent.loadClass(name, false);
                    } else {
                    // 如果該類載入器已經沒有父載入器,就直接找Bootstrap classloader,Bootstrap ClassLoader是用C++編寫的,在Java中看不到它
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果父載入器沒法找到該類,那就只能自己來載入了,呼叫findClass方法查詢。
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

整個過程就是雙親委派的程式碼實現,很簡單,但是這個實現卻保證了在java系統裡不會出現一個類被同時兩個類載入器載入的情況。

然後我們開始寫一個簡單的classloader:


public class SimpleClassLoader extends ClassLoader {

    //載入class類的入口靠的是這個方法,在雙親委派模型中,如果父載入器的findClass方法找不到類時,最後就會執行本方法。
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
//      bootstrap ext app 三個類載入器都找不到,最後還要我自己來找,那我也找不到,我就隨意直接返回Test類吧
        InputStream map = Test.class.getResourceAsStream("Test.class");
        try {
            byte[] bytes = new byte[map.available()];
            map.read(bytes);
            return super.defineClass(Test.class.getName(), bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    //    如果你的classloader不想破壞雙親委派模型,就不要自己複寫
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return super.loadClass(name);
    }
}

測試類 Test:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
        Class<?> aClass = simpleClassLoader.loadClass("JDK.Array.Man1");
        System.out.println(aClass);
    }
}

simpleClassLoader 用空的構造器會預設關聯 appclassloader ,這是預設 該執行緒關聯的classloader,每個執行中的執行緒都有一個成員contextClassLoader,用來在執行時動態地載入其它類,使用

Thread.currentThread().getContextClassLoader() ;

可以知道當前是appclassloader作為執行緒上下文類載入器。

然後我們呼叫入口方法 loadClass ,裡面寫一個不存在的類名,這樣根據雙親委派的規則,父載入器都不會載入到這個類,最後必須要用我們的findClass方法解決這個問題,再看一下我們複寫的findClass方法,我們直接用Test測試類作為載入的類,最後有一個defineClass方法,把位元組陣列 中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的例項。這個方法被宣告為 final的。所以這個方法是載入器的核心方法,且不能繼承複寫。

最後我們可以發現載入的類是由simpleClassLoader載入
這裡寫圖片描述

如果我們寫的類名是真實存在的,換句話說 appclassloader能夠載入到,那我們的simpleClassLoader就只是載入類的發起者,而真正的執行者卻是appclassloader

simpleClassLoader.loadClass("JDK.classLoader.Test");

這裡寫圖片描述

三.總結

我們在後面 自己實現動態代理的時候 需要用到calssloader,所以這篇文章只是大致的介紹,網上關於calssloader的文章也是數不勝數,感謝網上的一些資料,這裡做一個學習的總結。