類(lèi)隔離自定義類(lèi)加載器實(shí)現(xiàn),你學(xué)會(huì)了嗎?
前言
由于微服務(wù)的快速迭代、持續(xù)集成等特性,越來(lái)越多的團(tuán)隊(duì)更傾向于它。但是也體現(xiàn)出了一些問(wèn)題,比如在基礎(chǔ)設(shè)施建設(shè)過(guò)程中,需要把通用功能下沉,把現(xiàn)有大而全的基礎(chǔ)設(shè)施按領(lǐng)域拆分,考慮需要兼容現(xiàn)有生產(chǎn)服務(wù),會(huì)產(chǎn)生不同的依賴(lài)版本,有時(shí)不注意就可以引發(fā)問(wèn)題。比如本文遇到的依賴(lài)包版本沖突問(wèn)題,以及如何利用類(lèi)隔離技術(shù)解決的分析。
類(lèi)隔離是什么?
類(lèi)隔離是一種通過(guò)類(lèi)加載器實(shí)現(xiàn)加載所需類(lèi)的實(shí)現(xiàn)方式,使得不同版本類(lèi)間隔離,避免了使用沖突問(wèn)題,最終的效果就是不同模塊的內(nèi)容被不同的類(lèi)加載器加載,滿(mǎn)足同一環(huán)境下同時(shí)兼容不同接口實(shí)現(xiàn)類(lèi)。
使用場(chǎng)景
比如業(yè)務(wù)服務(wù)A和業(yè)務(wù)服務(wù)B均需要消息通知等,均依賴(lài)消息中間件,但所引用版本不一致,導(dǎo)致最終只有一個(gè)版本加載到JVM,在某一個(gè)服務(wù)調(diào)用時(shí)會(huì)出現(xiàn) NoSuchMethodError或NoSuchClassError問(wèn)題,這就很難排查出來(lái),沒(méi)準(zhǔn)會(huì)影響項(xiàng)目進(jìn)度,最終月度的績(jī)效(“雞腿”)不保。
服務(wù)A pom.xml:
<!-- common-message-->
<dependency>
<groupId>com.lgy</groupId>
<artifactId>spring-common-message</artifactId>
<version>1.0.0<version>
</dependency>
服務(wù)B pom.xml:
<!-- common-message-->
<dependency>
<groupId>com.lgy</groupId>
<artifactId>spring-common-message</artifactId>
<version>2.0.0<version>
</dependency>
業(yè)務(wù)調(diào)用流程:
// 業(yè)務(wù)A調(diào)用微信服務(wù)通知
MessageUtil.sendMessage(content,peopleId,templateId,"wechat");
// 業(yè)務(wù)B調(diào)用微信服務(wù)通知
MessageUtil.sendToWechat(content,peopleId,templateId);
JVM最終加載的為 2.0.0 版本的依賴(lài),導(dǎo)致業(yè)務(wù)A在調(diào)用時(shí)拋異常java.lang.NoSuchMethodError。
解決方案
大體的解決思路就是,在不改變業(yè)務(wù)代碼的前提下, 業(yè)務(wù)A調(diào)用 1.0.0 版本的消息工具類(lèi), 業(yè)務(wù)B調(diào)用2.0.0版本的消息工具類(lèi),因此需要JVM能夠利用自定義類(lèi)加載器加載所需的類(lèi)或關(guān)聯(lián)的類(lèi)。
實(shí)現(xiàn)思路
- 重寫(xiě)類(lèi)加載器,實(shí)現(xiàn)自定義類(lèi)加載(java.lang.ClassLoader)
- 重寫(xiě)類(lèi)加載函數(shù)
重寫(xiě) findClass(String name)
重寫(xiě) loadClass(String name)
涉及的知識(shí)點(diǎn)
- JVM加載過(guò)程:加載-》鏈接-》初始化(具體后續(xù)介紹)
- 雙親委派機(jī)制:委托父加載器查詢(xún);如果父加載器查詢(xún)不到,則調(diào)用自身的findClass加載
重寫(xiě)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() {
// 業(yè)務(wù)A的自定義類(lèi)加載器
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方式加載類(lèi)
*/
@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[]{};
}
最終結(jié)果與預(yù)期的結(jié)果不一致:
- 預(yù)期結(jié)果:業(yè)務(wù)A的MessageServiceImpl與MessageUtil由CustomerFindClass加載
- 實(shí)際結(jié)果:業(yè)務(wù)A的MessageServiceImpl由CustomerFindClass加載,而MessageUtil由sun.misc.AppClassLoader加載。
- 分析:由于JVM類(lèi)加載的雙親委托機(jī)制,業(yè)務(wù)A調(diào)用消息工具類(lèi)時(shí),類(lèi)加載器(CustomerFindClass)會(huì)委托父類(lèi)加載器(AppClassLoader)加載類(lèi),如果存在,則不再執(zhí)行自身的findClass方法加載,導(dǎo)致結(jié)果不理想。(main 方法類(lèi)默認(rèn)情況下都是由 JDK 自帶的 AppClassLoader 加載的)。
重寫(xiě)loadClass
private ClassLoader classLoader;
/**
* 重新loadClass方法
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class result = null;
try {
//這里要使用 JDK 的類(lèi)加載器加載 java.lang 包里面的類(lèi)
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);
}
滿(mǎn)足業(yè)務(wù)A的MessageServiceImpl與MessageUtil由CustomerFindClass加載
注意:這種方式破壞了雙親委托機(jī)制,但由于重寫(xiě)了loadClass方法,所有類(lèi)均會(huì)有CustomerFindClass加載器加載,需要過(guò)濾出不需要隔離的類(lèi),如java.lang包下的類(lèi),需要由ExtClassLoader 來(lái)加載。
總結(jié)
本文分享的方式是從類(lèi)加載器方向出發(fā),實(shí)現(xiàn)最終的類(lèi)隔離,避免了不同模塊間不同類(lèi)的沖突,其中順便也簡(jiǎn)單帶過(guò)了jvm類(lèi)加載相關(guān)的知識(shí)點(diǎn),也算是一勞多得。