類加載器從 jdk 1.0 就出現了,最初是為了滿足 java applet 的需要而開發出來的。java applet 需要從遠端下載下傳 java 類檔案到浏覽器中并執行。現在類加載器在 web 容器和 osgi 中得到了廣泛的使用。一般來說,java 應用的開發人員不需要直接同類加載器進行互動。java 虛拟機預設的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類加載器進行互動的情況,而對類加載器的機制又不是很了解的話,就很容易花大量的時間去調試<code>classnotfoundexception</code>和<code>noclassdeffounderror</code>等異常。本文将詳細介紹 java 的類加載器,幫助讀者深刻了解 java 語言中的這個重要概念。下面首先介紹一些相關的基本概念。
顧名思義,類加載器是用來将java類加載到java虛拟機(jvm)中的。它是java語言的一個創新,也是java語言流行的重要原因之一。它使得java類可以被動态加載到java虛拟機中并執行。
一般來說,java虛拟機使用java類的方式如下:java源程式(.java檔案)---->經過編譯之後---->變成java位元組碼(.class檔案)。類加載器(classloader)負責讀取java位元組碼,并轉換成java.lang.class類的一個執行個體,每個這樣的一個執行個體用來表示一個java類,通過此執行個體的newinstance()方法就可以建立出該類的一個對象。
java 類加載器的作用就是在運作時加載類(把類的二進制加載到記憶體中),它可以在将類加載到虛拟機中的時候檢查類的完整性。
java虛拟機中可以安裝多個類加載器,系統預設三個主要的類加載器,每個類加載器負責加載特定位置的類。他們分别是:
bootstrap、extclassloader、appclassloader
加載器也是一個java類,由于java類要由類加載器來加載,那第一個類加載器又是由誰來加載它呢?這個加載器是bootstrap。
bootstrap------它不是一個java類,他是嵌套在jvm(java虛拟機)中的一個用c++編寫的二進制代碼。
//得到我們自定義類的位元組碼,得到此位元組碼的類加載器,得到類加載器的位元組碼,得到位元組碼的名字
system.out.println(myclassloadertest.class.getclassloader().getclass().getname());
//得到我們自定義類的位元組碼,得到此位元組碼的類加載器,得到類加載器的位元組碼
system.out.println(myclassloadertest.class.getclassloader().getclass());
運作輸出:
sun.misc.launcher$appclassloader
class sun.misc.launcher$appclassloader
運作下面代碼:
//得到系統類的位元組碼,得到此位元組碼的類加載器
system.out.println(system.class.getclassloader());-------------------------------------------------1
//得到系統類的位元組碼,得到此位元組碼的類加載器,得到類加載器的位元組碼,得到位元組碼的名字
system.out.println(system.class.getclassloader().getclass().getname());
null
exception in thread "main" java.lang.nullpointerexception
at shipin44.myclassloadertest.main(myclassloadertest.java:16)
程式報空指針異常。
位置1的代碼,運作結果為null,不代表它沒類加載器,(如果沒有那是誰加載它的呢?)而是加載它的加載器是bootstrap(一個特殊的類加載器,是c++寫的二進制代碼),bootstrap不是一個java對象,是以上面列印null。

