简介
  在 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.<em>COMPUTE_MAXS</em>);
  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&lt;?&gt; redefineClass(String className){
  ClassWriter cw = new ClassWriter(ClassWriter.<em>COMPUTE_MAXS</em>);
  ClassReader cr = null;
  ClassSource cs = <em>classFiles</em>.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);
  }
  首先 load 原始类的 class 文件,此处定义了一个增强组件 ClassModifier,作用是修改原始类的类型,将它转换成接口。原始类的所有方法逻辑都会被去掉。
  第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。这样做的目的是不论如何修改,同一个 class 的派生类都有一个共同的接口,他们之间的转换变得对外不透明。
  清单 3. 定义一个派生类
  // 在 class 文件发生改变时重新定义这个类
  private Class&lt;?&gt; redefineClass(String className, ClassSource classSource){
  ClassWriter cw = new ClassWriter(ClassWriter.<em>COMPUTE_MAXS</em>);
  ClassReader cr = null;
  classSource.update();
  String enhancedClassName = classSource.getEnhancedName();
  try {
  cr = new ClassReader(
  new FileInputStream(classSource.getFile()));
  } catch (IOException e) {
  e.printStackTrace();
  return null;
  }
  EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"),
  enhancedClassName.replace(".", "/"));
  ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"),
  enhancedClassName.replace(".", "/"));
  cr.accept(exm, 0);
  byte[] code = cw.toByteArray();
  classSource.setByteCopy(code);
  Class&lt;?&gt; clazz = defineClass(enhancedClassName, code, 0, code.length);
  classSource.setClassCopy(clazz);
  return clazz;
  }
  再次 load 原始类的 class 文件,此处定义了两个增强组件,一个是 EnhancedModifier,这个增强组件的作用是改变原有的类名。第二个增强组件是 ExtendModifier,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。
  自定义 classloader 还有一个作用是监听会发生改变的 class 文件,classloader 会管理一个定时器,定时依次扫描这些 class 文件是否改变。
  改变创建对象的行为
  Java 虚拟机常见的创建对象的方法有两种,一种是静态创建,直接 new 一个对象,一种是动态创建,通过反射的方法,创建对象。
  由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。我们要做的是将实例化原始类的行为变成实例化派生类。
  对于第一种方法,需要做的是将静态创建,变为通过 classloader 获取 class,然后动态创建该对象。
  清单 4. 替换后的指令集所对应的逻辑
  // 原始逻辑
  Greeter p = new Greeter();
  // 改变后的逻辑
  IGreeter p = (IGreeter)MyClassLoader.<em>getInstance</em>().
  findClass("com.example.Greeter").newInstance();
  这里又需要用到 ASM 来修改 class 文件了。查找到所有 new 对象的语句,替换成通过 classloader 的形式来获取对象的形式。