動態代理和類加載
本節主要複習動態代理和類加載機制。這2個知識點是非常重要的,也是很常見的,可能我們自己用的並不多,但是很多框架中的基礎都是它們2個。如果不知道這些知識 那麼看那些開源框架的源碼就會很喫力。是謂 基礎不牢地動山搖
類加載
還是按標準的靈魂5問來學習:
- 什麼是類加載
- 怎麼使用類加載
- 類加載的優缺點
- 類加載的原理
- 類加載的使用場景
什麼是類加載
類加載是一種機制是一套流程和動作。我們寫的源碼其實就是文本文件,如我們寫的第一個java程序 HelloWorld.java 。這個就是源文件給人看的。
但是這個文件並不能直接運行, 需要編譯成HelloWorld.class 這個是字節碼文件 打開裏面其實是二進制,但是一般不會顯示0101這種而是會以16進制顯示。這個給java虛擬機看的。
你要看到話需要對着-java虛擬機規範表才知道代表什麼意思。這個也是不能直接在電腦上運行的。它只能運行在JVM中,怎麼運行,當我們執行java HelloWorld
這裏java 就代表運行虛擬機,後面的HelloWorld就是 我們的字節碼,意思就是運行虛擬機 然後在虛擬機上運行HelloWorld 字節碼。虛擬機會把字節碼 翻譯成對應操作系統能識別的機器碼就可以在電腦上運行了。
類加載就是:虛擬機通過全限定名(就是路徑)把字節碼文件讀取進內存,怎麼讀?肯定是IO啊,然後進行必要的驗證、準備、解析、初始化等工作 最後轉爲規定的數據結構存儲在內存中方法區,並且生成一個Class對象來描述這個類文件,後續通過這個Class就可以訪問到這個類文件。
怎麼使用類加載
首先需要搞清楚 類加載器有哪些,並不是所有的 類都是又一種類加載器 去加載的,比如JDK自帶的類,我們自己寫的類。根據官方文檔類加載器有3種:
-
BootStrap ClassLoder —BootstrapClassLoader引導類加載器,這個是最頂層的類加載器,它主要加載核心類庫,JRE中的庫 具體就是%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。這裏提一句JDK:java develop kit 就是開發工具包。 JRE:Java Runtime Enviroment是指Java的運行環境。
-
Extention ClassLoader --ExtClassLoader擴展類加載器,加載我們開發用的到基礎庫,不懂的可以點進去看看有哪些類具體目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。
-
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類我們可以放到本地,也可以放到網絡上,用的時候再 下載過來。這個就是熱修復和插件化的基礎。還可以動態替換掉類。
類加載的優缺點
優點:
- 動態的替換和增加額外的類。
- 熱修復熱更新插件化
缺點:
- 需要反射使用很慢
- 非常規手段有風險
類加載的原理
原理就是多啦,涉及了虛擬機的類加載機制。主要的源碼就是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 的實現類。
代理模式本質上的目的是爲了增強現有代碼的功能。