public static void main(string[] args) {
classloader cl = myclassloadertest.class.getclassloader();
while (cl!=null) {
system.out.println(cl.getclass().getname());
//cl本來就是類加載器了,下面這樣的寫法是得到父加載器的父加載器多跳了1級
cl = cl.getparent().getclass().getclassloader();
}
//列印第一個類加載器
system.out.println(cl);
system.out.println("------------正确輸出-------------");
cl = myclassloadertest.class.getclassloader();
system.out.println(cl.getclass().getname());
cl = cl.getparent();
}
輸出結果:
null
------------正确輸出-------------
sun.misc.launcher$extclassloader
滑鼠右鍵點選myclassloadertest類--->選export--->java--->jar file--->next--->finish,然後将導出的包copy到jre\lib\ext目錄下
運作輸出:
zf.jar包中的myclassloadertest類中的代碼如下:
classloader cl = myclassloadertest.class.getclassloader();
system.out.println("------------正确輸出-------------");
當第一個類加載器要加載類的時候,它先不直接加載類,而是交(委托)給它的父類加載器去加載,而父類加載器又交(委托)給它的父類加載器去加載,就這樣一直往上的走,當到了bootstrap這個類加載器時,它沒有父加載器,然後它就到自己所管轄的範圍去找,找到了就加載出來,沒找到它就交給它的兒子加載器去加載,這時候它的兒子加載器才會去管轄的範圍找,如果又沒找到,那就又給兒子找,就這樣一直往下,當最後回到第一個類加載器的時候,如果還是沒找到的話就報異常。
圖文解說(2級标題)
它的好處是可以集中管理,有這樣一個情況,我有一個類要加載,這時候myclassloader1找到了這個類并加載了此類,而myclassloader2也找到了這個類也加載了此類,這時候記憶體中就存在了2個一樣的位元組碼,這樣就很浪費資源。
1-1、首先它會調用目前線程的類加載器去加載線程中的第一個類。
thread a = thread.currentthread();
system.out.println(a.getcontextclassloader().getclass().getname());
運作上面的代碼輸出:
sun.misc.launcher$appclassloader
1-2、如果類a中引用了類b,java虛拟機将使用加載類a的加載器來加載類b。
1-3、還可以直接調用classloader.loadclass()方法來指定某個類加載器去加載某個類。
thread a = thread.currentthread();
system.out.println(a.getcontextclassloader().getclass().getname());
a.setcontextclassloader(system.class.getclassloader());---------------------------------這裡不寫.getclass().getname())是因為null沒有位元組碼,寫了程式報空指針異常。
system.out.println(a.getcontextclassloader());
運作上面的代碼輸出:
sun.misc.launcher$appclassloader
null
問:能不能自己寫一個類叫java.lang.system?
答:一把情況下不能,因為委托機制的原因,當你寫了這個類時,到加載的時候,流程會一直想上走,當到bootstrap時,它發現自己的管轄範圍有,于是他就直接加載一個system給你了,而這個system是rt.jar中的類。如果自己寫個類加載器就可以了,或者類名相同但是包名不相同也是可以的,但是不能類名和包名都相同。比如樓主可以寫個類叫:com.lang.string。
1-1、自定義的類加載器必須繼承classloader
1-2、一般盡量不要覆寫已有的loadclass()方法中的委托邏輯
一般在jdk 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統預設的類加載器不能正常工作。在jvm規範和jdk文檔中(1.2或者以後版本中),都沒有建議使用者覆寫loadclass(…)方法,相比而言,明确提示開發者在開發自定義的類加載器時覆寫findclass(…)邏輯。
如果,覆寫loadclass方法,程式将不會用委托機制在建立代碼,是以一般都覆寫findclass方法。
問題一:自定義一個類,然後編寫一個簡單的加密算法(把類的位元組碼中的0換成1,把1換成0),并輸出到指定目錄下,然後再自定義一個類加載器,用來加載加密過的類。
建立一個用來加密的原始類,此類繼承java.util.date。并重寫tostring()方法。代碼如下
package shipin44;
import java.util.date;
public class classloaderattachment extends date {
public string tostring(){
return "hello,itcast";
}
編寫加密方法。代碼如下:
/**
* 将傳過來的二進制加密(把1變成0,把0變成1)
* cypher:暗号的意思
* @param ips:超類,為了能傳多種輸入流(裡氏代換原則)
* @param ops:超類,為了能傳多種輸出流(裡氏代換原則)
* @throws exception
*/
public static void cypher(inputstream ips,outputstream ops) throws exception{
int b = -1;
// ips.read();幫助文檔中的解釋
// read
// public abstract int read()
// throws ioexception從輸入流中讀取資料的下一個位元組。傳回 0 到 255 範圍内的 int 位元組值。如果因為已經到達流末尾而沒有可用的位元組,則傳回值 -1。在輸入資料可用、檢測到流末尾或者抛出異常前,此方法一直阻塞。
// 子類必須提供此方法的一個實作。
// 傳回:下一個資料位元組;如果到達流的末尾,則傳回 -1。
// 抛出: ioexception - 如果發生 i/o 錯誤。
while ((b = ips.read()) != -1) {
ops.write( b ^ 0xff);
//system.out.println(b+"\t");//列印輸出的内容看不懂
//system.out.println(b ^ 0xff);//列印輸出的内容看不懂
編寫執行加密操作的方法。代碼如下
* 開始做加密檔案的工作
* @param args:調用min方法時傳進來的參數
* @throws exception
public static void doworkcypher(string[] args) throws exception{
//源檔案路徑
string srcpath = args[0];
//檔案所在的目錄
string destdir = args[1];
//獲得檔案名
string destfilename = srcpath.substring(srcpath.lastindexof('\\')+1);
//目标檔案的相對路徑
string destpath = destdir + "\\" + destfilename;
//輸入流,把東西寫入記憶體中,srcpath原檔案的絕對路徑
fileinputstream fis = new fileinputstream(srcpath);
//輸入流,把東西從記憶體中寫出來,寫到硬碟上,destpath加密後的檔案的相對路徑
fileoutputstream fos = new fileoutputstream(destpath);
//加密類
cypher(fis, fos);
//關閉流
fis.close();
fos.close();
編寫main方法。
public static void main(string[] args) throws exception{
// todo auto-generated method stub
doworkcypher(args);
調用main方法
1、滑鼠右鍵點工程--->new--->folder--->輸入itcast
2、滑鼠右鍵點以上方法所屬的類--->run as--->java application,程式報錯,因為調用min方法時沒傳相應的參數進去。
3、滑鼠右鍵點以上方法所屬的類--->run as--->run confugurarion--->arguments--->program(節目) arguments(參數)--->輸入classloaderattachment.class所在的絕對路徑--->空格--->再輸入上面的folder名--->點apply--->最後run。
4、最後點項目工程f5重新整理後,會發現itcast檔案夾下多了個classloaderattachment.class檔案,此檔案是加過密的檔案。
測試結果
1、在一個min方法中列印system.out.println(new classloaderattachment().tostring());,輸出結果是:
hello,itcast
這時候的加載的類是原始的類
2、用itcast檔案夾下的classloaderattachment.class,把f:\java\bianchengruanjian\myeclipseworkspace\zhangxiaoxiangjichujiaqiang\bin\shipin44\classloaderattachment.class目錄下的原始.class(未加過密的)替換掉,運作system.out.println(new classloaderattachment().tostring());,程式報錯。
編寫自定義類加載器,此類必須繼承classloader。
重寫findclass方法,不能重寫loadclass方法。代碼如下:
//目錄名(itcast)
string classdir;
/**
* 重寫findclass方法
@override
protected class<?> findclass(string name) throws classnotfoundexception {
// todo auto-generated method stub
string classfilename = classdir + "\\" + name + ".class";
try {
fileinputstream fis = new fileinputstream(classfilename);
//加密的.class的絕對路徑,這樣也可以,這位置的classloaderattachment.class是加過密的
// classfilename = "c:\\users\\json\\desktop\\classloaderattachment.class";
bytearrayoutputstream bos = new bytearrayoutputstream();
cypher(fis, bos);
fis.close();
byte[] bytes = bos.tobytearray();
return defineclass(bytes, 0, bytes.length);
} catch (exception e) {
// todo auto-generated catch block
e.printstacktrace();
return super.findclass(name);
//構造函數
public myclassloader(string classdir){
this.classdir = classdir;
1、在一個min方法中運作system.out.println(new classloaderattachment().tostring());,列印輸出:
2、将加過密後的classloaderattachment.class替換掉未加過密的classloaderattachment.class檔案。在myeclipseworkspace\zhangxiaoxiangjichujiaqiang\bin\shipin44目錄下運作程式報錯,如下圖:
3、用自定義類加載器myclassloader加載,加過密的classloaderattachment,在min方法中運作如下代碼:
class clazz = new myclassloader("itcast").loadclass("classloaderattachment");
date d1 = (date) clazz.newinstance();
system.out.println(d1.getclass().getclassloader().getclass().getname());
system.out.println(d1.getclass().getclassloader().getparent().getclass().getname());
system.out.println(d1.tostring());
輸出結果:
shipin44.myclassloader
模闆方法模式(template method pattern),定義一個操作中的算法骨架,而将一些實作步驟延遲到子類當中。模闆方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟。
模闆方法模式是比較簡單的一種設計模式,但是它卻是代碼複用的一項基本的技術,在類庫中尤其重要,它遵循“抽象類應當擁有盡可能多的行為,應當擁有盡可能少的資料”的重構原則。作為模闆的方法要定義在父類中(并寫完此方法),在方法的定義中使用抽象方法,而隻看父類的抽象方法是根本不知道怎麼處理的,實際具體處理的是子類,在子類中實作具體功能,是以不同的子類執行将會得出不同的實作結果,但是處理流程還是按照父類定制的方式。這就是模闆方法的要義所在,定制算法骨架,讓子類具體實作。
1-1、一次性實作一個算法不變的部分,并将可變的行為留給子類來實作;
1-2、各子類中具有公共行為的時候,應該被提取出來并集中到一個公共父類中以避免代碼重複。
1-3、當需要控制子類擴充的時候。模闆方法在特定點調用鈎子操作,這樣就隻允許在這些點進行擴充。
下面簡單地描述一下上班族正常一天的生活行為
我們看到,每個人吃早餐和乘坐交通工具的方式都是個性化行為,但是每個人的行為架構确實一緻的,那就是:起床、吃早餐、乘坐交通工具、工作。而每個人吃早餐和乘坐交通工具的行為卻是要單獨實作的。
package com.demo;
public abstract class abstractpeople {
* 起床
public void getup(){
system.out.println("起床");
* 吃早餐
public abstract void havebreakfast();
* 抽象乘坐交通工具的方法
public abstract void transport();
* 工作
public void dowork(){
system.out.println("工作");
* 模闆方法(每天的行為)
public void daylift(){
system.out.println("-----------------------");
getup();
havebreakfast();
transport();
dowork();
system.out.println("------------------------");
public class peoplea extends abstractpeople {
* 具體吃早餐的方法
public void havebreakfast() {
system.out.println("吃三明治,喝牛奶");
* 具體做交通工具的方法
public void transport() {
system.out.println("開私家車上班");
public class peopleb extends abstractpeople {
system.out.println("喝粥,吃小菜");
system.out.println("坐公共汽車上班");
public class peoplec extends abstractpeople {
system.out.println("吃煎餅,喝豆漿");
system.out.println("做地鐵上班");
public class client {
* @param args
public static void main(string[] args) {
abstractpeople peoplea = new peoplea();
abstractpeople peopleb = new peopleb();
abstractpeople peoplec = new peoplec();
peoplea.daylift();
peopleb.daylift();
peoplec.daylift();
運作client類輸出結果:
-----------------------
起床
吃三明治,喝牛奶
開私家車上班
工作
------------------------
喝粥,吃小菜
坐公共汽車上班
吃煎餅,喝豆漿
做地鐵上班