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