java類加載的故事一、各種熱加載機(jī)制
故事起源: ?
從第一天學(xué)JAVA,就開始寫一個JAVA文件,定義main方法,然后寫下大名鼎鼎的System.out.println("Hello World")。然后,javac編譯成class文件,再java執(zhí)行,就能開始我們JAVA的愉快之旅。這個過程簡單愉悅,但是,一直都沒有深究這Hello World是怎樣如晴天霹靂一般出現(xiàn)在屏幕上的。學(xué)習(xí)JAVA一段時間之后,開始用Eclipse愉快的開發(fā)著各種各樣的框架,有一天,經(jīng)理讓我去linux上把項目部署起來。興奮的一上手,才突然發(fā)現(xiàn),原來服務(wù)器上沒有Eclipse!面對一大堆的jar包,完全不知道要怎么跑。再到多年以后,SpringBoot橫空出世,tomcat,jetty等中間件隱藏幕后,強(qiáng)大的J2EE又回歸到了JAVA指令來運行,各種部署調(diào)優(yōu),才發(fā)現(xiàn),這強(qiáng)大聽話的JAVA,運行底層別有洞天。
故事發(fā)展: ?
對JAVA底層的了解,從實用角度,莫過于反射和類加載了。這些底層的機(jī)制,在平常開發(fā)中用到的可能不多,但是在各種高大上的框架開發(fā)中,被大量的運用。 ? 之前的博客《JAVA基于注解的報表映射》中討論過通過反射和注解,基于JAVABEAN快速完成一個MVC的報表頁面,這一設(shè)想目前已經(jīng)實現(xiàn)在了自己的GenUI項目中,再結(jié)合freemarker生成很少量,基本不帶什么邏輯的controller、service等WEB應(yīng)用代碼,最終的效果可以只需要在數(shù)據(jù)庫中建好表,就可以一鍵生成針對表的增刪改查以及導(dǎo)出的管理頁面的全部功能。并且有完善的權(quán)限機(jī)制對頁面進(jìn)行權(quán)限管理,而且針對復(fù)雜查詢的場景,留有很方便的擴(kuò)展支持,并且支持圖形化的報表開發(fā)。很多大型應(yīng)用,UI管理只是其中很不愿花功夫但又很重要的一小塊,用GenUI快速搭建,還是能省不少事情的。有興趣的可以到碼云了解下。
故事繼續(xù): ?
既然談到了反射,后面肯定繞不開的就是類加載了。下面就結(jié)合自己的理解,談?wù)凧AVA類加載那些事。 ?
為了節(jié)約時間,先來個大綱把。
JAVA類加載的基礎(chǔ)知識--簡略 ?
外部Jar包加載
實現(xiàn)自定義加載 --不停機(jī)熱加載
實現(xiàn)CLASS防反編譯 ?
一、類加載的基礎(chǔ)知識:
先來個簡單粗暴的main方法,看看類加載器到底是什么玩意。
?public class LoaderDemo1 {
?
? public static void main(String[] args) throws Exception {
? ?//java指令可以通過增加-verbose:class -verbose:gc 參數(shù)在啟動時打印出類加載情況
? ?//BootStrap Classloader,加載java基礎(chǔ)類。這個屬性不能在java指令中指定,推斷不是由java語言處理。。
? ?System.out.println("BootStrap ClassLoader加載目錄:"+System.getProperty("sun.boot.class.path"));
? ?//Extention Classloader 加載JAVA_HOME/ext下的jar包。 可通過-D java.ext.dirs另行指定目錄
? ?? ?System.out.println("Extention ClassLoader加載目錄:"+System.getProperty("java.ext.dirs"));
? ?//AppClassLoader 加載CLASSPATH,應(yīng)用下的Jar包??赏ㄟ^-D java.class.path另行指定目錄
? ?System.out.println("AppClassLoader加載目錄:"+System.getProperty("java.class.path"));
? ?
? ?//父子關(guān)系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
? ?ClassLoader cl1 = LoaderDemo1.class.getClassLoader();
? ?System.out.println("cl1 > "+cl1);
? ?System.out.println("parent of cl1 > "+cl1.getParent());
? ?//BootStrap Classloader由C++開發(fā),是JVM虛擬機(jī)的一部分,本身不是JAVA類。
? ?System.out.println("grant parent of cl1 > "+cl1.getParent().getParent());
? ?//String,Int等基礎(chǔ)類由BootStrap Classloader加載。
? ?ClassLoader cl2 = String.class.getClassLoader();
? ?System.out.println("cl2 > "+ cl2);
? ?System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());
? }
?}
看看執(zhí)行結(jié)果,找找里面的ClassLoader看看
?BootStrap ClassLoader加載目錄:D:\Java\jdk1.8.0_144\jre\lib\resources.jar;D:\Java\jdk1.8.0_144\jre\lib\rt.jar;D:\Java\jdk1.8.0_144\jre\lib\sunrsasign.jar;D:\Java\jdk1.8.0_144\jre\lib\jsse.jar;D:\Java\jdk1.8.0_144\jre\lib\jce.jar;D:\Java\jdk1.8.0_144\jre\lib\charsets.jar;D:\Java\jdk1.8.0_144\jre\lib\jfr.jar;D:\Java\jdk1.8.0_144\jre\classes
?Extention ClassLoader加載目錄:D:\Java\jdk1.8.0_144\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
?AppClassLoader加載目錄:D:\Java\jdk1.8.0_144\jre\lib\resources.jar;D:\Java\jdk1.8.0_144\jre\lib\rt.jar;D:\Java\jdk1.8.0_144\jre\lib\jsse.jar;D:\Java\jdk1.8.0_144\jre\lib\jce.jar;D:\Java\jdk1.8.0_144\jre\lib\charsets.jar;D:\Java\jdk1.8.0_144\jre\lib\jfr.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;F:\workspace-oxygen\ClassLoaderDemo\bin
?cl1 > sun.misc.Launcher$AppClassLoader@2a139a55
?parent of cl1 > sun.misc.Launcher$ExtClassLoader@7852e922
?grant parent of cl1 > null
?cl2 > null
?null
???? 1. JAVA類加載類型: ?
JAVA的類加載器,父子關(guān)系如下:
?BootStrap ClassLoader -> Extention ClassLoader -> APP ClassLoader
其中,BootStrap ClassLoader是基礎(chǔ)類加載器,是C++編寫的,本身也是JVM虛擬機(jī)的一部分,屏蔽了系統(tǒng)之間的差異。他本身不是JAVA類,在JAVA中是不可見的。他主要負(fù)責(zé)加載#JRE_HOME#/lib下的主要jar包,如rt.jar;charset.jar等。如java.lang.String,java.lang.System,java.util.List等就是這個東東加載出來的。加載的jar包路徑最終會保存到 sun.boot.class.path 這個屬性中。--查看源碼就能看到,BootStrapClass的聲明是native,一個原生態(tài)的方法。 ? ExtentionClassLoader JAVA擴(kuò)展類加載器,主要加載#JRE_HOME#/lib/ext下的jar包,另外,可擴(kuò)展加載 -D java.ext.dirs選項目錄下的jar包。 ? APPClassLoader加載當(dāng)前應(yīng)用的classPath中的所有類。JAVA運行時通過指定ClassPath或者-cp指定依賴包,就是通過這個東東加載進(jìn)JVM的。 ?
???? 2. 雙親委派模型: ?

