本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
上節,咱們探討了動態代理,在前幾節中,咱們屢次提到了類加載器ClassLoader,本節就來詳細討論Java中的類加載機制與ClassLoader。java
類加載器ClassLoader就是加載其餘類的類,它負責將字節碼文件加載到內存,建立Class對象。與以前介紹的反射、註解、和動態代理同樣,在大部分的應用編程中,咱們不太須要本身實現ClassLoader。git
不過,理解類加載的機制和過程,有助於咱們更好的理解以前介紹的內容,更好的理解Java。在反射一節,咱們介紹過Class的靜態方法Class.forName,理解類加載器有助於咱們更好的理解該方法。程序員
ClassLoader通常是系統提供的,不須要本身實現,不過,經過建立自定義的ClassLoader,能夠實現一些強大靈活的功能,好比:github
理解自定義ClassLoader有助於咱們理解這些系統程序和框架,如Tomat, JSP, OSGI,在業務須要的時候,也能夠藉助自定義ClassLoader實現動態靈活的功能。正則表達式
下面,咱們首先來進一步理解Java加載類的過程,理解類ClassLoader和Class.forName,介紹一個簡單的應用,而後咱們探討如何實現自定義ClassLoader,演示如何利用它實現熱部署。數據庫
運行Java程序,就是執行java這個命令,指定包含main方法的完整類名,以及一個classpath,即類路徑。類路徑能夠有多個,對於直接的class文件,路徑是class文件的根目錄,對於jar包,路徑是jar包的完整名稱(包括路徑和jar包名)。編程
Java運行時,會根據類的徹底限定名尋找並加載類,尋找的方式基本就是在系統類和指定的類路徑中尋找,若是是class文件的根目錄,則直接查看是否有對應的子目錄及文件,若是是jar文件,則首先在內存中解壓文件,而後再查看是否有對應的類。swift
負責加載類的類就是類加載器,它的輸入是徹底限定的類名,輸出是Class對象。類加載器不是隻有一個,通常程序運行時,都會有三個:設計模式
這三個類加載器有必定的關係,能夠認爲是父子關係,Application ClassLoader的父親是Extension ClassLoader,Extension的父親是Bootstrap ClassLoader,注意不是父子繼承關係,而是父子委派關係,子ClassLoader有一個變量parent指向父ClassLoader,在子ClassLoader加載類時,通常會首先經過父ClassLoader加載,具體來講,在加載一個類時,基本過程是:
這個過程通常被稱爲"雙親委派"模型,即優先讓父ClassLoader去加載。爲何要先讓父ClassLoader去加載呢?這樣,能夠避免Java類庫被覆蓋的問題,好比用戶程序也定義了一個類java.lang.String,經過雙親委派,java.lang.String只會被Bootstrap ClassLoader加載,避免自定義的String覆蓋Java類庫的定義。
須要瞭解的是,"雙親委派"雖然是通常模型,但也有一些例外,好比:
一個程序運行時,會建立一個Application ClassLoader,在程序中用到ClassLoader的地方,若是沒有指定,通常用的都是這個ClassLoader,因此,這個ClassLoader也被稱爲系統類加載器(System ClassLoader)。
下面,咱們來具體看下錶示類加載器的類 - ClassLoader。
類ClassLoader是一個抽象類,Application ClassLoader和Extension ClassLoader的具體實現類分別是sun.misc.LauncherExtClassLoader,Bootstrap ClassLoader不是由Java實現的,沒有對應的類。
每一個Class對象都有一個方法,能夠獲取實際加載它的ClassLoader,方法是:
public ClassLoader getClassLoader() 複製代碼
ClassLoader有一個方法,能夠獲取它的父ClassLoader:
public final ClassLoader getParent() 複製代碼
若是ClassLoader是Bootstrap ClassLoader,返回值爲null。
好比:
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
while (cl != null) {
System.out.println(cl.getClass().getName());
cl = cl.getParent();
}
System.out.println(String.class.getClassLoader());
}
}
複製代碼
輸出爲:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
複製代碼
ClassLoader有一個靜態方法,能夠獲取默認的系統類加載器:
public static ClassLoader getSystemClassLoader() 複製代碼
ClassLoader中有一個主要方法,用於加載類:
public Class<?> loadClass(String name) throws ClassNotFoundException
複製代碼
好比:
ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = cl.loadClass("java.util.ArrayList");
ClassLoader actualLoader = cls.getClassLoader();
System.out.println(actualLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
複製代碼
須要說明的是,因爲委派機制,Class的getClassLoader()方法返回的不必定是調用loadClass的ClassLoader,好比,上面代碼中,java.util.ArrayList實際由BootStrap ClassLoader加載,因此返回值就是null。
在反射一節,咱們介紹過Class的兩個靜態方法forName:
public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
複製代碼
第一個方法使用系統類加載器加載,第二個指定ClassLoader,參數initialize表示,加載後,是否執行類的初始化代碼(如static語句塊),沒有指定默認爲true。
ClassLoader的loadClass方法與forName方法均可以加載類,它們有什麼不一樣呢?基本是同樣的,不過,有一個不一樣,ClassLoader的loadClass不會執行類的初始化代碼,看個例子:
public class CLInitDemo {
public static class Hello {
static {
System.out.println("hello");
}
};
public static void main(String[] args) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
String className = CLInitDemo.class.getName() + "$Hello";
try {
Class<?> cls = cl.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
複製代碼
使用ClassLoader加載靜態內部類Hello,Hello有一個static語句塊,輸出"hello",運行該程序,類被加載了,但沒有任何輸出,即static語句塊沒有被執行。若是將loadClass的語句換爲:
Class<?> cls = Class.forName(className);
複製代碼
則static語句塊會被執行,屏幕將輸出"hello"。
咱們來看下ClassLoader的loadClass代碼,以進一步理解其行爲:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
複製代碼
它調用了另外一個loadClass方法,其主要代碼爲(省略了一些代碼,加了註釋,以便於理解):
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,檢查類是否已經被加載了
Class c = findLoadedClass(name);
if (c == null) {
//沒被加載,先委派父ClassLoader或BootStrap ClassLoader去加載
try {
if (parent != null) {
//委派父ClassLoader,resolve參數固定爲false
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//沒找到,捕獲異常,以便嘗試本身加載
}
if (c == null) {
// 本身去加載,findClass纔是當前ClassLoader的真正加載方法
c = findClass(name);
}
}
if (resolve) {
// 連接,執行static語句塊
resolveClass(c);
}
return c;
}
}
複製代碼
參數resolve相似Class.forName中的參數initialize,能夠看出,其默認值爲false,即便經過自定義ClassLoader重寫loadClass,設置resolve爲true,它調用父ClassLoader的時候,傳遞的也是固定的false。
findClass是一個protected方法,類ClassLoader的默認實現就是拋出ClassNotFoundException,子類應該重寫該方法,實現本身的加載邏輯,後文咱們會看個具體例子。
能夠經過ClassLoader的loadClass或Class.forName本身加載類,但什麼狀況須要本身加載類呢?
不少應用使用面向接口的編程,接口具體的實現類可能有不少,適用於不一樣的場合,具體使用哪一個實現類在配置文件中配置,經過更改配置,不用改變代碼,就能夠改變程序的行爲,在設計模式中,這是一種策略模式,咱們看個簡單的示例。
定義一個服務接口IService:
public interface IService {
public void action();
}
複製代碼
客戶端經過該接口訪問其方法,怎麼得到IService實例呢?查看配置文件,根據配置的實現類,本身加載,使用反射建立實例對象,示例代碼爲:
public class ConfigurableStrategyDemo {
public static IService createService() {
try {
Properties prop = new Properties();
String fileName = "data/c87/config.properties";
prop.load(new FileInputStream(fileName));
String className = prop.getProperty("service");
Class<?> cls = Class.forName(className);
return (IService) cls.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
IService service = createService();
service.action();
}
}
複製代碼
config.properties的內容示例爲:
service=shuo.laoma.dynamic.c87.ServiceB
複製代碼
代碼比較簡單,就不贅述了。
Java類加載機制的強大之處在於,咱們能夠建立自定義的ClassLoader,自定義ClassLoader是Tomcat實現應用隔離、支持JSP,OSGI實現動態模塊化的基礎。
怎麼自定義呢?通常而言,繼承類ClassLoader,重寫findClass就能夠了。怎麼實現findClass呢?使用本身的邏輯尋找class文件字節碼的字節形式,找到後,使用以下方法轉換爲Class對象:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
複製代碼
name表示類名,b是存放字節碼數據的字節數組,有效數據從off開始,長度爲len。
看個例子:
public class MyClassLoader extends ClassLoader {
private static final String BASE_DIR = "data/c87/";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replaceAll("\\.", "/");
fileName = BASE_DIR + fileName + ".class";
try {
byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException ex) {
throw new ClassNotFoundException("failed to load class " + name, ex);
}
}
}
複製代碼
MyClassLoader從BASE_DIR下的路徑中加載類,它使用了咱們在57節介紹的BinaryFileUtils讀取文件,轉換爲byte數組。MyClassLoader沒有指定父ClassLoader,默認是系統類加載器,即ClassLoader.getSystemClassLoader()的返回值,不過,ClassLoader有一個可重寫的構造方法,能夠指定父ClassLoader:
protected ClassLoader(ClassLoader parent) 複製代碼
MyClassLoader有什麼用呢?將BASE_DIR加到classpath中不就好了,確實能夠,這裏主要是演示基本用法,實際中,能夠從Web服務器、數據庫或緩存服務器獲取bytes數組,這就不是系統類加載器能作到的了。
不過,不把BASE_DIR放到classpath中,而是使用MyClassLoader加載,確實有一個很大的好處,能夠建立多個MyClassLoader,對同一個類,每一個MyClassLoader均可以加載一次,獲得同一個類的不一樣Class對象,好比:
MyClassLoader cl1 = new MyClassLoader();
String className = "shuo.laoma.dynamic.c87.HelloService";
Class<?> class1 = cl1.loadClass(className);
MyClassLoader cl2 = new MyClassLoader();
Class<?> class2 = cl2.loadClass(className);
if (class1 != class2) {
System.out.println("different classes");
}
複製代碼
cl1和cl2是兩個不一樣的ClassLoader,class1和class2對應的類名同樣,但它們是不一樣的對象。
這到底有什麼用呢?
下面,咱們來具體看熱部署的示例。
所謂熱部署,就是在不重啓應用的狀況下,當類的定義,即字節碼文件修改後,可以替換該Class建立的對象,怎麼作到這一點呢?咱們利用MyClassLoader,看個簡單的示例。
咱們使用面向接口的編程,定義一個接口IHelloService:
public interface IHelloService {
public void sayHello();
}
複製代碼
實現類是shuo.laoma.dynamic.c87.HelloImpl,class文件放到MyClassLoader的加載目錄中。
演示類是HotDeployDemo,它定義瞭如下靜態變量:
private static final String CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
private static final String FILE_NAME = "data/c87/"
+CLASS_NAME.replaceAll("\\.", "/")+".class";
private static volatile IHelloService helloService;
複製代碼
CLASS_NAME
表示實現類名稱,FILE_NAME
是具體的class文件路徑,helloService是IHelloService實例。
當CLASS_NAME
表明的類字節碼改變後,咱們但願從新建立helloService,反映最新的代碼,怎麼作呢?先看用戶端獲取IHelloService的方法:
public static IHelloService getHelloService() {
if (helloService != null) {
return helloService;
}
synchronized (HotDeployDemo.class) {
if (helloService == null) {
helloService = createHelloService();
}
return helloService;
}
}
複製代碼
這是一個單例模式,createHelloService()的代碼爲:
private static IHelloService createHelloService() {
try {
MyClassLoader cl = new MyClassLoader();
Class<?> cls = cl.loadClass(CLASS_NAME);
if (cls != null) {
return (IHelloService) cls.newInstance();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
複製代碼
它使用MyClassLoader加載類,並利用反射建立實例,它假定實現類有一個public無參構造方法。
在調用IHelloService的方法時,客戶端老是先經過getHelloService獲取實例對象,咱們模擬一個客戶端線程,它不停的獲取IHelloService對象,並調用其方法,而後睡眠1秒鐘,其代碼爲:
public static void client() {
Thread t = new Thread() {
@Override
public void run() {
try {
while (true) {
IHelloService helloService = getHelloService();
helloService.sayHello();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
}
}
};
t.start();
}
複製代碼
怎麼知道類的class文件發生了變化,並從新建立helloService對象呢?咱們使用一個單獨的線程模擬這一過程,代碼爲:
public static void monitor() {
Thread t = new Thread() {
private long lastModified = new File(FILE_NAME).lastModified();
@Override
public void run() {
try {
while (true) {
Thread.sleep(100);
long now = new File(FILE_NAME).lastModified();
if (now != lastModified) {
lastModified = now;
reloadHelloService();
}
}
} catch (InterruptedException e) {
}
}
};
t.start();
}
複製代碼
咱們使用文件的最後修改時間來跟蹤文件是否發生了變化,當文件修改後,調用reloadHelloService()來從新加載,其代碼爲:
public static void reloadHelloService() {
helloService = createHelloService();
}
複製代碼
就是利用MyClassLoader從新建立HelloService,建立後,賦值給helloService,這樣,下次getHelloService()獲取到的就是最新的了。
在主程序中啓動client和monitor線程,代碼爲:
public static void main(String[] args) {
monitor();
client();
}
複製代碼
在運行過程當中,替換HelloImpl.class,能夠看到行爲會變化,爲便於演示,咱們在data/c87/shuo/laoma/dynamic/c87/目錄下準備了兩個不一樣的實現類HelloImpl_origin.class
和HelloImpl_revised.class
,在運行過程當中替換,會看到輸出不同,以下圖所示:
HelloImpl_origin.class
同樣,輸出爲"hello",若是與
HelloImpl_revised.class
同樣,輸出爲"hello revised"。
完整的代碼和數據在github上,文末有連接。
本節探討了Java中的類加載機制,包括Java加載類的基本過程,類ClassLoader的用法,以及如何建立自定義的ClassLoader,探討了兩個簡單應用示例,一個經過動態加載實現可配置的策略,另外一個經過自定義ClassLoader實現熱部署。
從84節到本節,咱們探討了Java中的多個動態特性,包括反射、註解、動態代理和類加載器,做爲應用程序員,大部分用的都比較少,用的較多的就是使用框架和庫提供的各類註解了,但這些特性大量應用於各類系統程序、框架、和庫中,理解這些特性有助於咱們更好的理解它們,也能夠在須要的時候本身實現動態、通用、靈活的功能。
在註解一節,咱們提到,註解是一種聲明式編程風格,它提升了Java語言的表達能力,平常編程中一種常見的需求是文本處理,在計算機科學中,有一種技術大大提升了文本處理的表達能力,那就是正則表達式,大部分編程語言都有對它的支持,它有什麼強大功能呢?
(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…,位於包shuo.laoma.dynamic.c87下)
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。