JVM自定義類加載器在代碼擴(kuò)展性實(shí)踐分享
一、背景
名單管理系統(tǒng)是手機(jī)上各個(gè)模塊將需要管控的應(yīng)用配置到文件中,然后下發(fā)到手機(jī)上進(jìn)行應(yīng)用管控的系統(tǒng),比如各個(gè)應(yīng)用的耗電量管控;各個(gè)模塊的管控應(yīng)用文件考慮到安全問題,有自己的不同的加密方式,按照以往的經(jīng)驗(yàn),我們可以利用模板方法+工廠模式來根據(jù)模塊的類型來獲取到不同的加密方法。
代碼類層次結(jié)構(gòu)示意如下:

獲取不同加密方法的類結(jié)構(gòu)圖:
利用工廠模式和模板方法模式,在有新的加密方法時(shí),我們可以通過添加新的handler來滿足"對(duì)修改關(guān)閉,對(duì)擴(kuò)展開放"的原則,但是這種方式不可避免的需要修改代碼和需要重新發(fā)版本和上線。那么有沒有更好的方式能夠去解決這個(gè)問題,這里就是我們今天要重點(diǎn)講的主題。
二、類加載的時(shí)機(jī)
一個(gè)類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期將會(huì)經(jīng)歷加載 (Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱為連接(Linking)。這七個(gè)階段的發(fā)生順序如圖1所示。

雖然classloader的加載過程有復(fù)雜的7步,但事實(shí)上除了加載之外的四步,其它都是由JVM虛擬機(jī)控制的,我們除了適應(yīng)它的規(guī)范進(jìn)行開發(fā)外,能夠干預(yù)的空間并不多。而加載則是我們控制classloader實(shí)現(xiàn)特殊目的最重要的手段了。也是接下來我們介紹的重點(diǎn)了。
三、加載
“加載”(Loading)階段是整個(gè)“類加載”(Class Loading)過程中的一個(gè)階段。
在加載階段,Java虛擬機(jī)需要完成以下三件事情:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。
《Java虛擬機(jī)規(guī)范》對(duì)這三點(diǎn)沒有進(jìn)行特別具體的要求,從而留給虛擬機(jī)實(shí)現(xiàn)與Java應(yīng)用的靈活度都是相當(dāng)大的。例如“通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”這條規(guī)則,它并沒有指明二 進(jìn)制字節(jié)流必須得從某個(gè)Class文件中獲取,確切地說是根本沒有指明要從哪里獲取、如何獲取。比如我們可以從ZIP壓縮包中讀取、從網(wǎng)絡(luò)中獲取、運(yùn)行時(shí)計(jì)算生成、由其他文件生成、從數(shù)據(jù)庫中讀取。也可以可以從加密文件中獲取。
從這里我們可以看出,只需要我們能夠獲取到加密類的.class文件,我們就可以通過類加載器獲取到對(duì)應(yīng)的加密類class對(duì)象,進(jìn)而通過反射去調(diào)用具體的加密方法。因此類加載器在.class文件的加載過程有著至關(guān)重要的地位。
四、雙親委派模型
目前Java虛擬機(jī)已經(jīng)存在三種類加載器,分別為啟動(dòng)類加載器、擴(kuò)展類加載器和應(yīng)用程序類加載器;絕大多數(shù)的Java程序都會(huì)使用這三種類加載器進(jìn)行加載。

4.1 啟動(dòng)類加載器
這個(gè)類由C++實(shí)現(xiàn),負(fù)責(zé)加載存放在\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機(jī)能夠識(shí)別的(按照文件名識(shí)別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會(huì)被加載)類庫加載到虛擬機(jī)的內(nèi)存中。啟動(dòng)類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時(shí), 如果需要把加載請(qǐng)求委派給引導(dǎo)類加載器去處理,那直接使用null代替即可。
4.2 擴(kuò)展類加載器
這個(gè)類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實(shí)現(xiàn)的。它負(fù)責(zé)加載\lib\ext目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫。根據(jù)“擴(kuò)展類加載器”這個(gè)名稱,就可以推斷出這是一種Java系統(tǒng)類庫的擴(kuò)展機(jī)制,JDK的開發(fā)團(tuán)隊(duì)允許用戶將具有通用性的類庫放置在ext目錄里以擴(kuò)展Java SE的功能,在JDK9之后,這種擴(kuò)展機(jī)制被模塊化帶來的天然的擴(kuò)展能力所取代。由于擴(kuò)展類加載器是由Java代碼實(shí)現(xiàn)的,開發(fā)者可以直接在程序中使用擴(kuò)展類加載器來加載Class文件。
4.3 應(yīng)用程序類加載器
這個(gè)類加載器由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)。由于應(yīng)用程序類加載器是ClassLoader類中的getSystemClassLoader()方法的返回值,所以有些場(chǎng)合中也稱它為“系統(tǒng)類加載器”。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個(gè)類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
由于現(xiàn)有的類加載器加載路徑都有特殊的要求,自己所編譯的加密類所產(chǎn)生的.class文件所存放的路徑不在三個(gè)現(xiàn)有類加載器的路徑里面,因此我們有必要自己定義類加載器。
五、自定義類加載器
除了根類加載器,所有類加載器都是ClassLoader的子類。所以我們可以通過繼承ClassLoader來實(shí)現(xiàn)自己的類加載器。
ClassLoader類有兩個(gè)關(guān)鍵的方法:
- protected Class loadClass(String name, boolean resolve):name為類名,resove如果為true,在加載時(shí)解析該類。
- protected Class findClass(String name) :根據(jù)指定類名來查找類。
所以,如果要實(shí)現(xiàn)自定義類,可以重寫這兩個(gè)方法來實(shí)現(xiàn)。但推薦重寫findClass方法,而不是重寫loadClass方法,重寫loadClass方法可能會(huì)破壞類加載的雙親委派模型,因?yàn)閘oadClass方法內(nèi)部會(huì)調(diào)用findClass方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}loadClass加載方法流程:
- 判斷此類是否已經(jīng)加載;
- 如果父加載器不為null,則使用父加載器進(jìn)行加載;反之,使用根加載器進(jìn)行加載;
- 如果前面都沒加載成功,則使用findClass方法進(jìn)行加載。
所以,為了不影響類的加載過程,我們重寫findClass方法即可簡(jiǎn)單方便的實(shí)現(xiàn)自定義類加載。
六、代碼實(shí)現(xiàn)
6.1 實(shí)現(xiàn)自定義的類加載器
public class DynamicClassLoader extends ClassLoader {
private static final String CLASS_EXTENSION = "class";
@Override
public Class<?> findClass(String encryptClassInfo) {
EncryptClassInfo info = JSON.parseObject(encryptClassInfo, EncryptClassInfo.class);
String filePath = info.getAbsoluteFilePath();
String systemPath = System.getProperty("java.io.tmpdir");
String normalizeFileName = FilenameUtils.normalize(filePath, true);
if (StringUtils.isEmpty(normalizeFileName) || !normalizeFileName.startsWith(systemPath)
||getApkFileExtension(normalizeFileName) == null
|| !CLASS_EXTENSION.equals(getApkFileExtension(normalizeFileName))) {
return null;
}
String className = info.getEncryptClassName();
byte[] classBytes = null;
File customEncryptFile = new File(filePath);
try {
Path path = Paths.get(customEncryptFile.toURI());
classBytes = Files.readAllBytes(path);
} catch (IOException e) {
log.info("加密錯(cuò)誤", e);
}
if (classBytes != null) {
return defineClass(className, classBytes, 0, classBytes.length);
}
return null;
}
private static String getApkFileExtension(String fileName) {
int index = fileName.lastIndexOf(".");
if (index != -1) {
return fileName.substring(index + 1);
}
return null;
}
}這里主要是通過集成ClassLoader,復(fù)寫findClass方法,從加密類信息中獲取到對(duì)應(yīng)的.class文件信息,最后獲取到加密類的對(duì)象
6.2 .class文件中的encrypt()方法
public String encrypt(String rawString) {
String keyString = "R.string.0x7f050001";
byte[] enByte = encryptField(keyString, rawString.getBytes());
return Base64.encode(enByte);
}
6.3 具體的調(diào)用
public class EncryptStringHandler {
private static final Map<String, Class<?>> classMameMap = new HashMap<>();
@Autowired
private VivofsFileHelper vivofsFileHelper;
@Autowired
private DynamicClassLoader dynamicClassLoader;
public String encryptString(String fileId, String encryptClassName, String fileContent) {
try {
Class<?> clazz = obtainEncryptClass(fileId, encryptClassName);
Object obj = clazz.newInstance();
Method method = clazz.getMethod("encrypt", String.class);
String encryptStr = (String) method.invoke(obj, fileContent);
log.info("原字符串為:{},加密后的字符串為:{}", fileContent, encryptStr);
return encryptStr;
} catch (Exception e) {
log.error("自定義加載器加載加密類異常", e);
return null;
}
}
private Class<?> obtainEncryptClass(String fileId, String encryptClassName) {
Class<?> clazz = classMameMap.get(encryptClassName);
if (clazz != null) {
return clazz;
}
String absoluteFilePath = null;
try {
String domain = VivoConfigManager.getString("vivofs.host");
String fullPath = domain + "/" + fileId;
File classFile = vivofsFileHelper.downloadFileByUrl(fullPath);
absoluteFilePath = classFile.getAbsolutePath();
EncryptClassInfo encryptClassInfo = new EncryptClassInfo(encryptClassName, absoluteFilePath);
String info = JSON.toJSONString(encryptClassInfo);
clazz = dynamicClassLoader.findClass(info);
//設(shè)置緩存
Assert.notNull(clazz, "自定義類加載器加載加密類異常");
classMameMap.put(encryptClassName, clazz);
return clazz;
} finally {
if (absoluteFilePath != null) {
FileUtils.deleteQuietly(new File(absoluteFilePath));
}
}
}
}通過上述代碼的實(shí)現(xiàn),我們可以通過在管理平臺(tái)添加編譯好的.class文件,最后通過自定義的類加載器和反射調(diào)用方法,來實(shí)現(xiàn)具體方法的調(diào)用,避免了我們需要修改代碼和重新發(fā)版來適應(yīng)不斷新增加密方法的問題。
七、問題
上面的代碼在本地測(cè)試時(shí),沒有出現(xiàn)任何異常,但是部署到測(cè)試服務(wù)器以后出現(xiàn)了JSON解析異常,看上去貌似是json字符串的格式不對(duì)。

