当项目越来越庞大复杂的时候,有时候需要动态引入第三方Jar包,这就导致我们可能会遇到Jar包冲突的问题,如果冲突的jar包是兼容的,程序还能正常执行,但是如果遇到不兼容的情况,那么不管选择哪个版本,都会出问题,导致各种各样的报错,例如 LinkageError, NoSuchMethodError 等.
功能模块化是实现系统能力高可扩展性的常见思路。而模块化又可分为静态模块化和动态模块化两类:
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包冲突的方法.
这里使用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)'
自定义类加载器并破坏双亲委派模型
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); System.out.println("findLoadedClass " +name+" --- "+c); if (c == null) { try { c = findClass(name); System.out.println("loaded from child, name=" + name); } catch (ClassNotFoundException e) { if (getParent() != null) { System.out.println("loaded from parent, name=" + name); c = super.loadClass(name, resolve); } else { System.out.println("loaded from system, name=" + name); c = findSystemClass(name); } } } if (resolve) { resolveClass(c); } return c; } } @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(); } }; } }
用于存储需要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; } }
用于切换线程上下文类加载器.因为有些类是使用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、反射、依赖隔离)