内容简介:记一次类加载器的简单应用
jvm和 java 语言是两种产品,java代码编译后生成字节码bytecode(.class文件),jvm解释字节码转换为机器码并真正执行,字节码和虚拟机之间的桥梁就是java开发中常见的类加载器,实现从外部来加载某个类的字节码并传递给虚拟机。
类加载器主要有启动类加载器(BootClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)以及自定义类加载器(CustomClassLoader,视应用实现有无)四类,类加载器加载类的方式为双亲委托模式,默认的加载流程可以简单表述为:
-
findLoadedClass:检查class是否已经被加载过,已经加载过直接返回
-
检查classloader的parent:尝试从parent加载
-
如果parent为空:尝试从BootClassLoader加载
-
如果还是没有找到:通过当前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,又可以分成两种:
-
打包成一个大文件,类似spring boot的加载方式,将jar及其全部依赖打包成一个文件,然后通过不同的文件偏移量来load,一个文件数据段代表一个class;
-
从指定目录加载指定jar,不同版本的Es交互代码放在不同的工程模块,打包时将不同的模块打包到不同的文件夹,应用程序启动时通过不同的classloader加载不同文件夹下的class;
通过maven shade plugin来将依赖包重命名,因为Es核心包又有其他依赖,也会导致类冲突,需要将Es核心包及其全部依赖都重命名。
这里我们采用了比较低成本的方法,通过不同文件夹来隔离不兼容的Es核心包及其依赖,利用多个classloader之间加载的class不会冲突以及classloader不会向下请求的方法来实现正常加载高低版本Es及其依赖包, 主要的实现思路如下:
-
将高低版本Es交互隔离到不同的工程module
-
通过module的编译配置(maven assembly),编译时将其输出到target下的不同目录
-
配置主工程的assembly,通过文件依赖的方式将第2步的多个目录拷贝到应用程序的lib目录下(lib/ext/*.jar)
-
自定义classloader,通过环境变量传入各个Es的lib目录,拼接为不同的classpath
-
应用启动时通过多个自定义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)。
最近略忙,有段时间没有更新了,准备最近重启写作,欢迎大家关注跳跳爸的公众号:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 如何优雅重加载 Web 应用
- Apache重新加载WSGI应用
- Tomcat 应用中并行流带来的类加载问题
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
- Vue.js应用性能优化:第三部分-延迟加载Vuex模块
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
MD5 加密
MD5 加密工具
HEX HSV 转换工具
HEX HSV 互换工具