记一次类加载器的简单应用

栏目: Java · 发布时间: 6年前

内容简介:记一次类加载器的简单应用

jvm和 java 语言是两种产品,java代码编译后生成字节码bytecode(.class文件),jvm解释字节码转换为机器码并真正执行,字节码和虚拟机之间的桥梁就是java开发中常见的类加载器,实现从外部来加载某个类的字节码并传递给虚拟机。

类加载器主要有启动类加载器(BootClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)以及自定义类加载器(CustomClassLoader,视应用实现有无)四类,类加载器加载类的方式为双亲委托模式,默认的加载流程可以简单表述为:

  1. findLoadedClass:检查class是否已经被加载过,已经加载过直接返回

  2. 检查classloader的parent:尝试从parent加载

  3. 如果parent为空:尝试从BootClassLoader加载

  4. 如果还是没有找到:通过当前classloader加载

类加载的代码可以在java.lang.ClassLoader.loadClass方法中找到,简单画个图,单个classloader内部的加载流程:

记一次类加载器的简单应用

假定CustomClassLoader指定了AppClassLoader为双亲(parent classloader),整个加载类控制流的流程图可以简单画作:

记一次类加载器的简单应用

其中:

  • BootClassLoader默认加载核心类(jre目录下的lib/*.jar),可以通过-Xbootclasspath追加其他路径,会让指定路径下的class优先被找到;

  • ExtClassLoader加载扩展类,jre目录下的lib/ext/*.jar;

  • AppClassLoader加载应用程序需要的类库,通过-cp传入,或在启动目录下 ".",也可以直接打成一个fat jar;

  • CustomClassLoader主要是用户自定义的实现。

需要注意的一点是,类加载器会通过parent来确认是否需要加载类,但是不会向下通过children来确认,因此高优先级classloader比如BootClassLoader中的类如果要加载AppClassLoader中的类,就需要通过ContextClassLoader来执行,因为ClassLoader不会向下请求,只能单向委托双亲加载,ContextClassLoader可以通过当前工作线程的上下文来传递。

记一次类加载器的简单应用

图中虚线箭头表示的类加载方式就是通过context classloader作为中转媒介,当然也可以通过实现自定义的classloader,覆盖loadClass方法来修改加载类文件的控制流。

字节码存在的位置可以是一个与jvm运行在同一操作系统的本地路径,也可以是一个通过网络访问的远端存储,JDK专门提供了URLClassLoader之类的加载器来实现通过网络加载远程bytecode的方法,本地加载的话就可以直接通过classpath告诉系统加载器来加载,本地其实是逻辑上的本地路径,也可以通过操作系统挂载远程文件夹来模拟本地加载远程文件。

背景介绍到这里,接下来解决一个实际问题,因为历史原因,我们的平台系统同时存在高低版本的ElasticSearch(1.3.2/5.1.2,以下简称为Es),又不希望分开两套代码,不便维护,这里有三种解决方法:

  • 自定义classloader,又可以分成两种:

  1. 打包成一个大文件,类似spring boot的加载方式,将jar及其全部依赖打包成一个文件,然后通过不同的文件偏移量来load,一个文件数据段代表一个class;

  2. 从指定目录加载指定jar,不同版本的Es交互代码放在不同的工程模块,打包时将不同的模块打包到不同的文件夹,应用程序启动时通过不同的classloader加载不同文件夹下的class;

  • 通过maven shade plugin来将依赖包重命名,因为Es核心包又有其他依赖,也会导致类冲突,需要将Es核心包及其全部依赖都重命名。

  • 这里我们采用了比较低成本的方法,通过不同文件夹来隔离不兼容的Es核心包及其依赖,利用多个classloader之间加载的class不会冲突以及classloader不会向下请求的方法来实现正常加载高低版本Es及其依赖包, 主要的实现思路如下:

    1. 将高低版本Es交互隔离到不同的工程module

    2. 通过module的编译配置(maven assembly),编译时将其输出到target下的不同目录

    3. 配置主工程的assembly,通过文件依赖的方式将第2步的多个目录拷贝到应用程序的lib目录下(lib/ext/*.jar)

    4. 自定义classloader,通过环境变量传入各个Es的lib目录,拼接为不同的classpath

    5. 应用启动时通过多个自定义classloader加载多个目录下的类文件

    记一次类加载器的简单应用

    为了节省篇幅,这里只简要列出主要的实现代码:

    public void loadFiles() {
        // 通过自定义classloader加载高低版本
        String es5x;
        String es1x;
        String libPath = System.getProperty("lib.path");
        if (StringUtils.isNotBlank(libPath)) {
            es5x = libPath + "/esx5/";
            es1x = libPath + "/esx1/";
        } else {
            // 异常代码省略
        }
    
        try {
            MyClassLoader loader1 = new MyClassLoader(getClassPath(es1x), getClass().getClassLoader());
            MyClassLoader loader2 = new MyClassLoader(getClassPath(es5x), getClass().getClassLoader());
    
            // 用不同的classloader来加载es依赖
            Class esclientX1 = loader1.loadClass("com.youzan.platform.esclient.ESClientX1", true);
            Class esclientX5 = loader2.loadClass("com.youzan.platform.esclient.ESClientX5", true);
    
            // 初始化es client
            for (ESConn conn : config.getConnections()) {
                ESClient esClient;
                if (ESVersion.X5 == conn.getType()) {
                    esClient = (ESClient) esclientX5.newInstance();
                } else {
                    esClient = (ESClient) esclientX1.newInstance();
                }
                // 原方法初始化ESClient
                esClient.init(conn);
            }
        } catch (Exception e) {
            log.error("initialize es client failed with {}", ExceptionUtils.getStackTrace(e));
            // 异常处理省略
        }
    }
    private URL[] getClassPath(String dir) throws MalformedURLException {
        List<URI> jars = new LinkedList<>();
    
        // 拼接全部jar文件路径
        Collection<File> files = FileUtils.listFiles(new File(dir), FileFilterUtils.suffixFileFilter(".jar"), null);
        files.forEach((f) -> jars.add(f.toURI()));
    
        URL[] urls = new URL[jars.size()];
        for (int i = 0; i < jars.size(); i++) {
            urls[i] = jars.get(i).toURL();
        }
    
        return urls;
    }

    这里提一下实现过程中遇到的一个坑,Es1.x启动时需要指定context class loader,Es1.x的内部异常在实际处理时才会load,默认会用AppClassloader加载,而我们实际是通过一个继承自AppClassloader的自定义加载器加载的Es核心包,因为classloader不会向下请求,因此会报运行时异常,解决方法就是在传入Client的初始化参数时设置加载核心包的类加载器:

    Settings settings = ImmutableSettings.builder()
            // 设置上下文classloader,其他代码省略
            .classLoader(getClass().getClassLoader())
            .build();
    TransportClient client = new TransportClient(settings);
    

    Es5.x版本已经fix这个问题了。

    另外再提一句,一般实现自定义的classloader都是建议覆盖findClass方法,而不是直接覆盖loadClass方法,避免在不知情的情况下改变类加载的控制流,导致其不符合双亲委托模型,引发ClassNotFoundException或者ClassCastException,因为不同的classloader加载类在jvm看来并不是同一个,即使内部的代码实现甚至class文件都是同一个。

    本次问题分析及解决方法就到这里,在构思这篇文章的过程中,也想到了以前遇到的一个问题(错误将一个应用依赖包拷贝到了jre的ext lib目录下,导致应用程序的lib目录中的依赖一直加载失败),假设有多个团队引用了同一个公共包,想要升级这个包的时候就需要通知多个团队配合升级,如果想跳过这个费时的过程直接升级发布,也可以考虑类似的方法,通过更高优先级的classloader来加载公共包,只要保证这个目录下的包能够统一更新,升级问题就变得很省力了。

    后序:

    如果某种语言的编译器遵守虚拟机规范,编译后输出标准的字节码,那么用这个语言写出的应用程序代码可以通过jvm运行,这应该是java平台产品设计中最成功的点,使之具有相当的生态开放程度,目前运行于jvm之上的衍生语言(jvm language)已经有Scala/Clojure/Groovy/Kotlin等多种(https://en.wikipedia.org/wiki/List_of_JVM_languages)。

    最近略忙,有段时间没有更新了,准备最近重启写作,欢迎大家关注跳跳爸的公众号:

    记一次类加载器的简单应用


    以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

    查看所有标签

    猜你喜欢:

    本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

    SEO深度解析

    SEO深度解析

    痞子瑞 / 电子工业出版社 / 2014-3-1 / CNY 99.00

    《SEO深度解析》以SEO从业人员普遍存在的疑问、经常讨论的问题、容易被忽视的细节以及常见的错误理论为基础,对SEO行业所包含的各方面内容进行了深入的讨论,使读者更加清晰地了解SEO及操作思路。内容分为两类:一类为作者根据自己真实、丰富的SEO经验对SEO所涉及的各种问题进行详细的讨论,主要包括SEO 基础原理剖析、SEO实操思路方法、常用工具数据剖析、竞争对手分析案例实操、网站数据分析思路指导、......一起来看看 《SEO深度解析》 这本书的介绍吧!

    MD5 加密
    MD5 加密

    MD5 加密工具

    HEX HSV 转换工具
    HEX HSV 转换工具

    HEX HSV 互换工具