Java進階04-動態代理、類加載

動態代理和類加載

本節主要複習動態代理和類加載機制。這2個知識點是非常重要的,也是很常見的,可能我們自己用的並不多,但是很多框架中的基礎都是它們2個。如果不知道這些知識 那麼看那些開源框架的源碼就會很喫力。是謂 基礎不牢地動山搖
在這裏插入圖片描述

類加載

還是按標準的靈魂5問來學習:

  • 什麼是類加載
  • 怎麼使用類加載
  • 類加載的優缺點
  • 類加載的原理
  • 類加載的使用場景

什麼是類加載
類加載是一種機制是一套流程和動作。我們寫的源碼其實就是文本文件,如我們寫的第一個java程序 HelloWorld.java 。這個就是源文件給人看的。
在這裏插入圖片描述
但是這個文件並不能直接運行, 需要編譯成HelloWorld.class 這個是字節碼文件 打開裏面其實是二進制,但是一般不會顯示0101這種而是會以16進制顯示。這個給java虛擬機看的。
在這裏插入圖片描述
你要看到話需要對着-java虛擬機規範表才知道代表什麼意思。這個也是不能直接在電腦上運行的。它只能運行在JVM中,怎麼運行,當我們執行java HelloWorld
在這裏插入圖片描述
這裏java 就代表運行虛擬機,後面的HelloWorld就是 我們的字節碼,意思就是運行虛擬機 然後在虛擬機上運行HelloWorld 字節碼。虛擬機會把字節碼 翻譯成對應操作系統能識別的機器碼就可以在電腦上運行了。
類加載就是:虛擬機通過全限定名(就是路徑)把字節碼文件讀取進內存,怎麼讀?肯定是IO啊,然後進行必要的驗證、準備、解析、初始化等工作 最後轉爲規定的數據結構存儲在內存中方法區,並且生成一個Class對象來描述這個類文件,後續通過這個Class就可以訪問到這個類文件。

怎麼使用類加載
首先需要搞清楚 類加載器有哪些,並不是所有的 類都是又一種類加載器 去加載的,比如JDK自帶的類,我們自己寫的類。根據官方文檔類加載器有3種:

  1. BootStrap ClassLoder —BootstrapClassLoader引導類加載器,這個是最頂層的類加載器,它主要加載核心類庫,JRE中的庫 具體就是%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。這裏提一句JDK:java develop kit 就是開發工具包。 JRE:Java Runtime Enviroment是指Java的運行環境。
    在這裏插入圖片描述

  2. Extention ClassLoader --ExtClassLoader擴展類加載器,加載我們開發用的到基礎庫,不懂的可以點進去看看有哪些類具體目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。
    在這裏插入圖片描述

  3. Appclass ClassLoader—AppClassLoader也稱爲SystemAppClass 加載當前應用的classpath的所有類。就是你自己寫的類。
    在這裏插入圖片描述
    BootstrapClassLoader這個類在虛擬機中是C++編寫的。不過在Launcher中也有一個BootClassPathHolder 私有的靜態內部類
    AppClassLoader和ExtClassLoader 是java編寫的在 sun.misc包下 其實是Launcher中的2個靜態內部類。
    這3個類我們都改了不了什麼,系統的東西。
    不過我們可以自定義自己的類加載器 只要繼承ClassLoader就行了。舉個栗子:
    步驟也很簡單:
    1.寫一個類繼承自ClassLoader抽象類。
    2.複寫它的findClass()方法。
    3.在findClass()方法中調用defineClass()。
    如:

package com.zh.loader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * @descriable 自定義蘋果 類 加載器
 */
