努力保头发的打工人
努力保头发的打工人
发布于 2023-07-25 / 205 阅读 / 0 评论 / 0 点赞

Java同时引用不同版本相同的jar包

前言

对接不同公司时,由于用到了bouncycastle进行签名,都是其他公司封装好的jar包或方法依赖了bouncycastle系列的包,由于各种原因必须同时使用不同版本的包。

直接引用会报类重复加载的问题,由于java类加载使用双亲委派模型,同一个包名类名必定是同一个类加载器加载。java是当类名包名相同且类加载器相同时认为是同一个类,想要同时使用不同版本jar包只能通过自定义类加载器,破坏双亲委派模型来实现。

一 、Java类加载的过程

Java代码从编码完成到运行,包含两个步骤:

  • 编译:把写好的java文件通过javac命令编译成字节码(.class文件)。

  • 运行:把字节码文件交给JVM执行。

类加载的过程就是JVM把.class文件中类信息加载进内存,并解析生成class对象的过程。这个过程主要为3步:加载、链接、初始化,而链接可以分为3小步:验证、准备、解析,每个过程主要过程如下:

  1. 加载:把各个来源的的class字节码文件通过不同类加载器载入内存。

  2. 验证:保证加载进来的字节流符合虚拟机规范,不会造成安全规范。验证包括对于**文件格式**的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?对于**元数据的验证**,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载;对于**字节码的验证**,保证程序语义的合理性,比如要保证类型转换的合理性。对于**符号引用的验证**,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?

  3. 准备:为变量分配内存,并且赋予初值,初值不是代码中的初始化的值而是根据不同变量设置默认值,其中引用类型为null。

  4. 解析:常量池内的符号引用替换为直接引用的过程。例如调用hello()方法,替换为方法的内存地址。

  5. 初始化:对static修饰的变量或语句进行初始化。

二、类加载器

1. BootstrapClassLoader:启动类加载器,负责加载jre/lib/re.jar中的所有class。

2. ExtensionClassLoader:标准扩展类加载器,负责加载jre/lib/ext中的所有class。

3. AppClassLoader:系统类加载器,负责加载classpath中指定的jar包和目录中的class。

4. CustomClassLoader:自定义类加载器。

三、双亲委派模型

双亲并没有特殊的含义,只是层级关系的称呼方式(女拳警告!)层级关系并不是继承的关系,而是组合,每个类加载器都有parent字段来定义上级。

java为了保证类只加载一次,类加载器加载类时,首先检查是否加载过,查不到就委派给上级,直到顶级的类加载器没有查到就加载。当加载的时候该顶级类会尝试加载,如果加载不了再交给下级加载。这样可以保证每个类在类加载器中只会加载一次。

下面是类加载器接口ClassLoader

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) {

            long t0 = System.nanoTime();

            try {

                if (parent != null) {

                    c = parent.loadClass(name, false); //没有加载过而且有上级就委派给上级

                } else {

                    c = findBootstrapClassOrNull(name); //没有上级就交给顶级类加载器

                }

            } catch (ClassNotFoundException e) {

                // ClassNotFoundException thrown if class not found

                // from the non-null parent class loader

            }

            if (c == null) {

                // If still not found, then invoke findClass in order

                // to find the class.

                long t1 = System.nanoTime();

                c = findClass(name);

                // this is the defining class loader; record the stats

                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

                sun.misc.PerfCounter.getFindClasses().increment();

            }

        }

        if (resolve) {

            resolveClass(c);

        }

        return c;

    }

}

自定义类加载器

自定义类加载器参考:https://blog.csdn.net/u011943534/article/details/89204709

package cn.gq.jdsk;

import java.io.*;

import java.net.URL;

import java.net.URLClassLoader;

import java.util.ArrayList;

import java.util.List;

import java.util.jar.JarEntry;

import java.util.jar.JarFile;

/**

 * @author chiangtaol

 * @date 2021-10-19

 * @describe

 */

