原來類加載器這么簡單,手把手教你用純java代碼實現(xiàn)熱部署
1、什么是熱部署
相信大家在寫代碼的時候都有這個困擾,就是每次我改完代碼之后都需要重啟項目才能看到結果,小項目還好,啟動不會占用太多時間,如果是一個大項目,每次重啟都要耗費很多時間的情況下,這無疑對開發(fā)效率都會大幅下降,那么有沒有這樣一種技術? 我修改后無需重啟服務器,就可以馬上看到效果?我可以很肯定地回答你:“有”,就是熱部署技術,在你修改完代碼之后,這項技術會自動幫你重新加載被修改后class文件,真正實現(xiàn)實時查看改動的結果;
2、準備
要知道熱部署,就得先了解class加載機制,在我們啟動項目的時候,首先編輯器會將后綴為.java的文件編譯成.class文件,之后jvm虛擬機會把class文件轉成二進制的字節(jié)碼加載到內存里面,中間經過了一系列的鏈接過程,最后完成初始化,加載過程不是本文的重點,這里簡單說明略過;
3、類加載器的分類
類加載器基類叫做 ClassLoader,這是一個抽象類,既然是抽象就代表著它是可擴展的,所以衍生出了不同種類的類加器
BootstrapClassLoader :主要負責加載核心的類庫(java.lang.*等),構造ExtClassLoader和APPClassLoader。
ExtClassLoader?:主要負責加載?%JAVA_HOME%/jre/lib/ext??目錄下的一些擴展的jar。
AppClassLoader:主要負責加載應用程序的主函數(shù)類, 加載classPath目錄
UserClassLoader:?用戶自定義類加載器,名字隨便起,只需要繼承ClassLoader類即可;
3.1、BootstrapClassLoader
BootStrapClassLoader 是一個純的C++實現(xiàn),沒有對應的Java類。所以在Java中是取不到的。如果我們在 idea 編輯器中搜索BootStrapClassLoader,是找不到這個類的
如果一個類的classLoader是null。已經足可以證明他就是由BootStrapClassLoader 加載的,現(xiàn)在我們用一組代碼測試下
public static void main(String[] args) throws Exception {
String str = new String("123");
System.out.println("String類的類加載器為:"+str.getClass().getClassLoader());
}
java.lang.String 這個類是系統(tǒng)的類,當我們獲取這個類時,可以看到它的類加載器是null值,所以就可以斷定,String 使用的是BootStrapClassLoader加載的;
3.2、ExtClassLoader
ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類庫?,那么都有哪些類呢?看下圖就知道啦,主要加載下圖jar包中的類!此類繼承ClassLoader加載器;
需要注意的是,ExtClassLoader這個類是在 Launcher? 類里面的一個內部類
3.3、AppClassLoader
App就是應用程序的意思,所以這個類加載器就是用來加載我們用戶自己寫的java類;此類繼承ClassLoader加載器,需要注意的是,這個類也是在 Launcher? 類里面的一個內部類
接下來我們自己定義一個類 User.java ,然后在控制臺打印一下這個類的類加載器;
package com;
public class User {
public static void main(String[] args) {
System.out.println("User類的類加載器為:"+User.class.getClassLoader());
}
}
通過控制臺打印的結果可以得知,自定義的 User類 使用的是AppClassLoader 類加載器;
3.5、關于類加載,還有2點需要注意:
1、同一個classLoader只會生成一個相同的class對象
2、不同的ClassLoader 加載相同的Class對象不能互相強轉
為什么不能強轉呢?我們來寫一組代碼測試下就知道啦
先定一個 自動義類加載器? ? MyClassLoader.java
package com;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
/**
* 加載class文件
* 重寫此方法的目的是為了能讓此方法被外部調用,父類的 findClass 是 protected 修飾的,只能被子類調用
* @param name 類的全類名 示例: com.xd.User
* @return
* @throws ClassNotFoundException
*/
@Override
public Class> findClass(String name) throws ClassNotFoundException {
try {
String fileName = name.replaceAll("\.","/") + ".class";
fileName = "/"+ fileName;
// 獲取文件輸入流
InputStream is = this.getClass().getResourceAsStream(fileName);
// 讀取字節(jié)
byte[] b = new byte[is.available()];
is.read(b);
// 將byte字節(jié)流解析成jvm能夠識別的Class對象
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
定義一個 HotUserModel.java 類,用來實例化用的
package com.hot.deploy.model;
public class HotUserModel {
}
主程序入口測試
public static void main(String[] args) throws Exception {
// 默認加載器加載的 HotUserModel
HotUserModel hotUserModel = null;
// 使用自定義加載器加載 HotUserModel
MyClassLoader myClassLoader = new MyClassLoader();
Class> aClass =myClassLoader.findClass("com.hot.deploy.model.HotUserModel");
Object o = aClass.newInstance();
System.out.println("普通 HotUserModel 使用的類加載器為:" + HotUserModel.class.getClassLoader());
System.out.println();
System.out.println("使用自動義類加載器的 HotUserModel 使用的加載器為:" + o.getClass().getClassLoader());
System.out.println();
// 以下語句賦值會報錯
hotUserModel = (HotUserModel) o;
}
運行后打印結果如下
通過打印的結果可以看到,相同的類無法強轉,?因為使用了不同的類加載器;
4、雙親委派加載機制
當類加載器在加載一個類的時候,它首先不會馬上去加載這個類,而是先把這個請求委派給父類去完成,而父類接受到加載請求的時候也不會馬上去加載這個類,而是在交給自己的父類去加載,每一個層次加載器都是如此,直到父類加載器無法加載的時候,子類才會自己去嘗試完成加載,我們的要實現(xiàn)的熱部署原理就是要打破雙親委派加載機制,直接由子類來加載;而不去請求父加載器;
加載流程如下
將自定義加載器掛載到應用程序加載器
應用程序加載器將類加載請求委托給擴展類加載器
擴展類加載器將類加載請求委托給啟動類加載器
啟動類加載器在加載路徑下查找并加載Class文件,如果未找到目標Class文件,則交由擴展類加載器加載;
擴展類加載器在加載路徑下未找到目標Class文件,交由應用程序類加載器加載
應用程序類加載器在加載路徑下未找到目標Class文件,交由自定義加載器加載
自定義加載器在加載路徑下未找到目標Class文件,拋出ClassNotFound異常
4.1、雙親委派的作用
1、通過以上的委托流程,雙親委派機制保障了類的唯一性,
2、保證核心.class不被篡改,即使被篡改也不會被加載,即使被加載也不會是同一個class對象,因為不同的加載器加載同一個.class也不是同一個Class對象。這樣則保證了Class的執(zhí)行安全。
5、全盤負責委托機制
當一個A加載器加載一個類的時候,除非顯式地使用另一個加載器B,否則該類的所有依賴和引用都使用這個A加載器; 也就是說,如果一個類 為User類,這個User類的加載器是AppClassLoader加載器加載的,那么這個User類下面的所有引用類的加載器都是AppClassLoader;
接下來測試一把,先創(chuàng)建一個自定義類加載器?MyClassLoader.java
package com;
import java.io.InputStream;
import java.net.URL;
public class MyClassLoader extends ClassLoader{
/**
* 加載class文件
* 重寫此方法的目的是為了能讓此方法被外部調用,父類的 findClass 是 protected 修飾的,只能被子類調用
* @param name 類的全類名 示例: com.xd.User
* @return
* @throws ClassNotFoundException
*/
@Override
public Class> findClass(String name) throws ClassNotFoundException {
try {
// 獲取class文件名稱 去掉包路徑
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 獲取文件輸入流
InputStream is = this.getClass().getResourceAsStream(fileName);
// 讀取字節(jié)
byte[] b = new byte[is.available()];
is.read(b);
// 將byte字節(jié)流解析成jvm能夠識別的Class對象
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
用來創(chuàng)建對象的類 User.java
注意:在User的show方法里面創(chuàng)建了一個新的User對象,我們就是要測試這個新的User對象是否使用了全盤負責委托機制
package com;
public class User {
public void show(){
System.out.println("當前User使用的類加載器為:"+this.getClass().getClassLoader());
User user = new User();
System.out.println("新的User對象使用的類加載器為:"+user.getClass().getClassLoader());
}
}
創(chuàng)建main方法測試
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class> aClass = myClassLoader.findClass("com.User");
// 實例化User對象
Object o = aClass.newInstance();
Method show = aClass.getDeclaredMethod("show");
// 執(zhí)行show方法
show.invoke(o);
}
運行后打印結果如下,很明顯,全盤負責委托機制生效了,用的都是自定義加載類加載的class對象;
當前User使用的類加載器為:com.MyClassLoader@1d44bcfa
新的User對象使用的類加載器為:com.MyClassLoader@1d44bcfa
Process finished with exit code 0
6、熱部署代碼
實現(xiàn)熱部署需要做2件事,一是打破雙親委派機制,二是使用全盤委托機制實現(xiàn)熱部署(廢話,默認是使用的);廢話不多說,上代碼,需要注意的是,熱部署的代碼和上面5步的代碼沒有任何關系,請不要將其混淆
HotUserModel.java
package com.hot.deploy.model;
public class HotUserModel {
// 用戶名
private String userName="yexindong";
// 年齡
private Integer age = 18;
public void show(){
System.out.println("大家好,我叫:"+userName+",我的年齡是:"+age);
System.out.println("當前使用的類加載器:"+this.getClass().getClassLoader());
}
}
自定義類加載器? HotClassLoader.java
package com.hot.deploy;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class HotClassLoader extends ClassLoader{
// 項目目錄
private String projectPath;
// class的包路徑列表 格式: [com.xxx.App.class]
private List
/**
*
* @param projectPath 項目絕對路徑
* @param classPaths class文件的完整路徑列表
* @throws IOException
*/
public HotClassLoader(String projectPath,String... classPaths) throws IOException {
// 空格解碼
this.projectPath = projectPath.replaceAll("%20"," ");
classList = new ArrayList
// 掃描包路徑,并加載類,打破雙親委派機制
for (String classPath : classPaths) {
// 轉為路徑
classPath = classPath.replaceAll("\\.", "/");
// 空格解碼
classPath.replaceAll("%20"," ");
File file = new File(classPath);
readFile(file);
}
}
public void readFile(File file) throws IOException {
if (file.isDirectory()) {
for (File child : file.listFiles()) {
readFile(child);
}
} else {
// 加載類
String fileName = file.getName();
String suffix = fileName.substring(fileName.lastIndexOf(".")+1);
if(!"class".equals(suffix)){
return;
}
//將class文件轉為字節(jié)碼
InputStream inputStream = new FileInputStream(file);
byte[] classByte = new byte[(int)file.length()];
inputStream.read(classByte);
// 獲取全類名 com.xx.xx.class
String className = this.getClassName(file);
// 記錄已被加載過的class文件
classList.add(className);
// 加載class到jvm虛擬機
defineClass(className,classByte, 0,classByte.length);
}
}
/**
* 獲取全類名 com.xx.xx.class
* @param file
* @return
*/
public String getClassName(File file){
String className = file.getPath().replace(projectPath, "");
// 去掉.class
className = className.substring(1,className.indexOf("."));
// 將斜杠轉為.
className = className.replaceAll("/",".");
return className;
}
// 這個方法可改可不改
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
Class> loadedClass = findLoadedClass(name);
if(loadedClass == null ){
if(classList.contains(name)){
throw new ClassNotFoundException("找不到類");
}
// 調用系統(tǒng)類加載器
loadedClass = getSystemClassLoader().loadClass(name);
}
return loadedClass;
// return super.loadClass(name);
}
}
Application.java
package com.hot.deploy;
import com.hot.deploy.model.HotUserModel;
import java.io.File;
import java.lang.reflect.Method;
public class Application {
public void showApplication(){
// 創(chuàng)建新對象,這個創(chuàng)建對象時使用的類加載器也是自定義的加載器
new HotUserModel().show();
}
/**
* 開始加載
* @param classLoader
* @throws Exception
*/
public static void start0(HotClassLoader classLoader) throws Exception {
// 加載當前類Application
Class> aClass = classLoader.loadClass("com.hot.deploy.Application");
// 調用showApplication方法
Method show = aClass.getDeclaredMethod("showApplication");
Object o = aClass.newInstance();
Object invoke = show.invoke(o);
// 延時一秒
Thread.sleep(1000);
}
/**
* 使用死循環(huán)來重新加載class,每次循環(huán)加載的class都是new出來的新對象
* @throws Exception
*/
public static void run() throws Exception {
for(;;){
// 獲取項目的目錄
String projectPath = HotClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
projectPath = new File(projectPath).getPath();
// 使用自定義加載器加載所有的對象
HotClassLoader hotClassLoader = new HotClassLoader(projectPath, projectPath + "/com");
// 開始加載
start0(hotClassLoader);
}
}
}
創(chuàng)建一個啟動類來運行? MainApp.java
package com.hot.deploy;
public class MainApp {
/**
* 運行main 方法后,修改 HotUserModel 類的System.out.println()打印的內容,然后按一下右上角的綠色錘子(Build Project)就可以看到效果了
*
* 熱部署實現(xiàn)原理: 是因為打破了雙親委派機制 和 全盤委托 機制;
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Application.run();
}
}
先運行main方法,在運行的同時,先修改HotUserModel類的userName屬性
然后點擊idea 右上角的綠色的錘子進行手動編譯
編譯完成后,馬上就可以在控制臺看到修改的內容了
完
熱部署這塊確實有些晦澀難懂,但是只要認真學習,多去網(wǎng)上找找資料學習,總會茅塞頓開的,俗話說,難的不會,會的不難,說的就是這個道理,當你不知道的時候,你會覺得好復雜,當你學會之后會覺得也就那樣!也希望童鞋們不要停下學習的腳步,向著詩和遠方,我們一起出發(fā)!!
Java JVM
版權聲明:本文內容由網(wǎng)絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內刪除侵權內容。