json解析邏輯主要存在于DynamicClassLoader#findClass方法入口處的將字符串轉(zhuǎn)換為對(duì)象邏輯,為什么這里會(huì)報(bào)錯(cuò),我們?cè)谌肟谔幋蛴×巳雲(yún)ⅰ?/p>

發(fā)現(xiàn)這里除了我們需要的正確的入?yún)?第一個(gè)入?yún)⑿畔⒋蛴?外,還多了一個(gè)Base64的全路徑名cn.hutool.core.codec.Base64。出現(xiàn)這種情況,說明由于我們重寫了ClassLoader的findClass方法,而Base64加載的時(shí)候會(huì)調(diào)用原始的ClassLoader類的loadClass方法去加載,并且里面調(diào)用了findClass方法,由于findClass已經(jīng)被重寫,所以就會(huì)報(bào)上面的json解析錯(cuò)誤。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}但是這里期望的是除了用于加密的.class文件用自定義類加載器進(jìn)行以外,不希望其他的類用自定義類加載器加載,通過對(duì)ClassLoader#loadClass方法分析,那么我們就希望能否通過其父類加載器加載到Base64這個(gè)三方類。因?yàn)閱?dòng)類加載器Bootstrap Class Loader肯定不能加載到Base64,所以我們需要顯示的設(shè)置父類加載器,但是這個(gè)父類加載器究竟設(shè)置為哪一個(gè)類加載器,那么就需要我們了解Tomcat類加載器結(jié)構(gòu)。
為什么Tomcat需要在JVM基礎(chǔ)之上做一套類加載結(jié)構(gòu),主要是為了解決如下問題:
- 部署在同一個(gè)服務(wù)器上的兩個(gè)web應(yīng)用程序所使用的Java類庫可以實(shí)現(xiàn)相互隔離;
- 部署在同一個(gè)服務(wù)器上的兩個(gè)web應(yīng)用程序所使用的Java類庫可以實(shí)現(xiàn)共享;
- 服務(wù)器需要盡可能保證自身安全,服務(wù)器所使用的類庫應(yīng)該與應(yīng)用程序的類庫相互獨(dú)立;
- 支持JSP應(yīng)用的Web服務(wù)器,大對(duì)數(shù)需要支持HotSwap功能。
為此,tomcat擴(kuò)展出了Common類加載器(CommonClassLoader)、Catalina類加載器(CatalinaClassLoader)、Shared類加載器(SharedClassLoader)和WebApp類加載器(WebAppClassLoader),他們分別加載/commons/、/server/、/shared/*和/WebApp/WEB-INF/*中的Java類庫的邏輯。

通過分析,我們知道WebAppClassLoader類加載器可以加載到/WEB-INF/*目錄下的依賴包,而我們所依賴的類cn.hutool.core.codec.Base64所在的包hutool-all-4.6.10-sources.jar就存在于/WEB-INF/*目錄下面,并且我們自定義類加載器所在的包 vivo-namelist-platform-service-1.0.6.jar也在/WEB-INF/*下,所以自定義類加載器DynamicClassLoader也是WebAppClassLoader加載的。
我們可以寫一個(gè)測(cè)試類測(cè)試一下:
@Slf4j
@Component
public class Test implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("classLoader DynamicClassLoader:" + DynamicClassLoader.class.getClassLoader().toString());
}
}
測(cè)試結(jié)果:

所以我們可以設(shè)置自定義類加載器DynamicClassLoader的父加載器為加載其本身的類加載器:
public DynamicClassLoader() {
super(DynamicClassLoader.class.getClassLoader());
}
我們?cè)俅螆?zhí)行文件的加解密操作時(shí),已經(jīng)沒有發(fā)現(xiàn)報(bào)錯(cuò),并且通過添加日志,我們可以看到加載類cn.hutool.core.codec.Base64對(duì)應(yīng)的類加載器確實(shí)為加載DynamicClassLoader對(duì)應(yīng)的類加載器WebAppClassLoader。
public String encrypt(String rawString) {
log.info("classLoader Base64:{}", Base64.class.getClassLoader().toString());
String keyString = "R.string.0x7f050001";
byte[] enByte = encryptField(keyString, rawString.getBytes());
return Base64.encode(enByte);
}

現(xiàn)在再來思考一下,為什么在IDEA運(yùn)行環(huán)境下不需要設(shè)置自定義類加載器的父類加載器就可以加載到cn.hutool.core.codec.Base64。
在IDEA運(yùn)行環(huán)境下添加如下打印信息:
public String encrypt(String rawString) {
System.out.println("類加載器詳情...");
System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().toString());
System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().getParent().toString());
String classPath = System.getProperty("java.class.path");
System.out.println("classPath:" + classPath);
System.out.println("classLoader Base64:" + Base64.class.getClassLoader().toString());
String keyString = "R.string.0x7f050001";
byte[] enByte = encryptField(keyString, rawString.getBytes());
return Base64.encode(enByte);
}

發(fā)現(xiàn)加載.class文件的類加載器為自定義類加載器DynamicClassLoader,并且.class加載器的父類加載器為應(yīng)用類加載器AppClassLoader,加載cn.hutool.core.codec.Base64的類加載器也是AppClassLoader。
具體的加載流程如下:
1)先由自定義類加載器委托給AppClassLoader;
2)AppClassLoader委托給父類加載器ExtClassLoader;
3)ExtClassLoader再委托給BootStrapClassLoader,但是BootClassLoader無法加載到,于是ExtClassLoader自己進(jìn)行加載,也無法加載到;
4)再由AppClassLoader進(jìn)行加載;
AppClassLoader會(huì)調(diào)用其父類UrlClassLoader的findClass方法進(jìn)行加載;
5)最終從用戶類路徑j(luò)ava.class.path中加載到cn.hutool.core.codec.Base64。


由此,我們發(fā)現(xiàn)在IDEA環(huán)境下面,自定義的加密類.class文件中依賴的三方cn.hutool.core.codec.Base64是可以通過AppClassLoader進(jìn)行加載的。
而在linux環(huán)境下面,經(jīng)過遠(yuǎn)程調(diào)試,發(fā)現(xiàn)初始時(shí)加載cn.hutool.core.codec.Base64的類加載器為DynamicClassLoader。然后委托給父類加載器AppClassLoader進(jìn)行加載,根據(jù)雙親委派原理,后續(xù)會(huì)交由AppClassLoader自己進(jìn)行處理。但是在用戶路徑下仍然沒有找到類cn.hutool.core.codec.Base64,最終交由DynamicClassLoader進(jìn)行加載,最終出現(xiàn)了最開始的JSON解析錯(cuò)誤。




八、總結(jié)
由于類加載階段沒有嚴(yán)格限制如何獲取一個(gè)類的二進(jìn)制字節(jié)流,因此給我們提供一個(gè)通過自定義類加載器來動(dòng)態(tài)加載.class文件實(shí)現(xiàn)代碼可擴(kuò)展性的可能。通過靈活自定義classloader,也可以在其他領(lǐng)域發(fā)揮重要作用,例如實(shí)現(xiàn)代碼加密來避免核心代碼泄漏、解決不同服務(wù)依賴同一個(gè)包的不同版本所引起的沖突問題以及實(shí)現(xiàn)程序熱部署來避免調(diào)試時(shí)頻繁重啟應(yīng)用。
到此這篇關(guān)于JVM自定義類加載器在代碼擴(kuò)展性實(shí)踐分享的文章就介紹到這了,更多相關(guān)JVM加載器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Spring Cloud中Hystrix的請(qǐng)求合并
這篇文章主要介紹了詳解Spring Cloud中Hystrix的請(qǐng)求合并,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)
這篇文章主要介紹了SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
SpringAop實(shí)現(xiàn)原理及代理模式詳解
Spring的AOP就是通過動(dòng)態(tài)代理實(shí)現(xiàn)的,使用了兩個(gè)動(dòng)態(tài)代理,分別是JDK的動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理,本文重點(diǎn)給大家介紹下SpringAop實(shí)現(xiàn)原理及代理模式,感興趣的朋友一起看看吧2022-04-04
從字節(jié)碼角度解析synchronized和反射實(shí)現(xiàn)原理
這篇文章主要介紹了從字節(jié)碼角度解析synchronized和反射的實(shí)現(xiàn)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
ZooKeeper集群操作及集群Master選舉搭建啟動(dòng)
這篇文章主要為大家介紹了ZooKeeper集群操作及集群Master選舉搭的建啟動(dòng)詳解,<BR>有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Java?JWT實(shí)現(xiàn)跨域身份驗(yàn)證方法詳解
JWT(JSON?Web?Token)是目前流行的跨域認(rèn)證解決方案,是一個(gè)開放標(biāo)準(zhǔn)(RFC?7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對(duì)象在各方之間安全地傳輸信息。本文將介紹JWT如何實(shí)現(xiàn)跨域身份驗(yàn)證,感興趣的可以學(xué)習(xí)一下2022-01-01
Spring boot注解@Async線程池實(shí)例詳解
這篇文章主要介紹了Spring boot注解@Async線程池實(shí)例詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12

