JVM自定義類加載器在代碼擴(kuò)展性實(shí)踐分享
一、背景
名單管理系統(tǒng)是手機(jī)上各個模塊將需要管控的應(yīng)用配置到文件中,然后下發(fā)到手機(jī)上進(jìn)行應(yīng)用管控的系統(tǒng),比如各個應(yīng)用的耗電量管控;各個模塊的管控應(yīng)用文件考慮到安全問題,有自己的不同的加密方式,按照以往的經(jīng)驗(yàn),我們可以利用模板方法+工廠模式來根據(jù)模塊的類型來獲取到不同的加密方法。
代碼類層次結(jié)構(gòu)示意如下:
獲取不同加密方法的類結(jié)構(gòu)圖:
利用工廠模式和模板方法模式,在有新的加密方法時,我們可以通過添加新的handler來滿足"對修改關(guān)閉,對擴(kuò)展開放"的原則,但是這種方式不可避免的需要修改代碼和需要重新發(fā)版本和上線。那么有沒有更好的方式能夠去解決這個問題,這里就是我們今天要重點(diǎn)講的主題。
二、類加載的時機(jī)
一個類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載 (Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗(yàn)證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking)。這七個階段的發(fā)生順序如圖1所示。
雖然classloader的加載過程有復(fù)雜的7步,但事實(shí)上除了加載之外的四步,其它都是由JVM虛擬機(jī)控制的,我們除了適應(yīng)它的規(guī)范進(jìn)行開發(fā)外,能夠干預(yù)的空間并不多。而加載則是我們控制classloader實(shí)現(xiàn)特殊目的最重要的手段了。也是接下來我們介紹的重點(diǎn)了。
三、加載
“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段。
在加載階段,Java虛擬機(jī)需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
《Java虛擬機(jī)規(guī)范》對這三點(diǎn)沒有進(jìn)行特別具體的要求,從而留給虛擬機(jī)實(shí)現(xiàn)與Java應(yīng)用的靈活度都是相當(dāng)大的。例如“通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”這條規(guī)則,它并沒有指明二 進(jìn)制字節(jié)流必須得從某個Class文件中獲取,確切地說是根本沒有指明要從哪里獲取、如何獲取。比如我們可以從ZIP壓縮包中讀取、從網(wǎng)絡(luò)中獲取、運(yùn)行時計算生成、由其他文件生成、從數(shù)據(jù)庫中讀取。也可以可以從加密文件中獲取。
從這里我們可以看出,只需要我們能夠獲取到加密類的.class文件,我們就可以通過類加載器獲取到對應(yīng)的加密類class對象,進(jìn)而通過反射去調(diào)用具體的加密方法。因此類加載器在.class文件的加載過程有著至關(guān)重要的地位。
四、雙親委派模型
目前Java虛擬機(jī)已經(jīng)存在三種類加載器,分別為啟動類加載器、擴(kuò)展類加載器和應(yīng)用程序類加載器;絕大多數(shù)的Java程序都會使用這三種類加載器進(jìn)行加載。
4.1 啟動類加載器
這個類由C++實(shí)現(xiàn),負(fù)責(zé)加載存放在\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機(jī)能夠識別的(按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)的內(nèi)存中。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時, 如果需要把加載請求委派給引導(dǎo)類加載器去處理,那直接使用null代替即可。
4.2 擴(kuò)展類加載器
這個類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實(shí)現(xiàn)的。它負(fù)責(zé)加載\lib\ext目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫。根據(jù)“擴(kuò)展類加載器”這個名稱,就可以推斷出這是一種Java系統(tǒng)類庫的擴(kuò)展機(jī)制,JDK的開發(fā)團(tuán)隊允許用戶將具有通用性的類庫放置在ext目錄里以擴(kuò)展Java SE的功能,在JDK9之后,這種擴(kuò)展機(jī)制被模塊化帶來的天然的擴(kuò)展能力所取代。由于擴(kuò)展類加載器是由Java代碼實(shí)現(xiàn)的,開發(fā)者可以直接在程序中使用擴(kuò)展類加載器來加載Class文件。
4.3 應(yīng)用程序類加載器
這個類加載器由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)。由于應(yīng)用程序類加載器是ClassLoader類中的getSystemClassLoader()方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
由于現(xiàn)有的類加載器加載路徑都有特殊的要求,自己所編譯的加密類所產(chǎn)生的.class文件所存放的路徑不在三個現(xiàn)有類加載器的路徑里面,因此我們有必要自己定義類加載器。
五、自定義類加載器
除了根類加載器,所有類加載器都是ClassLoader的子類。所以我們可以通過繼承ClassLoader來實(shí)現(xiàn)自己的類加載器。
ClassLoader類有兩個關(guān)鍵的方法:
- protected Class loadClass(String name, boolean resolve):name為類名,resove如果為true,在加載時解析該類。
- protected Class findClass(String name) :根據(jù)指定類名來查找類。
所以,如果要實(shí)現(xiàn)自定義類,可以重寫這兩個方法來實(shí)現(xiàn)。但推薦重寫findClass方法,而不是重寫loadClass方法,重寫loadClass方法可能會破壞類加載的雙親委派模型,因?yàn)閘oadClass方法內(nèi)部會調(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方法即可簡單方便的實(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("加密錯誤", 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方法,從加密類信息中獲取到對應(yīng)的.class文件信息,最后獲取到加密類的對象
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),我們可以通過在管理平臺添加編譯好的.class文件,最后通過自定義的類加載器和反射調(diào)用方法,來實(shí)現(xiàn)具體方法的調(diào)用,避免了我們需要修改代碼和重新發(fā)版來適應(yīng)不斷新增加密方法的問題。
七、問題
上面的代碼在本地測試時,沒有出現(xiàn)任何異常,但是部署到測試服務(wù)器以后出現(xiàn)了JSON解析異常,看上去貌似是json字符串的格式不對。
json解析邏輯主要存在于DynamicClassLoader#findClass方法入口處的將字符串轉(zhuǎn)換為對象邏輯,為什么這里會報錯,我們在入口處打印了入?yún)ⅰ?/p>
發(fā)現(xiàn)這里除了我們需要的正確的入?yún)?第一個入?yún)⑿畔⒋蛴?外,還多了一個Base64的全路徑名cn.hutool.core.codec.Base64。出現(xiàn)這種情況,說明由于我們重寫了ClassLoader的findClass方法,而Base64加載的時候會調(diào)用原始的ClassLoader類的loadClass方法去加載,并且里面調(diào)用了findClass方法,由于findClass已經(jīng)被重寫,所以就會報上面的json解析錯誤。
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)行以外,不希望其他的類用自定義類加載器加載,通過對ClassLoader#loadClass方法分析,那么我們就希望能否通過其父類加載器加載到Base64這個三方類。因?yàn)閱宇惣虞d器Bootstrap Class Loader肯定不能加載到Base64,所以我們需要顯示的設(shè)置父類加載器,但是這個父類加載器究竟設(shè)置為哪一個類加載器,那么就需要我們了解Tomcat類加載器結(jié)構(gòu)。
為什么Tomcat需要在JVM基礎(chǔ)之上做一套類加載結(jié)構(gòu),主要是為了解決如下問題:
- 部署在同一個服務(wù)器上的兩個web應(yīng)用程序所使用的Java類庫可以實(shí)現(xiàn)相互隔離;
- 部署在同一個服務(wù)器上的兩個web應(yīng)用程序所使用的Java類庫可以實(shí)現(xiàn)共享;
- 服務(wù)器需要盡可能保證自身安全,服務(wù)器所使用的類庫應(yīng)該與應(yīng)用程序的類庫相互獨(dú)立;
- 支持JSP應(yīng)用的Web服務(wù)器,大對數(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加載的。
我們可以寫一個測試類測試一下:
@Slf4j @Component public class Test implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { log.info("classLoader DynamicClassLoader:" + DynamicClassLoader.class.getClassLoader().toString()); } }
測試結(jié)果:
所以我們可以設(shè)置自定義類加載器DynamicClassLoader的父加載器為加載其本身的類加載器:
public DynamicClassLoader() { super(DynamicClassLoader.class.getClassLoader()); }
我們再次執(zhí)行文件的加解密操作時,已經(jīng)沒有發(fā)現(xiàn)報錯,并且通過添加日志,我們可以看到加載類cn.hutool.core.codec.Base64對應(yīng)的類加載器確實(shí)為加載DynamicClassLoader對應(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會調(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)初始時加載cn.hutool.core.codec.Base64的類加載器為DynamicClassLoader。然后委托給父類加載器AppClassLoader進(jìn)行加載,根據(jù)雙親委派原理,后續(xù)會交由AppClassLoader自己進(jìn)行處理。但是在用戶路徑下仍然沒有找到類cn.hutool.core.codec.Base64,最終交由DynamicClassLoader進(jìn)行加載,最終出現(xiàn)了最開始的JSON解析錯誤。
八、總結(jié)
由于類加載階段沒有嚴(yán)格限制如何獲取一個類的二進(jìn)制字節(jié)流,因此給我們提供一個通過自定義類加載器來動態(tài)加載.class文件實(shí)現(xiàn)代碼可擴(kuò)展性的可能。通過靈活自定義classloader,也可以在其他領(lǐng)域發(fā)揮重要作用,例如實(shí)現(xiàn)代碼加密來避免核心代碼泄漏、解決不同服務(wù)依賴同一個包的不同版本所引起的沖突問題以及實(shí)現(xiàn)程序熱部署來避免調(diào)試時頻繁重啟應(yīng)用。
到此這篇關(guān)于JVM自定義類加載器在代碼擴(kuò)展性實(shí)踐分享的文章就介紹到這了,更多相關(guān)JVM加載器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Cloud?Gateway編碼實(shí)現(xiàn)任意地址跳轉(zhuǎn)的示例
本文主要介紹了Spring?Cloud?Gateway編碼實(shí)現(xiàn)任意地址跳轉(zhuǎn)的示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12命令行中 javac、java、javap 的使用小結(jié)
使用 java 命令運(yùn)行一個.class文件,需要使用該類的全限定類名,同時需要在當(dāng)前路徑下有該類的包層次文件夾,這篇文章主要介紹了命令行中 javac、java、javap 的使用小結(jié),需要的朋友可以參考下2023-07-07Java多線程實(shí)現(xiàn)Callable接口
本文給大家分享的是使用Java多線程來實(shí)現(xiàn)callable接口的方法,以及使用方法,另外還有一個網(wǎng)友的實(shí)例,希望能夠?qū)Υ蠹艺莆認(rèn)ava多線程有所幫助。2016-06-06springboot實(shí)現(xiàn)圖片大小壓縮功能
這篇文章主要為大家詳細(xì)介紹了springboot實(shí)現(xiàn)圖片大小壓縮功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04一篇文章帶你入門java算術(shù)運(yùn)算符(加減乘除余,字符連接)
這篇文章主要介紹了Java基本數(shù)據(jù)類型和運(yùn)算符,結(jié)合實(shí)例形式詳細(xì)分析了java基本數(shù)據(jù)類型、數(shù)據(jù)類型轉(zhuǎn)換、算術(shù)運(yùn)算符、邏輯運(yùn)算符等相關(guān)原理與操作技巧,需要的朋友可以參考下2021-08-08Mybatis之映射實(shí)體類中不區(qū)分大小寫的解決
這篇文章主要介紹了Mybatis之映射實(shí)體類中不區(qū)分大小寫的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11在Java中避免NullPointerException的解決方案
這篇文章主要介紹了在Java中避免NullPointerException的解決方案,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04spring 中事務(wù)注解@Transactional與trycatch的使用
這篇文章主要介紹了spring 中事務(wù)注解@Transactional與trycatch的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06