/**

 * 提供Jar隔离的加载机制,会把传入的路径、及其子路径、以及路径中的jar文件加入到class path。

 * 破坏双亲委派机制,改为逆向

 /

public class JarLoader extends URLClassLoader {

    private static ThreadLocal<URL[]> threadLocal = new ThreadLocal<>();

    private URL[] allUrl;

    public JarLoader(String[] paths) {

        this(paths, JarLoader.class.getClassLoader());

    }

    public JarLoader(String[] paths, ClassLoader parent) {

        super(getURLs(paths), parent);

        //暂时先这样

        allUrl = threadLocal.get();

    }

    private static URL[] getURLs(String[] paths) {

        if (null  paths || 0  paths.length) {

            throw new RuntimeException("jar包路径不能为空.");

        }

        List<String> dirs = new ArrayList<String>();

        for (String path : paths) {

            dirs.add(path);

            JarLoader.collectDirs(path, dirs);

        }

        List<URL> urls = new ArrayList<URL>();

        for (String path : dirs) {

            urls.addAll(doGetURLs(path));

        }

        URL[] urls1 = urls.toArray(new URL[0]);

        threadLocal.set(urls1);

        return urls1;

    }

    private static void collectDirs(String path, List<String> collector) {

        if (null == path || "".equalsIgnoreCase(path)) {

            return;

        }

        File current = new File(path);

        if (!current.exists() || !current.isDirectory()) {

            return;

        }

        for (File child : current.listFiles()) {

            if (!child.isDirectory()) {

                continue;

            }

            collector.add(child.getAbsolutePath());

            collectDirs(child.getAbsolutePath(), collector);

        }

    }

    private static List<URL> doGetURLs(final String path) {

        if (null == path || "".equalsIgnoreCase(path)) {

            throw new RuntimeException("jar包路径不能为空.");

        }

        File jarPath = new File(path);

        if (!jarPath.exists() || !jarPath.isDirectory()) {

            throw new RuntimeException("jar包路径必须存在且为目录.");

        }

        /* set filter */

        FileFilter jarFilter = new FileFilter() {

            @Override

            public boolean accept(File pathname) {

                return pathname.getName().endsWith(".jar");

            }

        };

        /* iterate all jar */

        File[] allJars = new File(path).listFiles(jarFilter);

        List<URL> jarURLs = new ArrayList<URL>(allJars.length);

        for (int i = 0; i < allJars.length; i++) {

            try {

                jarURLs.add(allJars[i].toURI().toURL());

            } catch (Exception e) {

                throw new RuntimeException("系统加载jar包出错", e);

            }

        }

        return jarURLs;

    }

    //破坏双亲委派模型,采用逆向双亲委派

    @Override

    public Class<?> loadClass(String name) throws ClassNotFoundException {

        if (allUrl != null) {

            String classPath = name.replace(".", "/");

            classPath = classPath.concat(".class");

            for (URL url : allUrl) {

                byte[] data = null;

                ByteArrayOutputStream baos = new ByteArrayOutputStream();

                InputStream is = null;

                try {

                    File file = new File(url.toURI());

                    if (file != null && file.exists()) {

                        JarFile jarFile = new JarFile(file);

                        if (jarFile != null) {

                            JarEntry jarEntry = jarFile.getJarEntry(classPath);

                            if (jarEntry != null) {

                                is = jarFile.getInputStream(jarEntry);

                                int c = 0;

                                while (-1 != (c = is.read())) {

                                    baos.write(c);

                                }

                                data = baos.toByteArray();

                                return this.defineClass(name, data, 0, data.length);

                            }

                        }

                    }

                } catch (Exception e) {

                    e.printStackTrace();

                } finally {

                    try {

                        if (is != null) {

                            is.close();

                        }

                        baos.close();

                    } catch (IOException e) {

                        e.printStackTrace();

                    }

                }

            }

        }

        return super.loadClass(name);

    }

}

package cn.gq.jdsk;

/**

 * @author chiangtaol

 * @date 2021-10-19

 * @describe

 */

/**

 *

 * 为避免jar冲突,比如hbase可能有多个版本的读写依赖jar包

 * 就需要脱离当前classLoader去加载这些jar包,执行完成后,又退回到原来classLoader上继续执行接下来的代码

 */

public final class ClassLoaderSwapper {

    private ClassLoader storeClassLoader = null;

    private ClassLoaderSwapper() {

    }

    public static ClassLoaderSwapper newCurrentThreadClassLoaderSwapper() {

        return new ClassLoaderSwapper();

    }

    /**

     * 保存当前classLoader,并将当前线程的classLoader设置为所给classLoader

     *

     * @param

     * @return

     */

    public ClassLoader setCurrentThreadClassLoader(ClassLoader classLoader) {

        this.storeClassLoader = Thread.currentThread().getContextClassLoader();

        Thread.currentThread().setContextClassLoader(classLoader);

        return this.storeClassLoader;

    }

    /**

     * 将当前线程的类加载器设置为保存的类加载

     * @return

     */

    public ClassLoader restoreCurrentThreadClassLoader() {

        ClassLoader classLoader = Thread.currentThread()

                .getContextClassLoader();

        Thread.currentThread().setContextClassLoader(this.storeClassLoader);

        return classLoader;

    }

}

测试代码如下

@Test

public void classloader() throws Exception{

    String jar1 = "/Users/chiangtaol/Downloads/test/jd/jar1"; //自己定义的测试jar包,不同版本打印内容不同

    String jar2 = "/Users/chiangtaol/Downloads/test/jd/jar2";

    JarLoader jarLoader = new JarLoader(new String[]{jar1});

    ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();

    classLoaderSwapper.setCurrentThreadClassLoader(jarLoader);

    Class<?> aClass = Thread.currentThread().getContextClassLoader().loadClass("cn.tnt.bean.TestClass");

    classLoaderSwapper.restoreCurrentThreadClassLoader();

    Object o = aClass.newInstance();

    Method isEmptyMethod = aClass.getDeclaredMethod("hello");

    Object invoke = isEmptyMethod.invoke(o);

    System.out.println(invoke);

    JarLoader jarLoader2 = new JarLoader(new String[]{jar2});

    ClassLoaderSwapper classLoaderSwapper2 = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();

    classLoaderSwapper2.setCurrentThreadClassLoader(jarLoader2);

    Class<?> aClass2 = Thread.currentThread().getContextClassLoader().loadClass("cn.tnt.bean.TestClass");

    classLoaderSwapper.restoreCurrentThreadClassLoader();

    Object o2 = aClass2.newInstance();

    Method isEmptyMethod2 = aClass2.getDeclaredMethod("hello");

    Object invoke2 = isEmptyMethod2.invoke(o2);

    System.out.println(invoke2);

}
/**

 * @author chiangtaol

 * @date 2021-10-20

 * @describe

 */

public class TestClass {

    public String hello(){

        return SoutUtil.sout();

    }

}

//为了测试引用是否会加载正确,分为两个类打印

/**

 * @author chiangtaol

 * @date 2021-10-20

 * @describe

 */

public class SoutUtil {

    public static String sout(){

        System.out.println("这是jar包33333");

        return "333";

    }

}

结果如下:

这是jar包1111111

111

这是jar包22222

222

自定义的类主要针对必须同时引用两个不同版本的同名jar包时的问题解决思路。