背景
當項目越來越龐大復雜的時候,有時候需要動態引入第三方Jar包,這就導致我們可能會遇到Jar包沖突的問題,如果沖突的jar包是兼容的,程序還能正常執行,但是如果遇到不兼容的情況,那么不管選擇哪個版本,都會出問題,導致各種各樣的報錯,例如 LinkageError, NoSuchMethodError 等.
Jar包模塊加載方式
功能模塊化是實現系統能力高可擴展性的常見思路。而模塊化又可分為靜態模塊化和動態模塊化兩類:
- 靜態模塊化:指在編譯期可以通過引入新的模塊擴展系統能力。比如:通過Maven/Gradle引入一個依賴(本質是一組jar文件)。
- 動態模塊化:指在JVM運行期可以通過引入新的模塊擴展系統能力。比如:利用OSGI系統引入某個bundle(本質是一個jar文件),或者自己利用JDK提供的能力,將某個jar文件中的能力動態加載到運行時環境中。
- 動態模塊化主要是用於插件化擴展,例如開發者如果想擴展Solr某一項功能,只需要繼承Solr提供的分詞接口添加自己的實現,然后把自己的分詞jar包拷貝到Solr指定目錄,並在solr配置文件中配置,重啟即可生效。本篇文章主要介紹動態加載外部Jar包並動態解決Jar包沖突的問題.
如何產生Jar包沖突
Jar Hell問題引起的原因是當某個ClassLoader的Jar搜索路徑中的兩個Jar包里存在相同完全限定名的類時,ClassLoader只會從其中一個Jar包中加載該類。其不同版本的實現也使用的是相同的完全限定名。當這些完全限定名相同,但實現不同的Class所在的Jar包被作為第三方依賴同時引入到某個類加載器的Jar搜索路徑下時(比如AppClassLoader的搜索路徑為ClassPath),依賴沖突就產生了,而且難以解決。例如下圖,一個項目引入了外部兩個Jar包,A 和 B,但是 A 需要依賴版本號為 0.1 的 C 包,而恰好 B 需要依賴版本號為 0.2 的 C 包,且 C 包的這兩個版本無法兼容:
如何解決Jar包沖突
關於類加載機制以及類加載器的相關知識這里不再贅述,已經有很多大神幫忙總結了,這里重點介紹目前市面上主流解決動態加載Jar包沖突的方法.
- 利用類似於OSGI這樣的重框架來解決這類問題,但是這類框架太重太復雜,難以掌握,並且會加重項目的復雜度.
- 利用螞蟻金服公司開源貢獻的SOFAArk,基於 Java 實現的輕量級類隔離容器.
- 自定義類加載器來實現類隔離,例如Tomcat和Flink的實現,自定義類加載器並打破了雙親委派模型.因為Java虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。所以會出現相同類名的實例共存的情況.便達到了類相互隔離的作用.
動態加載Jar包流程
- 自定義類加載器.
- 將jar文件加載到內存中.
- 自定義ClassLoader將jar文件中的類加載到JVM.
- 通過反射獲取到需要調用的類並進行實例化.
- 通過反射獲取方法入參對象.
- 通過類的實例對象就可以調用這個Jar中的方法
解決問題的思路:
- 通過自定義類加載器加載依賴了不兼容的jar及其他依賴的jar,用不同的類加載器實例加載相關的類並創建對象.
- 打破類加載器的雙親委派機制,單獨創建出的類加載器要優先自己加載,加載不到則再委派給Parent類加載器進行加載.
- 通過動態監聽的方式監聽Jar是否被替換從而達到熱插拔的效果,這里不做實現,具體可參考Tomcat更新Jsp的方式,每當監聽到Jsp文件被修改,便重新加載該Jsp文件.
解決問題
第一步-編寫動態引入的Jar包
這里使用com.google.guava來模擬Jar包沖突,使用的版本分別為10.0和20.0,其中20.0版本有com.google.common.base.Strings#commonPrefix方法,用於求兩個字符串公共前綴,看下圖可知是在guava 11.0版本才引入的。也就是說使用10.0版本調用會報出NoSuchMethodError 異常。
可以直接寫一個簡單的test方法用於測試。
public String test() {
return Strings.commonPrefix("test123456","test789");
}
然后打出Jar包,名字為1.0-SNAPSHOT-all.jar.
第二步-模擬主程序
這里主程序已經加載了guava 10.0版本的包,里面是不存在Strings#commonPrefix方法的。所以直接使用反射加載Jar包並調用方法。
private static void load() throws NoSuchMethodException, MalformedURLException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
File file1 = new File("F:\\develop\\workspace\\1.0-SNAPSHOT-all.jar");
URLClassLoader classloader = new URLClassLoader(new URL[]{file1.toURI().toURL()});
Object o = Class.forName("com.MyTest", true, classloader).newInstance();
Method method = o.getClass().getMethod("test");
Object invoke = method.invoke(o);
System.out.println(invoke);
}
執行后,與預期一樣報錯
Caused by: java.lang.NoSuchMethodError: 'java.lang.String com.google.common.base.Strings.commonPrefix(java.lang.CharSequence, java.lang.CharSequence)'
第三步-自定義類加載器
ChildFirstClassLoader.java
自定義類加載器並破壞雙親委派模型
public class ChildFirstClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
protected ChildFirstClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
/**
* 重寫loadClass方法,部分類加載破壞雙親委派模型,(優先加載子類)。
*
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
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) {
if (resolve) {
resolveClass(c);
}
return c;
}
try {
c = findClass(name);
if (c != null) {
System.out.println("loaded from child, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// Ignore
}
try {
if (getParent() != null) {
c = super.loadClass(name, resolve);
if (c != null) {
System.out.println("loaded from parent, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
}
} catch (ClassNotFoundException e) {
// Ignore
}
try {
c = findSystemClass(name);
if (c != null) {
System.out.println("loaded from system, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// Ignore
}
throw new ClassNotFoundException(name);
}
}
@Override
public URL getResource(String name) {
// first, try and find it via the URLClassloader
URL urlClassLoaderResource = findResource(name);
if (urlClassLoaderResource != null) {
return urlClassLoaderResource;
}
// delegate to super
return super.getResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// first get resources from URLClassloader
Enumeration<URL> urlClassLoaderResources = findResources(name);
final List<URL> result = new ArrayList<>();
while (urlClassLoaderResources.hasMoreElements()) {
result.add(urlClassLoaderResources.nextElement());
}
// get parent urls
Enumeration<URL> parentResources = getParent().getResources(name);
while (parentResources.hasMoreElements()) {
result.add(parentResources.nextElement());
}
return new Enumeration<URL>() {
Iterator<URL> iter = result.iterator();
public boolean hasMoreElements() {
return iter.hasNext();
}
public URL nextElement() {
return iter.next();
}
};
}
}
ClassContainer.java
用於存儲需要ChildFirstClassLoader加載的jar包
public class ClassContainer {
private ChildFirstClassLoader childFirstClassLoader;
public ClassContainer() {
}
public ClassContainer(ClassLoader classLoader, String jarPath) {
if (jarPath == null || jarPath.length() == 0) {
return;
}
final URL[] urls = new URL[1];
try {
urls[0] = new File(jarPath).toURI().toURL();
this.childFirstClassLoader = new ChildFirstClassLoader(urls, classLoader);
} catch (MalformedURLException e) {
throw new DelegateCreateException("can not create classloader delegate", e);
}
}
public Class<?> getClass(String name) throws ClassNotFoundException {
return childFirstClassLoader.loadClass(name);
}
public ClassLoader getClassLoader () {
return childFirstClassLoader;
}
}
ThreadContextClassLoaderSwapper.java
用於切換線程上下文類加載器.因為有些類是使用Thread.currentThread().getContextClassLoader()類加載器來加載,例如java.sql包下的JDBC相關代碼,會使用線程上下文類加載器去加載實際的JDBC驅動中的代碼.
public class ThreadContextClassLoaderSwapper {
private static final ThreadLocal<ClassLoader> classLoader = new ThreadLocal<>();
// 替換線程上下文類加載器會指定的類加載器,並備份當前的線程上下文類加載器
public static void replace(ClassLoader newClassLoader) {
System.out.println("newClassLoader "+newClassLoader);
System.out.println("Thread.currentThread().getContextClassLoader() "+Thread.currentThread().getContextClassLoader());
classLoader.set(Thread.currentThread().getContextClassLoader());
Thread.currentThread().setContextClassLoader(newClassLoader);
}
// 還原線程上下文類加載器
public static void restore() {
if (classLoader.get() == null) {
return;
}
Thread.currentThread().setContextClassLoader(classLoader.get());
classLoader.set(null);
}
}
第四步用自定義類加載器調用
private void childFirstClassLoader() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InterruptedException {
ClassContainer container = new ClassContainer(getClass().getClassLoader(), "F:\\develop\\workspace\\1.0-SNAPSHOT-all.jar");
ThreadContextClassLoaderSwapper.replace(container.getClassLoader());
Object o = container.getClass("com.MyTest").newInstance();
Method method = o.getClass().getMethod("test");
Object invoke = method.invoke(o);
ThreadContextClassLoaderSwapper.restore();
System.out.println(invoke);
}
運行得出正確結果,而且通過打印的系統日志可以看出自定義的類和依賴的類是由自定義類加載器加載的,做到了類隔離。
總結
類隔離技術是為了解決依賴沖突而誕生的,它通過自定義類加載器破壞雙親委派機制,然后利用類加載傳導規則實現了不同模塊的類隔離。
參考
如何實現Java類隔離加載?
自定義child-first類加載器解決Jar包沖突
利用類加載器解決不兼容的Jar包共存的問題
Java進階知識點8:高可擴展架構的利器 - 動態模塊加載核心技術(ClassLoader、反射、依賴隔離)