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其它相關文章!

推薦閱讀: