類加載是java語言提供的最強大的機制之一盡管類加載並不是討論的熱點話題但所有的編程人員都應該了解其工作機制明白如何做才能讓其滿足我們的需要這能有效節省我們的編碼時間從不斷調試ClassNotFoundException ClassCastException的工作中解脫出來
這篇文章從基礎講起比如代碼與數據的不同之處是什麼他們是如何構成一個實例或對象的然後深入探討java虛擬機(JVM)是如何利用類加載器讀取代碼以及java中類加載器的主要類型接著用一個類加載的基本算法看一下類加載器如何加載一個內部類本文的下一節演示一段代碼來說明擴展和開發屬於自己的類加載器的必要性緊接著解釋如何使用定制的類加載器來完成一個一般意義上的任務使其可以加載任意遠端客戶的代碼在JVM中定義實例化並執行它本文包括了JEE關於類加載的規范——事實上這已經成為了JEE的標准之一
類與數據
一個類代表要執行的代碼而數據則表示其相關狀態狀態時常改變而代碼則不會當我們將一個特定的狀態與一個類相對應起來也就意味著將一個類事例化盡管相同的類對應的實例其狀態千差萬別但其本質都對應著同一段代碼在JAVA中一個類通常有著一個class文件但也有例外在JAVA的運行時環境中(Java runtime)每一個類都有一個以第一類(firstclass)的Java對象所表現出現的代碼其是javalangClass的實例我們編譯一個JAVA文件編譯器都會嵌入一個public static final修飾的類型為javalangClass名稱為class的域變量在其字節碼文件中因為使用了public修飾我們可以采用如下的形式對其訪問
javalangClass klass = Myclassclass;
一旦一個類被載入JVM中同一個類就不會被再次載入了(切記同一個類)這裡存在一個問題就是什麼是同一個類?正如一個對象有一個具體的狀態即標識一個對象始終和其代碼(類)相關聯同理載入JVM的類也有一個具體的標識我們接下來看
在JAVA中一個類用其完全匹配類名(fully qualified class name)作為標識這裡指的完全匹配類名包括包名和類名但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識因此如果一個名為Pg的包中有一個名為Cl的類被類加載器KlassLoader的一個實例kl加載Cl的實例即Cclass在JVM中表示為(Cl Pg kl)這意味著兩個類加載器的實例(Cl Pg kl) 和 (Cl Pg kl)是不同的被它們所加載的類也因此完全不同互不兼容的那麼在JVM中到底有多少種類加載器的實例?下一節我們揭示答案
類加載器
在JVM中每一個類都被javalangClassLoader的一些實例來加載類ClassLoader是在包中javalang裡開發者可以自由地繼承它並添加自己的功能來加載類
無論何時我們鍵入java MyMainClass來開始運行一個新的JVM引導類加載器(bootstrap class loader)負責將一些關鍵的Java類如javalangObject和其他一些運行時代碼先加載進內存中運行時的類在JRE\lib\rtjar包文件中因為這屬於系統底層執行動作我們無法在JAVA文檔中找到引導類加載器的工作細節基於同樣的原因引導類加載器的行為在各JVM之間也是大相徑庭
同理如果我們按照如下方式
log(javalangStringclassgetClassLoader());
來獲取java的核心運行時類的加載器
就會得到null
接下來介紹java的擴展類加載器
擴展庫提供比java運行代碼更多的特性
我們可以把擴展庫保存在由java
ext
dirs屬性提供的路徑中
(編輯注
java
ext
dirs屬性指的是系統屬性下的一個key
所有的系統屬性可以通過System
getProperties()方法獲得
在編者的系統中
java
ext
dirs的value是
C:\Program Files\Java\jdk
_
\jre\lib\ext
下面將要談到的如java
class
path也同屬系統屬性的一個key
)
類ExtClassLoader專門用來加載所有java
ext
dirs下的
jar文件
開發者可以通過把自己的
jar文件或庫文件加入到擴展目錄的classpath
使其可以被擴展類加載器讀取
從開發者的角度
第三種同樣也是最重要的一種類加載器是AppClassLoader
這種類加載器用來讀取所有的對應在java
class
path系統屬性的路徑下的類
Sun的java指南中
文章
理解擴展類加載
(Understanding Extension Class Loading)對以上三個類加載器路徑有更詳盡的解釋
這是其他幾個JDK中的類加載器
●
URLClassLoader
●java
security
SecureClassLoader
●java
rmi
server
RMIClassLoader
●sun
applet
AppletClassLoader
java
lang
Thread
包含了public ClassLoader getContextClassLoader()方法
這一方法返回針對一具體線程的上下文環境類加載器
此類加載器由線程的創建者提供
以供此線程中運行的代碼在需要加載類或資源時使用
如果此加載器未被建立
缺省是其父線程的上下文類加載器
原始的類加載器一般由讀取應用程序的類加載器建立
類加載器如何工作?除了引導類加載器
所有的類加載器都有一個父類加載器
不僅如此
所有的類加載器也都是java
lang
ClassLoader類型
以上兩種類加載器是不同的
而且對於開發者自訂制的類加載器的正常運行也至關重要
最重要的方面是正確設置父類加載器
任何類加載器
其父類加載器是加載該類加載器的類加載器實例
(記住
類加載器本身也是一個類!)
使用loadClass()方法可以從類加載器中獲得該類
我們可以通過java
lang
ClassLoader的源代碼來了解該方法工作的細節
如下
protected synchronized Class<?> loadClass
(String name boolean resolve)
throws ClassNotFoundException{
// First check if the class is already loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parentloadClass(name false);
} else {
c = findBootstrapClass(name);
}
} catch (ClassNotFoundException e) {
// If still not found then invoke
// findClass to find the class
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
我們可以使用ClassLoader的兩種構造方法來設置父類加載器
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(MyClassLoaderclassgetClassLoader());
}
}
或
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(getClass()getClassLoader());
}
}
第一種方式較為常用
因為通常不建議在構造方法裡調用getClass()方法
因為對象的初始化只是在構造方法的出口處才完全完成
因此
如果父類加載器被正確建立
當要示從一個類加載器的實例獲得一個類時
如果它不能找到這個類
它應該首先去訪問其父類
如果父類不能找到它(即其父類也不能找不這個類
等等)
而且如果findBootstrapClass
()方法也失敗了
則調用findClass()方法
findClass()方法的缺省實現會拋出ClassNotFoundException
當它們繼承java
lang
ClassLoader來訂制類加載器時開發者需要實現這個方法
findClass()的缺省實現方式如下
protected Class<?> findClass(String name)
throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
在findClass()方法內部
類加載器需要獲取任意來源的字節碼
來源可以是文件系統
URL
數據庫
可以產生字節碼的另一個應用程序
及其他類似的可以產生java規范的字節碼的來源
你甚至可以使用BCEL (Byte Code Engineering Library
字節碼工程庫)
它提供了運行時創建類的捷徑
BCEL已經被成功地使用在以下方面
編譯器
優化器
混淆器
代碼產生器及其他分析工具
一旦字節碼被檢索
此方法就會調用defineClass()方法
此行為對不同的類加載實例是有差異的
因此
如果兩個類加載實例從同一個來源定義一個類
所定義的結果是不同的
JAVA語言規范(Java language specification)詳細解釋了JAVA執行引擎中的類或接口的加載(loading)
鏈接(linking)或初始化(initialization)過程
圖一顯示了一個主類稱為MyMainClass的應用程序
依照之前的闡述
MyMainClass
class會被AppClassLoader加載
MyMainClass創建了兩個類加載器的實例
CustomClassLoader
和 CustomClassLoader
他們可以從某數據源(比如網絡)獲取名為Target的字節碼
這表示類Target的類定義不在應用程序類路徑或擴展類路徑
在這種情況下
如果MyMainClass想要用自定義的類加載器加載Target類
CustomClassLoader
和CustomClassLoader
會分別獨立地加載並定義Target
class類
這在java中有重要的意義
如果Target類有一些靜態的初始化代碼
並且假設我們只希望這些代碼在JVM中只執行一次
而這些代碼在我們目前的步驟中會執行兩次——分別被不同的CustomClassLoaders加載並執行
如果類Target被兩個CustomClassLoaders加載並創建兩個實例Target
和Target
如圖一顯示
它們不是類型兼容的
換句話說
在JVM中無法執行以下代碼
Target target = (Target) target;
以上代碼會拋出一個ClassCastException
這是因為JVM把他們視為分別不同的類
因為他們被不同的類加載器所定義
這種情況當我們不是使用兩個不同的類加載器CustomClassLoader
和 CustomClassLoader
而是使用同一個類加載器CustomClassLoader的不同實例時
也會出現同樣的錯誤
這些會在本文後邊用具體代碼說明
圖
在同一個JVM中多個類加載器加載同一個目標類
關於類加載
定義和鏈接的更多解釋
請參考Andreas Schaefer的
Inside Class Loaders
為什麼我們需要我們自己的類加載器原因之一為開發者寫自己的類加載器來控制JVM中的類加載行為
java中的類靠其包名和類名來標識
對於實現了java
io
Serializable接口的類
serialVersionUID扮演了一個標識類版本的重要角色
這個唯一標識是一個類名
接口名
成員方法及屬性等組成的一個
位的哈希字段
而且也沒有其他快捷的方式來標識一個類的版本
嚴格說來
如果以上的都匹配
那麼則屬於同一個類
但是讓我們思考如下情況
我們需要開發一個通用的執行引擎
可以執行實現某一特定接口的任何任務
當任務被提交到這個引擎
首先需要加載這個任務的代碼
假設不同的客戶對此引擎提交了不同的任務
湊巧
這些所有的任務都有一個相同的類名和包名
現在面臨的問題就是這個引擎是否可以針對不同的用戶所提交的信息而做出不同的反應
這一情況在下文的參考一節有可供下載的代碼樣例
samepath 和 differentversions
這兩個目錄分別演示了這一概念
圖
顯示了文件目錄結構
有三個子目錄samepath
differentversions
和 differentversionspush
裡邊是例子
圖
文件夾結構組織示例
在samepath 中
類version
Version保存在v
和v
兩個子目錄裡
兩個類具有同樣的類名和包名
唯一不同的是下邊這行
public void fx(){
log(this = + this + ; Versionfx());
}
V
中
日志記錄中有Version
fx(
)
而在v
中則是Version
fx(
)
把這個兩個存在細微不同的類放在一個classpath下
然後運行Test類
set CLASSPATH=
;%CURRENT_ROOT%\v
;%CURRENT_ROOT%\v
%JAVA_HOME%\bin\java Test
圖
顯示了控制台輸出
我們可以看到對應著Version
fx(
)的代碼被執行了
因為類加載器在classpath首先看到此版本的代碼
圖
在類路徑中samepath測試排在最前面的version
再次運行
類路徑做如下微小改動
set CLASSPATH=
;%CURRENT_ROOT%\v
;%CURRENT_ROOT%\v
%JAVA_HOME%\bin\java Test
控制台的輸出變為圖
對應著Version
fx(
)的代碼被加載
因為類加載器在classpath中首先找到它的路徑
圖
在類路徑中samepath測試排在最前面的version
根據以上例子可以很明顯地看出
類加載器加載在類路徑中被首先找到的元素
如果我們在v
和v
中刪除了version
Version
做一個非version
Version形式的
jar文件
如myextension
jar
把它放到對應java
ext
dirs的路徑下
再次執行後看到version
Version不再被AppClassLoader加載
而是被擴展類加載器加載
如圖
所示
圖
AppClassLoader及ExtClassLoader
繼續這個例子
文件夾differentversions包含了一個RMI執行引擎
客戶端可以提供給執行引擎任何實現了common
TaskIntf接口的任務
子文件夾client
和 client
包含了類client
TaskImpl有個細微不同的兩個版本
兩個類的區別在以下幾行
static{
log(clientTaskImplclassgetClassLoader
(v) : + TaskImplclassgetClassLoader());
}
public void execute(){
log(this = + this + ; execute());
}
在client
和client
裡分別有getClassLoader(v
) 與 execute(
)和getClassLoader(v
) 與 execute(
)的的log語句
並且
在開始執行引擎RMI服務器的代碼中
我們隨意地將client
的任務實現放在類路徑的前面
CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;
%CURRENT_ROOT%\client
;%CURRENT_ROOT%\client
%JAVA_HOME%\bin\java server
Server
如圖
的屏幕截圖
在客戶端VM
各自的client
TaskImpl類被加載
實例化
並發送到服務端的VM來執行
從服務端的控制台
可以明顯看到client
TaskImpl代碼只被服務端的VM執行一次
這個單一的代碼版本在服務端多次生成了許多實例
並執行任務
圖
執行引擎服務器控制台
圖
顯示了服務端的控制台
加載並執行兩個不同的客戶端的請求
如圖7
8所示
需要注意的是
代碼只被加載了一次(從靜態初始化塊的日志中也可以明顯看出)
但對於客戶端的調用這個方法被執行了兩次
圖7
執行引擎客戶端
控制台
圖7中
客戶端VM加載了含有client
TaskImpl
class
getClassLoader(v
)的日志內容的類TaskImpl的代碼
並提供給服務端的執行引擎
圖
的客戶端VM加載了另一個TaskImpl的代碼
並發送給服務端
圖
執行引擎客戶端
控制台
在客戶端的VM中
類client
TaskImpl被分別加載
初始化
並發送到服務端執行
圖
還揭示了client
TaskImpl的代碼只在服務端的VM中加載了一次
但這
唯一的一次
卻在服務端創造了許多實例並執行
或許客戶端
該不高興了因為並不是它的client
TaskImpl(v
)的方法調用被服務端執行了
而是其他的一些代碼
如何解決這一問題?答案就是實現定制的類加載器
定制類加載器要較好地控制類的加載
就要實現定制的類加載器
所有自定義的類加載器都應繼承自java
lang
ClassLoader
而且在構造方法中
我們也應該設置父類加載器
然後重寫findClass()方法
differentversionspush文件夾包含了一個叫做FileSystemClassLoader的自訂制的類加載器
其結構如圖
所示
圖
定制類加載器關系
以下是在common
FileSystemClassLoader實現的主方法
public byte[] findClassBytes(String className){
try{
String pathName = currentRoot +
FileseparatorChar + className
replace( FileseparatorChar)
+ class;
FileInputStream inFile = new
FileInputStream(pathName);
byte[] classBytes = new
byte[inFileavailable()];
inFileread(classBytes);
return classBytes;
}
catch (javaioIOException ioEx){
return null;
}
}
public Class findClass(String name)throws
ClassNotFoundException{
byte[] classBytes = findClassBytes(name);
if (classBytes==null){
throw new ClassNotFoundException();
}
else{
return defineClass(name classBytes
classByteslength);
}
}
public Class findClass(String name byte[]
classBytes)throws ClassNotFoundException{
if (classBytes==null){
throw new ClassNotFoundException(
(classBytes==null));
}
else{
return defineClass(name classBytes
classByteslength);
}
}
public void execute(String codeName
byte[] code){
Class klass = null;
try{
klass = findClass(codeName code);
TaskIntf task = (TaskIntf)
klassnewInstance();
taskexecute();
}
catch(Exception exception){
exceptionprintStackTrace();
}
}
這個類供客戶端把client
TaskImpl(v
)轉換成字節數組
之後此字節數組被發送到RMI服務端
在服務端
一個同樣的類用來把字節數組的內容轉換回代碼
客戶端代碼如下
public class Client{
public static void main (String[] args){
try{
byte[] code = getClassDefinition
(clientTaskImpl);
serverIntfexecute(clientTaskImpl
code);
}
catch(RemoteException remoteException){
remoteExceptionprintStackTrace();
}
}
private static byte[] getClassDefinition
(String codeName){
String userDir = SystemgetProperties()
getProperty(BytePath);
FileSystemClassLoader fscl = null;
try{
fscl = new FileSystemClassLoader
(userDir);
}
catch(FileNotFoundException
fileNotFoundException){
fileNotFoundExceptionprintStackTrace();
}
return fsclfindClassBytes(codeName);
}
}
在執行引擎中
從客戶端收到的代碼被送到定制的類加載器中
定制的類加載器把其從字節數組定義成類
實例化並執行
需要指出的是
對每一個客戶請求
我們用類FileSystemClassLoader的不同實例來定義客戶端提交的client
TaskImpl
而且
client
TaskImpl並不在服務端的類路徑中
這也就意味著當我們在FileSystemClassLoader調用findClass()方法時
findClass()調用內在的defineClass()方法
類client
TaskImpl被特定的類加載器實例所定義
因此
當FileSystemClassLoader的一個新的實例被使用
類又被重新定義為字節數組
因此
對每個客戶端請求類client
TaskImpl被多次定義
我們就可以在相同執行引擎JVM中執行不同的client
TaskImpl的代碼
public void execute(String codeName byte[] code)throws RemoteException{
FileSystemClassLoader fileSystemClassLoader = null;
try{
fileSystemClassLoader = new FileSystemClassLoader();
fileSystemClassLoaderexecute(codeName code);
}
catch(Exception exception){
throw new RemoteException(exceptiongetMessage());
}
}
示例在differentversionspush文件夾下服務端和客戶端的控制台界面分別如圖所示
圖 定制類加載器執行引擎
圖顯示的是定制的類加載器控制台我們可以看到clientTaskImpl的代碼被多次加載實際上針對每一個客戶端類都被加載並初始化
圖 定制類加載器客戶端
圖中含有clientTaskImplclassgetClassLoader(v)的日志記錄的類TaskImpl的代碼被客戶端的VM加載然後送到服務端圖 另一個客戶端把包含有clientTaskImplclassgetClassLoader(v)的類代碼加載並送往服務端
圖 定制類加載器客戶端
這段代碼演示了我們如何利用不同的類加載器實例來在同一個VM上執行不同版本的代碼
JEE的類加載器
JEE的服務器傾向於以一定間隔頻率丟棄原有的類並重新載入新的類在某些情況下會這樣執行而有些情況則不同樣對於一個web服務器如果要丟棄一個servlet實例可能是服務器管理員的手動操作也可能是此實例長時間未相應當一個JSP頁面被首次請求容器會把此JSP頁面翻譯成一個具有特定形式的servlet代碼一旦servlet代碼被創建容器就會把這個servlet翻譯成class文件等待被使用對於提交給容器的每次請求容器都會首先檢查這個JSP文件是否剛被修改過是的話就重新翻譯此文件這可以確保每次的請求都是及時更新的企業級的部署方案以ear war rar等形式的文件同樣需要重復加載可能是隨意的也可能是依照某種配置方案定期執行對所有的這些情況——類的加載卸載重新加載……全部都是建立在我們控制應用服務器的類加載機制的基礎上的實現這些需要擴展的類加載器它可以執行由其自身所定義的類Brett Peterson已經在他的文章 Understanding JEE Application Server Class Loading Architectures給出了JEE應用服務器的類加載方案的詳細說明詳見網站
結要
本文探討了類載入到虛擬機是如何進行唯一標識的以及類如果存在同樣的類名和包名時所產生的問題因為沒有一個直接可用的類版本管理機制所以如果我們要按自己的意願來加載類時需要自己訂制類加載器來擴展其行為我們可以利用許多JEE服務器所提供的熱部署功能來重新加載一個新版本的類而不改動服務器的VM即使不涉及應用服務器我們也可以利用定制類加載器來控制java應用程序載入類時的具體行為Ted Neward的書ServerBased Java Programming中詳細闡述java的類加載JEE的API以及使用他們的最佳途徑
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26923.html