简介
在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现 方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。 对于某些大型的应用来说,每次的重启都需要花费大量的时间成本。虽然 osgi 架构的出现,让模块重启 成为可能,但是如果模块之间有调用关系的话,这样的操作依然会让应用出现短暂的功能性休克。本文将 探索如何在不破坏 Java 虚拟机现有行为的前提下,实现某个单一类的热部署,让系统无需重启就完成某 个类的更新。
类加载的探索
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可 以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换 编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修 改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是 创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。本 文将具体探索如何实现这个方案。首先需要了解一下 Java 虚拟机现有的加载机制。目前的加载机制,称 为双亲委派,系统在使用一个 classloader 来加载类时,会先询问当前 classloader 的父类是否有能力 加载,如果父类无法实现加载操作,才会将任务下放到该 classloader 来加载。这种自上而下的加载方 式的好处是,让每个 classloader 执行自己的加载任务,不会重复加载类。但是这种方式却使加载顺序 非常难改变,让自定义 classloader 抢先加载需要监听改变的类成为了一个难题。
不过我们可以 换一个思路,虽然无法抢先加载该类,但是仍然可以用自定义 classloader 创建一个功能相同的类,让 每次实例化的对象都指向这个新的类。当这个类的 class 文件发生改变的时候,再次创建一个更新的类 ,之后如果系统再次发出实例化请求,创建的对象讲指向这个全新的类。
下面来简单列举一下需 要做的工作。
创建自定义的 classloader,加载需要监听改变的类,在 class 文件发生改变的时 候,重新加载该类。
改变创建对象的行为,使他们在创建时使用自定义 classloader 加载的 class。
自定义加载器的实现
自定义加载器仍然需要执行类加载的功能。这里却存在一个 问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不 会变,而 classloader 只能在虚拟机停止前销毁已经加载的类,这样 classloader 就无法加载更新后的 类了。这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的 class,比如加载 Test.class 时,保存在内存中的类是 Test_v1.class,当类发生改变时,重新加载的类名是 Test_v2.class。但是真 正执行加载 class 文件创建 class 的 defineClass 方法是一个 native 的方法,修改起来又变得很困 难。所以面前还剩一条路,那就是直接修改编译生成的 class 文件。
利用 ASM 修改 class 文件
可以修改字节码的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先来介绍一下 class 文 件的结构,class 文件包含了以下几类信息,一个是类的基本信息,包含了访问权限信息,类名信息,父 类信息,接口信息。第二个是类的变量信息。第三个是方法的信息。ASM 会先加载一个 class 文件,然 后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新 的 class。
首先看一下如何利用 ASM 修改类信息。
清单 1. 利用 ASM 修改字节码
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassReader cr = null; String enhancedClassName = classSource.getEnhancedName(); try { cr = new ClassReader(new FileInputStream( classSource.getFile())); } catch (IOException e) { e.printStackTrace(); return null; } ClassVisitor cv = new EnhancedModifier(cw, className.replace(".", "/"), enhancedClassName.replace(".", "/")); cr.accept(cv, 0);
ASM 修改字节码文件的流程是一个责任链模式,首先使用一个 ClassReader 读入字节码,然后利用 ClassVisitor 做个性化的修改,最后利用 ClassWriter 输出修改 后的字节码。
之前提过,需要将读取的 class 文件的类名做一些修改,加载成一个全新名字的派 生类。这里将之分为了 2 个步骤。
第一步,先将原来的类变成接口。
清单 2. 重定义的 原始类
public Class<?> redefineClass(String className){ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassReader cr = null; ClassSource cs = classFiles.get(className); if(cs==null){ return null; } try { cr = new ClassReader(new FileInputStream(cs.getFile())); } catch (IOException e) { e.printStackTrace(); return null; } ClassModifier cm = new ClassModifier(cw); cr.accept(cm, 0); byte[] code = cw.toByteArray(); return defineClass(className, code, 0, code.length); }
以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索文件
, class
, classloader
, 更新
, 类名
, 热部署
, 热部署 java web 运维
, class无法加载
, 热编译
, 类加载
, 一个
, 加载监听
, 热替换
自动加载类实现
深入探索c 对象模型、深入探索android、深入探索透视投影变换、深入探索、对 深入部署,以便于您获取更多的相关知识。