? 一個ClassLoader要加載一個class時,不會自己上來就加載,他會判斷類是否已經(jīng)加載過,加載過則返回緩存的類Class;沒有加載成功,則向上級加載器進(jìn)行加載申請,如果上級加載器加載過,就返回上級加載器加載的類;重復(fù)申請,直到BootStrap ClassLoader。如果所有父級ClassLoader都沒有加載過,那就由該ClassLoader自行加載。從下向上進(jìn)行委托,從上向下進(jìn)行查找。這也就解釋了為什么永遠(yuǎn)可以用System.out.println進(jìn)行打印而不怕被覆蓋掉。 ? 但是這里要搞清楚一點,父加載器并不是rt.jar中的父類。JAVA底層的東西不能全部用JAVA來解釋。JDK的類加載器集成關(guān)系大致如下: ?

? 其中這個URLCLassLoader,可以通過指定的URL加載Class.用這個類就能很容易的實現(xiàn)運行時加載某個固定目錄甚至是網(wǎng)絡(luò)上的class類了。 ?
二、外部Jar包加載:
前面說要簡略,但是最后還是說得羅里吧嗦。下面就盡量簡單點把。 ? 為了更容易理解,下面就設(shè)計實現(xiàn)一個簡單的場景:發(fā)工資。每個人的工資是額定的,但是碰上了一個無良經(jīng)理,到手之前,要經(jīng)過經(jīng)理審核,他偷偷克扣掉一部分后再發(fā)給別人。那我們簡單模擬下這個計算過程。
?public class SalaryCalDemo {
? public static void main(String[] args) throws Exception {
? ?Double salary = 1999.99;
? ?Double money = 0.00;
? ?while(true) {
? ? money = calSalary(salary);
? ? System.out.println("實際到手Money:"+money);
? ? Thread.sleep(1000);
? ?}
? }
? //計算薪水
? private static Double calSalary(Double salary) {
? ?SalaryCaler caler = new SalaryCaler();
? ?return caler.cal(salary);
? }
?}
這就是計算薪水的主邏輯,用一個while(true)用來模擬不停機(jī)場景,比如我們的OA系統(tǒng)。線程休眠模擬用戶的不定時請求,比如每月計算一次薪水。 ? calSalary方法專門用來進(jìn)行薪水計算。 ?這樣按照面向?qū)ο蟮乃枷耄x一個計算器,專門進(jìn)行計算:
?public class SalaryCaler {
? public Double cal(Double salary) {
? ?return salary*0.8;
? }
?}
好了。需求就這么愉快的實現(xiàn)了。然后呢?經(jīng)理發(fā)現(xiàn)這計算代碼和發(fā)工資代碼在一起,那發(fā)工資的人事不是就知道工資是怎么計算的了嗎?那不行, 那我克扣別人工資的事情他不就全都知道了? ? 于是,萬能的程序員找到了下面的解決辦法: ? 代碼在一起不安全是吧。那我把計算器SalaryCaler部署到另外一個工程,發(fā)布成一個jar包。讓薪水計算程序動態(tài)的去加載jar包。這樣發(fā)工資的人不就看不到代碼了嗎? 好。于是我們用Eclipse可以輕松的把這個SalaryCaler類export成一個Jar包。把他放到F:\lib目錄下,讓計算程序去這個jar包里獲取計算器的實現(xiàn)。于是,就有了下面這個樣子。
? //計算薪水
? private static Double calSalary(Double salary) throws Exception {
? ?? ?//運行時加載外部jar。加載一次就不能再更新。jar包刪掉也不行。
? ?URL url = new URL("file://F:/lib/");
? ?URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {url});
? ?Class<?> objClass = urlClassLoader.loadClass("com.roy.SalaryCaler");
? ?Object obj = objClass.newInstance();
? ?return (Double)objClass.getMethod("cal", Double.class).invoke(obj, salary);
? }
執(zhí)行起來很完美,也完成了經(jīng)理的要求。然后經(jīng)理就開始了下一步折騰。 ?
三、實現(xiàn)自定義加載 --不停機(jī)熱加載 ?
經(jīng)理偷偷達(dá)到了他的目的,但是,突然上面要開始徹查。經(jīng)理趕緊要求程序員把計算方法改掉,克扣的那一部分返還回去。 ? 好吧。程序員趕緊修改計算方法。
?public class SalaryCaler {
? public Double calSalary(Double salary) {
? ?return salary;
? }
?}
一通忙乎,重新打jar包,放到F:\lib。然后需要重啟整個OA系統(tǒng)。 ? 嗯,程序員剛放心呼了一口氣。經(jīng)理又來了。這樣每次調(diào)整計算方法都要重啟應(yīng)用,這可不行。OA系統(tǒng)那么多人要用,每次都重啟,別人不是一下就知道我的手段了嗎?要讓別人在不知情的情況下偷偷實現(xiàn)。 ? 麻煩又來了,程序員發(fā)現(xiàn),用URLClassLoader加載jar包,那只能一次性加載。加載完成后,jar包即使更新甚至刪除,SalaryCalDemo中獲取到的都是第一次加載出來的結(jié)果。因為按照J(rèn)DK的類加載機(jī)制,ClassLoader會把加載過的類緩存起來,下次如果發(fā)現(xiàn)緩存中有,就會返回緩存中的類,不會重新加載了。JDK中現(xiàn)成的類加載器看來是不行了,于是要開始實現(xiàn)自己的類加載器,實現(xiàn)熱加載,即jar包或者class文件,一丟上去就能生效。 有了這個思路,下面的東西就不賣關(guān)子了。簡單說明一下開發(fā)一個自定義類加載器的步驟:
編寫一個類繼承 ?ClassLoader抽象類或者其子類 ?
復(fù)寫他的findClass()方法 --注意雖然實際調(diào)用時loadClass()方法,但是這個是在ClassLoader基類中的方法,最好不要覆蓋。而findClass()方法為JDK的API中明確提供的一個擴(kuò)展點。
在findClass()方法中調(diào)用defineClas()方法實現(xiàn)最終加載 下面改造的代碼實現(xiàn)了兩種熱加載方式。 ?SalaryClassLoader實現(xiàn)了從文件系統(tǒng)中加載class文件。而SalaryJARLoader實現(xiàn)了從jar包中加載class文件。兩者實現(xiàn)的效果都是熱加載,即將SalaryCaler導(dǎo)出成class或者jar包,扔到指定目錄上,就能即使更新新的cal方法實現(xiàn)。 ?
SalaryCalDemo
?public class SalaryCalDemo {
? public static void main(String[] args) throws Exception {
? ?Double salary = 1999.99;
? ?Double money = 0.00;
? ?//模擬不停機(jī)狀態(tài)
? ?while (true) {
? ? try {
? ? ?//外部jar或者class文件替換過程中,讀取會有異常。加try保證程序不退出
? ? ?money = calSalary(salary);
? ? ?System.out.println("實際到手Money:" + money);
? ? }catch(Exception e) {
? ? ?System.out.println("加載出現(xiàn)異常 :"+e.getMessage());
? ? }
? ? Thread.sleep(1000);
? ?}
? }
?
? // 計算薪水
? private static Double calSalary(Double salary) throws Exception {
? ?// 啟動加載
? ?// SalaryCaler caler = new SalaryCaler();
? ?// return caler.cal(salary);
? ?// 運行時加載。不能熱更新
?// ?URL url = new URL("file://F:/lib/");
?// ?URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {url});
?// ?Class<?> objClass = urlClassLoader.loadClass("com.roy.SalaryCaler");
?// ?Object obj = objClass.newInstance();
?// ?return (Double)objClass.getMethod("cal", Double.class).invoke(obj, salary);
? ?// 運行時加載文件系統(tǒng)中的class文件。每次運行都重新加載。
? ?// String libPath="F:\\lib\\";
? ?// SalaryClassLoader classloader = new SalaryClassLoader(libPath);
? ?// Class<?> objClass = classloader.loadClass("com.roy.SalaryCaler");
? ?// Object obj = objClass.newInstance();
? ?// return (Double)objClass.getMethod("cal", Double.class).invoke(obj, salary);
? ?// 運行時加載jar包中的class文件。每次運行都重新加載。
? ?String jarPath = "F:/lib/SalaryCaler.jar";
? ?SalaryJARLoader classloader = new SalaryJARLoader(jarPath);
? ?Class<?> objClass = classloader.loadClass("com.roy.SalaryCaler");
? ?Object obj = objClass.newInstance();
? ?return (Double) objClass.getMethod("cal", Double.class).invoke(obj, salary);
? }
?}
SalaryClassLoader
//加載文件系統(tǒng)中的class文件 public class SalaryClassLoader extends SecureClassLoader{ private String libPath; public SalaryClassLoader(String libPath) { this.libPath = libPath; } @Override protected Class<?> findClass(String fullClassName) throws ClassNotFoundException { String classFilepath = this.getFileName(fullClassName); ? ? ? ? File file = new File(libPath,classFilepath); ? ? ? ? try { ? ? ? ? ?System.out.println("重新加載類:"+file.getPath()); ? ? ? ? ? ? FileInputStream is = new FileInputStream(file); ? ? ? ? ? ? ByteArrayOutputStream bos = new ByteArrayOutputStream(); ? ? ? ? ? ? int len = 0; ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? while ((len = is.read()) != -1) { ? ? ? ? ? ? ? ? ? ? bos.write(len); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? } ? ? ? ? ? ? byte[] data = bos.toByteArray(); ? ? ? ? ? ? is.close(); ? ? ? ? ? ? bos.close(); ? ? ? ? ? ? //重新加載類,老的類會等待GC ? ? ? ? ? ? return defineClass(fullClassName,data,0,data.length); ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? } ? ? ? ? //自己定義類不成功,就交由父類去加載 ? ? ? ? return super.findClass(fullClassName); } ? ?//獲取要加載 的class文件名 ? ? private String getFileName(String name) { ? ? ? ? // TODO Auto-generated method stub ? ? ? ? int index = name.lastIndexOf('.'); ? ? ? ? if(index == -1){ ? ? ? ? ? ? ?return name+".class"; ? ? ? ? }else{ ? ? ? ? ? ? return name.substring(index+1)+".class"; ? ? ? ? } ? ? } }
SalaryJARLoader
//加載jar包中的class文件 public class SalaryJARLoader extends SecureClassLoader { private String jarPath; public SalaryJARLoader(String jarPath) { this.jarPath = jarPath; } @Override protected Class<?> findClass(String fullClassName) throws ClassNotFoundException { String classFilepath = fullClassName.replace('.', '/').concat(".class"); try { ? ? //訪問jar包的url URL jarURL = new URL("jar:file:/" + jarPath + "!/" + classFilepath); System.out.println("重新加載類:"+jarURL); InputStream is = jarURL.openStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; while ((len = is.read()) != -1) { bos.write(len); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(fullClassName, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(e.getMessage()); } ?} }
忽忽悠悠忙乎了半天,程序員終于又一次成功的幫助惡毒的經(jīng)理達(dá)到了他的目的。 ? 這種熱加載貌似很好用嘛,那為什么又沒有大面積的使用呢?大名鼎鼎的SpringBoot的devtools支持熱更新也只是支持自動重啟應(yīng)用。因為這樣的實現(xiàn)暴露了熱加載幾個很重要的問題。我大致總結(jié)出來的問題如下:
這種熱加載機(jī)制使用相當(dāng)別扭,不但各種各樣的反射讓人暈頭轉(zhuǎn)向,而且繞過了JAVA語言所有的靜態(tài)檢查,方法不對?參數(shù)類型不對?這些原本能在編譯之前就暴露出來的問題全部拖到了運行時,對接口包的質(zhì)量要求不是一般的高。雖然能用定義接口,外部jar包繼承接口的方式做一定的優(yōu)化,但是,人家按不按你的來,誰知道呢。
還一個問題,這種頻繁的加載類-卸載類,對JVM虛擬機(jī)的內(nèi)存是一種不小的負(fù)擔(dān)。會在JVM堆內(nèi)存創(chuàng)建出大量的類,要靠GC進(jìn)行處理。但是GC,大家都懂的,什么時候干活是誰也說不準(zhǔn)的事情。而且,這些大量熱加載占用的內(nèi)存,是很難預(yù)估的,這也導(dǎo)致無法通過JVM參數(shù)提前申請好內(nèi)存,內(nèi)存隨時容易崩潰。 --說到這一點,就再提一下對這種熱加載機(jī)制減少內(nèi)存的一個方法。一般可以做一個緩存,把class文件的最后修改時間保存起來。每次加載時,通過最后修改時間來判斷jar包或者class文件是否有修改。有修改就重新加載,沒有修改就從緩存中拿。而且,這種重新判斷加載的操作,再盡量控制下操作頻率,在某些特定的場合,比如我們的這個迷你計算引擎,是完全可以用好的。這種擴(kuò)展的實現(xiàn),就不花功夫繼續(xù)丟人了,有興趣的自己擴(kuò)展。 ?
前面兩點總結(jié)完了,又該輪到惡毒的經(jīng)理出場了。雖然目前為止,OA系統(tǒng)的人事人員已經(jīng)接觸不到SalaryCaler的源碼了,但是,OA系統(tǒng)總還是要接觸到SalaryCaler的class文件的。而這時,碰到懂行的人,用jad等工具,還是很容易反編譯出SalaryCaler的源代碼,那不還是能夠知道薪水是怎么計算出來的嗎?而且,更可怕的是,class文件雖然是二進(jìn)制文件,理論上是很難被篡改的。但是如果是只在二進(jìn)制文件中修改幾個小的參數(shù),百度上搜一搜,會發(fā)現(xiàn)方法一大堆。那這樣,豈不是誰都可以任意修改自己的到手的工資了?那公司就玩不下去了。那好。下面就來討論最后一個問題,class如何防反編譯。
四、Class防反編譯 ? ?
首先說明一點背景,這個問題其實已經(jīng)不是上面的應(yīng)用程序員能夠深入的范疇了。既要防止非法的反編譯,又要保證class能夠正常的加載,這是算法工程師之間攻防博弈的戰(zhàn)場。我等應(yīng)用程序員只能稍微討論下思路,權(quán)當(dāng)拋磚引玉了。
java編譯出來的class文件,已經(jīng)是01組成的二進(jìn)制文件了。而防止反編譯的方式,當(dāng)然最常用的就是混淆。通過一些算法,把其中的一些0和1給打亂了,別人不就反編譯不過來了嗎?對我等程序員, 高大上的算法不懂,但是常用的異或、位移、截取等算法還是可以湊合上一點點的。稍微改一改,class文件反編譯的難度就會大上很多。
那回到我們上面的故事,看我們自定義的兩個classLoader的實現(xiàn),其實對class文件的讀取都是通過流的方式一個一個字節(jié)的讀取出來的。那如果要混淆業(yè)務(wù)代碼,可以在class生成之后,再啟動一個應(yīng)用程序,將SalaryCaler編譯生成的class文件以流的方式讀取出來,做一定的修改后再保存下來,扔到熱加載目錄上。然后加載過程中,再通過反向的操作把二進(jìn)制文件給讀回來,給類加載器去加載。這樣,就完成了Class防反編譯。這種實現(xiàn),參照上面的兩個自定義類加載器,很容易實現(xiàn),玩法也很隨意,就不多說了。但是可以很負(fù)責(zé)任的說,這種思路是可行的。
另外,這種方式有什么用?我們享一下,通過篡改jar包的二進(jìn)制流,我們是不是可以在class文件二進(jìn)制流的前面自己添加一個字節(jié),0或者1來代表文件的版本?那在我們的服務(wù)端和打包端對這個版本進(jìn)行匹配,是不是就可以分離出兩個不同的運行版本?如果再對版本進(jìn)行配置化,那是不是就可以快速實現(xiàn)一個灰度發(fā)布了?是不是又玩出了一片新天地?
當(dāng)然,繼續(xù)深入,有人還會說,這個class文件是被篡改了無法反編譯,但是classloader里面的加載算法還是可以看到,那別人還是可以通過反向操作把class給還原出來。這個情況,有一些方式可以避免,例如將熱加載的路徑換成一個網(wǎng)絡(luò)地址,這樣即便你能通過算法弄出一個有問題的class,也無法注入到我們的類加載中來。例如Drools就支持從maven庫中加載規(guī)則文件,這也是這種思想的一個體現(xiàn)。 ? 最后就這個安全問題,提一提我對這方面的看法了。 ? 一是世上本很難有完全的安全策略。安全問題永遠(yuǎn)是一個博弈的過程,只能適當(dāng),沒有完全。所以,關(guān)于class反編譯帶來的安全風(fēng)險,基本不可能完全通過軟件層面解決。多途徑的結(jié)合,例如上面引入網(wǎng)絡(luò)加載地址,就能在網(wǎng)絡(luò)層面增加很多安全策略。 ? 二是關(guān)于安全與性能的平衡。在我們這個場景中,對class文件每增加一層混淆計算,固然能夠增加破解、篡改的難度,提高安全性。但是,對于正常的加載,也同樣會增加性能的消耗,而且對于我們實現(xiàn)的這種運行時的熱加載,消耗會更為明顯。所以我覺得最終的平衡點是在保障性能的前提下,讓破解攻擊要付出的代價遠(yuǎn)遠(yuǎn)大于能獲得的收益,而不是想辦法徹底的斷絕破解攻擊的可能。 ?
故事完結(jié): ?
最后說明下,這博文固然參照了很多網(wǎng)上的資料,甚至有部分內(nèi)容就是直接復(fù)制粘貼的,但是整體絕對都是經(jīng)過自己消化整理出來的。所有代碼都是自己重新整理編寫,親測可用,各位放心。不過最好有興趣的朋友還是自己都跑一遍。所有的實驗,最后的代碼長這樣: ?

故事最后:
還有向大家推薦下我的GenUI項目,期待讓它多歷練歷練,維護(hù)更新成一個穩(wěn)定可靠的版本。期待下一個故事。。。。。。
另外,我們邪惡的經(jīng)理和萬能的程序員的故事并沒有就此結(jié)束,請大家繼續(xù)等待續(xù)集 。