public class AppleLoader extends ClassLoader {
	private static String TAG = "AppleLoader";
	/**就是.class文件的路徑 如果是放在D盤 則 D:/*/
	private String mDir;
	public AppleLoader(String dir) {
		MainLoader.println(TAG, dir);
		this.mDir = dir;
	}
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		//這個name是全限定名 什麼意思?
		//其實就是包名加類名。如本類的全限定名就是 com.zh.loader.AppleLoader.class。
		//本質其實還是路徑  因爲你編譯後的路徑會變成 磁盤路徑+com/zh/loader/AppleLoader.class
		MainLoader.println(TAG, name);
		byte[] data = new byte[1024];
		InputStream inputStream;
		ByteArrayOutputStream byteArrayOutputStream;
		try {
			File file = new File(mDir, getFileName(name));
			inputStream = new FileInputStream(file);
			byteArrayOutputStream = new ByteArrayOutputStream();
			int len = 0;
			while ((len = inputStream.read(data)) != -1) {
				MainLoader.println(TAG, len);
				byteArrayOutputStream.write(data, 0, len);
			}
			//這部分就是把Apple.class文件從磁盤讀取出來 
			byte[] buff = byteArrayOutputStream.toByteArray();
			inputStream.close();
			byteArrayOutputStream.close();
			MainLoader.println(TAG, buff.length);
			//defineClass是原生方法:可以把符合虛擬機規則的 byte[]數組 轉化成爲描述類文件的Class對象
			return defineClass(name, buff, 0, buff.length);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return super.findClass(name);
	}
	// 獲取要加載 的class文件名
	private String getFileName(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1) {
			return name + ".class";
		} else {
			return name.substring(index + 1) + ".class";
		}
	}
}

上面的註釋已經很清楚了,關鍵點就是全限定名的理解。加載了類有什麼用?什麼都看不見啊 所以還需要進行測試:
先編寫一個Apple.java 編譯成Apple.class,然後把Apple.class放到電腦E盤根目錄下(隨便你放到哪)。

package com.zh.loader;

public class Apple {
	public void describe() {
		System.out.println("我是蘋果");
	}
}

這樣我們就從APK外部加載了一個類進我們的虛擬機,如何用這個類?只能通過反射如:

public static void main(String[] args) {
		try {
			println(TAG, "測試自定義類加載");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
			Method[] declaredMethods = aClass.getDeclaredMethods();
			declaredMethods[0].invoke(aClass.newInstance());

		} catch (Exception e) {
			println(TAG, e);
		}
	}

運行結果:
在這裏插入圖片描述
在這裏插入圖片描述
可以看到我們的工程中沒Apple類,但是卻可以使用。這個Apple類我們可以放到本地,也可以放到網絡上,用的時候再 下載過來。這個就是熱修復和插件化的基礎。還可以動態替換掉類。

類加載的優缺點
優點:

  1. 動態的替換和增加額外的類。
  2. 熱修復熱更新插件化

缺點:

  1. 需要反射使用很慢
  2. 非常規手段有風險

類加載的原理
原理就是多啦,涉及了虛擬機的類加載機制。主要的源碼就是ClassLoader中的loadClass()方法。它涉及雙親加載機制。源碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

可以看到它是通過遞歸的方式去加載類,這就是雙親加載機制。
具體解釋
類從被加載到虛擬機內存中開始,直到卸載出內存爲止,它的整個生命週期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱爲連接(linking)。
resolveClass:就是用來連接的。這個方法給Classloader用來鏈接一個類,如果這個類已經被鏈接過了,那麼這個方法只做一個簡單的返回。否則,這個類將被按照 Java規範中的Execution描述進行鏈接。等下我們來研究這個resolveClass()方法什麼時候纔會執行。
比如我們自定義的這個類加載器:
resolve一直都是會是false;所以其實我們調用loadClass時,並不會出發類的初始化。可以驗證如下:
我們修改一下Apple類 增加一段靜態代碼塊,

package com.zh.loader;

public class Apple {

	static{
		System.out.println("我被初始化了");
	}

	public void describe() {
		System.out.println("我是蘋果");
	}
}

測試,先註釋 反射調用相關,就只加載

public static void main(String[] args) {
		try {
			println(TAG, "測試自定義類加載");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
//			Method[] declaredMethods = aClass.getDeclaredMethods();
//			declaredMethods[0].invoke(aClass.newInstance());
		} catch (Exception e) {
			println(TAG, e);
		}
	}

運行結果:
在這裏插入圖片描述
接着加上反射調用方法:

public static void main(String[] args) {
		try {
			println(TAG, "測試自定義類加載");
			AppleLoader loader = new AppleLoader("E:\\");
			Class<?> aClass = loader.loadClass("com.zh.loader.Apple");
			Method[] declaredMethods = aClass.getDeclaredMethods();
			declaredMethods[0].invoke(aClass.newInstance());
		} catch (Exception e) {
			println(TAG, e);
		}
	}

