Java通過自定義類加載器實現類隔離
前言
由於微服務的快速迭代、持續集成等特性,越來越多的團隊更傾向於它。但是也體現出瞭一些問題,比如在基礎設施建設過程中,需要把通用功能下沉,把現有大而全的基礎設施按領域拆分,考慮需要兼容現有生產服務,會產生不同的依賴版本,有時不註意就可以引發問題。比如本文遇到的依賴包版本沖突問題,以及如何利用類隔離技術解決的分析。
類隔離是什麼
類隔離是一種通過類加載器實現加載所需類的實現方式,使得不同版本類間隔離,避免瞭使用沖突問題,最終的效果就是不同模塊的內容被不同的類加載器加載,滿足同一環境下同時兼容不同接口實現類。
使用場景
比如業務服務A和業務服務B均需要消息通知等,均依賴消息中間件,但所引用版本不一致,導致最終隻有一個版本加載到JVM,在某一個服務調用時會出現 NoSuchMethodError或NoSuchClassError問題,這就很難排查出來,沒準會影響項目進度,最終月度的績效(“雞腿”)不保。
服務A pom.xml:
<!-- common-message--> <dependency> <groupId>com.lgy</groupId> <artifactId>spring-common-message</artifactId> <version>1.0.0<version> </dependency>
服務B pom.xml:
<!-- common-message--> <dependency> <groupId>com.lgy</groupId> <artifactId>spring-common-message</artifactId> <version>2.0.0<version> </dependency>
業務調用流程:
// 業務A調用微信服務通知 MessageUtil.sendMessage(content,peopleId,templateId,"wechat"); // 業務B調用微信服務通知 MessageUtil.sendToWechat(content,peopleId,templateId);
JVM最終加載的為 2.0.0 版本的依賴,導致業務A在調用時拋異常java.lang.NoSuchMethodError。
解決方案
大體的解決思路就是,在不改變業務代碼的前提下, 業務A調用 1.0.0 版本的消息工具類, 業務B調用2.0.0版本的消息工具類,因此需要JVM能夠利用自定義類加載器加載所需的類或關聯的類。
實現思路
重寫類加載器,實現自定義類加載(java.lang.ClassLoader)
重寫類加載函數
- 重寫 findClass(String name)
- 重寫 loadClass(String name)
涉及的知識點
- JVM加載過程:加載-》鏈接-》初始化(具體後續介紹)
- 雙親委派機制:委托父加載器查詢;如果父加載器查詢不到,則調用自身的findClass加載
重寫findClass
import java.io.*; import java.util.HashMap; import java.util.Map; public class CustomerFindClass extends ClassLoader { private Map<String, String> classPathMap = new HashMap<>(); public CustomerFindClass() { // 業務A的自定義類加載器 classPathMap.put("com.lgy.businessA.service.impl.MessageServiceImpl", "E:/dataway-demo/example/target/classes/com/lgy/businessA/service/impl/MessageServiceImpl.class"); classPathMap.put("com.lgy.v1.message.util.MessageUtil", "E:/dataway-demo/example/target/classes/com/lgy/v1/message/util/MessageUtil.class"); } /** * findClass方式加載類 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String classPath = classPathMap.get(name); File file = new File(classPath); if (!file.exists()) { throw new ClassNotFoundException(); } byte[] bytes = getClassData(file); if (null == bytes || 0 == bytes.length) { throw new ClassNotFoundException(); } return defineClass(bytes, 0, bytes.length); } private byte[] getClassData(File file) { try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[4096]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return new byte[]{}; }
最終結果與預期的結果不一致
- 預期結果:業務A的MessageServiceImpl與MessageUtil由CustomerFindClass加載
- 實際結果:業務A的MessageServiceImpl由CustomerFindClass加載,而MessageUtil由sun.misc.AppClassLoader加載。
- 分析:由於JVM類加載的雙親委托機制,業務A調用消息工具類時,類加載器(CustomerFindClass)會委托父類加載器(AppClassLoader)加載類,如果存在,則不再執行自身的findClass方法加載,導致結果不理想。(main 方法類默認情況下都是由 JDK 自帶的 AppClassLoader 加載的)。
重寫loadClass
private ClassLoader classLoader; /** * 重新loadClass方法 */ @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class result = null; try { //這裡要使用 JDK 的類加載器加載 java.lang 包裡面的類 result = classLoader.loadClass(name); } catch (Exception e) { // ignore error } if (null != result) { return result; } String classPath = classPathMap.get(name); File file = new File(classPath); if (!file.exists()) { throw new ClassNotFoundException(); } byte[] bytes = getClassData(file); if (null == bytes || 0 == bytes.length) { throw new ClassNotFoundException(); } return defineClass(bytes, 0, bytes.length); }
滿足業務A的MessageServiceImpl與MessageUtil由CustomerFindClass加載
註意:這種方式破壞瞭雙親委托機制,但由於重寫瞭loadClass方法,所有類均會有CustomerFindClass加載器加載,需要過濾出不需要隔離的類,如java.lang包下的類,需要由ExtClassLoader 來加載。
總結
本文分享的方式是從類加載器方向出發,實現最終的類隔離,避免瞭不同模塊間不同類的沖突,其中順便也簡單帶過瞭jvm類加載相關的知識點,也算是一勞多得,後續會結合實際使用場景進一步分析。
以上就是Java通過自定義類加載器實現類隔離的詳細內容,更多關於Java類隔離的資料請關註WalkonNet其它相關文章!