運行結果:
在這裏插入圖片描述
類的加載 和類的初始化
這裏要搞清楚,類的加載和類的初始化是2個不同的東西。加載只是把類文件讀取進內存並生成一個Class對象。初始化 纔是運行類中的代碼。類中的靜態代碼。

什麼情況下需要開始類加載過程的第一個階段:“加載”。虛擬機規範中並沒強行約束,這點可以交給虛擬機的的具體實現自由把握,但是對於初始化階段虛擬機規範是嚴格規定了如下幾種情況,如果類未初始化會對類進行初始化。
1.遇到new,getstatic,putstatic,invokestatic這4條指令;
2.初始化一個類的時候,如果發現其父類沒有進行過初始化,則先初始化其父類(注意!如果其父類是接口的話,則不要求初始化父類);
3.訪問類的靜態變量(除常量【被final修辭的靜態變量】原因:常量一種特殊的變量,因爲編譯器把他們當作值(value)而不是域(field)來對待。如果你的代碼中用到了常變量(constant variable),編譯器並不會生成字節碼來從對象中載入域的值,而是直接把這個值插入到字節碼中。這是一種很有用的優化,但是如果你需要改變final域的值那麼每一塊用到那個域的代碼都需要重新編譯。
4.使用java.lang.reflect包的方法,對壘進行反射調用的時候,如果沒有初始化,則先觸發初始化
5.虛擬機啓動時,定義了main()方法的那個類先初始化

以上情況稱爲稱對一個類進行“主動引用”,除此種情況之外,均不會觸發類的初始化,稱爲“被動引用”
接口的加載過程與類的加載過程稍有不同。接口中不能使用static{}塊。當一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有真正在使用到父接口時(例如引用接口中定義的常量)纔會初始化。

以下情況會觸發類的初始化:

1.遇到new,getstatic,putstatic,invokestatic這4條指令;
2.使用java.lang.reflect包的方法對類進行反射調用;
3.初始化一個類的時候,如果發現其父類沒有進行過初始化,則先初始化其父類(注意!如果其父類是接口的話,則不要求初始化父類);

以下情況不會觸發類的初始化:
1同類子類引用父類的靜態字段,不會導致子類初始化。至於是否會觸發子類的加載和驗證,取決於虛擬機的具體實現;
2通過數組定義來引用類,也不會觸發類的初始化;例如:People[] ps = new People[100];
3.引用一個類的常量也不會觸發類的初始化

雙親委派:
前面說過,系統的類加載器有3種,具體的關係:
BootstrapClassLoader(祖父)–>ExtClassLoader(爺爺)–>AppClassLoader(也稱爲SystemClassLoader)(爸爸)–>自定義類加載器(兒子)
彼此相鄰的兩個爲父子關係,前爲父,後爲子。
比如現在我們寫了一個類Apple,整個加載過程如下:
在這裏插入圖片描述
簡單描述一下:
1
一個AppClassLoader查找資源時,先看看緩存是否有,緩存有從緩存中獲取,否則委託給父加載器。
2
遞歸,重複第1部的操作。
3
如果ExtClassLoader也沒有加載過,則由Bootstrap ClassLoader出面,它首先查找緩存,如果沒有找到的話,就去找自己的規定的路徑下,也就是
sun.mic.boot.class下面的路徑。找到就返回,沒有找到,讓子加載器自己去找。
4
Bootstrap ClassLoader如果沒有查找成功,則ExtClassLoader自己在java.ext.dirs路徑中去查找,查找成功就返回,查找不成功,再向下讓子加載器找。
5
ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路徑下查找。找到就返回。如果沒有找到就讓子類找,如果沒有子類會怎麼樣?拋出各種異常。
上面的序列,詳細說明了雙親委託的加載流程。我們可以發現委託是從下向上,然後具體查找過程卻是自上至下。

雙親委託模型好處
因爲這樣可以避免重複加載,當父親已經加載了該類的時候,就沒有必要 ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委託模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況,因爲String已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。

類與類加載器
類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達更通俗一些:比較兩個類是否”相等”,只有再這兩個類是有同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

類隔離機制
同一個類Dog可以加載兩次(只要loader1和loader3不是父子關係即可,加載出的 Class 對象不同),不同運行空間內的類不能互相訪問(eg. loader1和loader3不是父子關係,則Loader1加載的Dog不能訪問lodaer3加載的Sample)
父類加載器無法訪問到子類加載器加載的類,除非使用反射。Eg. Loader1 的父加載器是 系統類加載器,假設 Sample 類由 loader1 加載, 使用 loader1 的類 Test 是由系統類加載器加載的,例如下面這段代碼屬於 Test 類,那麼如果直接使用註釋部分的代碼(即通過常規的方式使用 Sample 是不行的),必須通過反射。

類加載的使用情景
熱修復,插件化的開源框架使用的比較多。我們平時開發基本不用。但是如果你不理解的 那麼插件化等框架就用不好。

其實具體可以去看研究《深入理解Java虛擬機》第二版。講得非常詳細,同時也很比較難理解需要多看幾遍。
在這裏插入圖片描述

動態代理

  • 什麼是動態代理
  • 怎麼使用動態代理
  • 動態代理的優缺點
  • 類加載的動態代理
  • 動態代理的使用場景

什麼是動態代理
代理生活中常見的就是 被告-律師-法官。法官提問題,被告回答一點,然後就是律師去回答。
代碼中的代理也類似。有一種模式就叫代理模式。
在這裏插入圖片描述
動態代理就是不需要提前寫代理類的代理模式。
怎麼使用動態代理
這個也簡單,和靜態代理類似有個基本套路在、舉個例子:現在有個接口描述水果。

public interface IFruit {
	void descriabe();
}
public class Apple implements IFruit{

	@Override
	public void descriabe() {
		System.out.println("我是蘋果!!!");
	}
	
}

如果是靜態代理的話:

public class AppleProxy implements IFruit {
	private IFruit mFruit;

	public AppleProxy(IFruit mFruit) {
		this.mFruit = mFruit;
	}

	@Override
	public void descriabe() {
		System.out.println("喫的");
		mFruit.descriabe();
		System.out.println("甜的");
	}

}

測試:

	public static void main(String[] args) {
		IFruit fruit = new AppleProxy(new Apple());
		fruit.descriabe();
	}

運行結果:
在這裏插入圖片描述
這個比較簡單。那怎麼處理不寫AppleProxy也可以增強desciable();
答案就是動態代理。其實java提供好了一套 動態代理API流程如下:
定義一個類實現InvocationHandler接口

public class AppleDynamicProxy implements InvocationHandler {

	private Object object;

	public AppleDynamicProxy(Object object) {
		this.object = object;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		System.out.println("喫的");
		method.invoke(object, args);
		System.out.println("甜的");
		return null;
	}

}

然後測試

	public static void main(String[] args) {

		Apple apple = new Apple();

		InvocationHandler invocationHandler = new AppleDynamicProxy(apple);

		Class<?> class1 = Apple.class;

		IFruit fruit = (IFruit) Proxy.newProxyInstance(class1.getClassLoader(),
				class1.getInterfaces(), invocationHandler);

		fruit.descriabe();

	}

運行結果:
在這裏插入圖片描述
可以看到我們的AppleDynamicProxy並沒有繼承IFruit接口,但是收到describe()方法,並且在它調用前後進行了增強。
動態代理的優缺點
優點:不修改被代理對象的源碼上,進行功能的增強。
缺點:暫無-可能用到反射會比較慢
動態代理的原理
主要用到了反射,如果對反射不熟悉的,看起來可能比較喫力。

動態代理的使用場景
這取決於你自己想幹什麼。主要看業務的需求。

在這裏插入圖片描述
關鍵就這個newProxyInstance通過名字就知道 創建一個代理類的實例。getProxyClass0會根據你傳入的類加載器和接口 去生成一個代理類。
總結
代理分爲靜態代理和動態代理兩種。
靜態代理,代理類需要自己編寫代碼寫成。
動態代理,代理類通過 Proxy.newInstance() 方法生成。
不管是靜態代理還是動態代理,代理與被代理者都要實現兩樣接口,它們的實質是面向接口編程。
靜態代理和動態代理的區別是在於要不要開發者自己定義 Proxy 類。
動態代理通過 Proxy 動態生成 proxy class,但是它也指定了一個 InvocationHandler 的實現類。
代理模式本質上的目的是爲了增強現有代碼的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章