Java Code

Java实现动态部署

Posted on 2020-11-16,11 min read

最近看深入理解Java虚拟机的时候,第九章给了一个实战-手写一个远程执行功能,觉得是一个对JVM类加载机制以及类文件结构的一个很不错的总结。

原文内容

首先,在实现“在服务端执行临时代码”这个需求之前,先来明确一下本次实战的具体目标,我们
希望最终的产品是这样的:

  • 不依赖某个JDK版本才加入的特性(包括JVMTI),能在目前还被普遍使用的JDK中部署,只要
    是使用JDK 1.4以上的JDK都可以运行。
  • 不改变原有服务端程序的部署,不依赖任何第三方类库。
  • 不侵入原有程序,即无须改动原程序的任何代码。也不会对原有程序的运行带来任何影响。
  • 考虑到BeanShell Script或JavaScript等脚本与Java对象交互起来不太方便,“临时代码”应该直接支持Java语言。
  • “临时代码”应当具备足够的自由度,不需要依赖特定的类或实现特定的接口。这里写的是“不需要”而不是“不可以”,当“临时代码”需要引用其他类库时也没有限制,只要服务端程序能使用的类型和接口,临时代码都应当能直接引用。
  • “临时代码”的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及抛出的异常等。

看完上面列出的目标,读者觉得完成这个需求需要做多少工作量呢?也许答案比大多数人所想的都要简单一些:5个类,250行代码(含注释),大约一个半小时左右的开发时间就可以了,现在就开始编写程序吧!

在程序实现的过程中,我们需要解决以下3个问题:

  • 如何编译提交到服务器的Java代码?

  • 如何执行编译之后的Java代码?

  • 如何收集Java代码的执行结果?

对于第一个问题,我们有两种方案可以选择。一种在服务器上编译,在JDK 6以后可以使用Compiler API,在JDK 6以前可以使用tools.jar包(在JAVA_HOME/lib目录下)中的com.sun.tools.Javac.Main类来编译Java文件,它们其实和直接使用Javac命令来编译是一样的。这种思路的缺点是引入了额外的依赖,而且把程序绑死在特定的JDK上了,要部署到其他公司的JDK中还得把tools.jar带上(虽然JRockit和J9虚拟机也有这个JAR包,但它总不是标准所规定必须存在的)。

另外一种思路是直接在客户端编译好,把字节码而不是Java代码传到服务端,这听起来好像有点投机取巧,
一般来说确实不应该假定客户端一定具有编译代码的能力,也不能假定客户端就有编译出产品所需的
依赖项。但是既然程序员会写Java代码去给服务端排查问题,那么很难想象他的机器上会连编译Java程
序的环境都没有。
对于第二个问题:要执行编译后的Java代码,让类加载器加载这个类生成一个Class对象,然后反射调用一下某个方法就可以了(因为不实现任何接口,我们可以借用一下Java中约定俗成的“main()”方法)。但我们还应该考虑得更周全些:一段程序往往不是编写、运行一次就能达到效果,同一个类可能要被反复地修改、提交、执行。另外,提交上去的类要能访问到服务端的其他类库才行。还有就是既然提交的是临时代码,那提交的Java类在执行完后就应当能被卸载回收掉。
最后一个问题,我们想把程序往标准输出(System.out)和标准错误输出(System.err)中打印的信息收集起来。但标准输出设备是整个虚拟机进程全局共享的资源,如果使用System.setOut()/System.setErr()方法把输出流重定向到自己定义的PrintStream对象上固然可以收集到输出信息,但也会对原有程序产生影响:会把其他线程向标准输出中打印的信息也收集了。虽然这些并不是不能解决的问题,不过为了达到完全不影响原程序的目的,我们可以采用另外一种办法:直接在执行的类中把对System.out的符号引用替换为我们准备的PrintStream的符号引用,依赖前面学习到的知识,做到这一点并不困难。

代码实现

  • 自定义类加载器:自定义类加载器用于开放defineClass方法,来将二进制流转换成Class对象,被虚拟机加载。其中指定了父类加载器super(HotSwapClassLoader.class.getClassLoader());用于委托加载其他的类

    • package hotswap;
      
      /**
       * 为了多次载入执行类而加入的加载器
       * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
       * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
       *
       * @author zzm
       */
      public class HotSwapClassLoader extends ClassLoader {
          public HotSwapClassLoader() {
              super(HotSwapClassLoader.class.getClassLoader());
          }
      
          public Class loadByte(byte[] classByte) {
              return defineClass(null, classByte, 0, classByte.length);
          }
          
      }
      
  • 修改二进制流文件:为了实现文章上说的把System.out替换成自己准备的PrintStream功能,此处是对常量池进行修改,将System的符号引用替换成自定义System(HackSystem)的符号引用。这样加载热部署的类的时候就会把System对象给替换成HackSystem对象

    • package hotswap;
      
      /**
       * 修改Class文件,暂时只提供修改常量池常量的功能
       *
       * @author zzm
       */
      public class ClassModifier {
          /**
           * Class文件中常量池的起始偏移
           */
          private static final int CONSTANT_POOL_COUNT_INDEX = 8;
          /**
           * CONSTANT_Utf8_info常量的tag标志
           */
          private static final int CONSTANT_Utf8_info = 1;
          /**
           * 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的
           */
          private static final int[] CONSTANT_ITEM_LENGTH = {-1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5};
          private static final int u1 = 1;
          private static final int u2 = 2;
          private byte[] classByte;
      
          public ClassModifier(byte[] classByte) {
              this.classByte = classByte;
          }
      
          /**
           * 修改常量池中CONSTANT_Utf8_info常量的内容
           *
           * @param oldStr 修改前的字符串
           * @param newStr 修改后的字符串
           * @return 修改结果
           */
          public byte[] modifyUTF8Constant(String oldStr, String newStr) {
              //获取常量池的长度
              int cpc = getConstantPoolCount();
              // u2为常量池长度
              int offset = CONSTANT_POOL_COUNT_INDEX + u2;
              for (int i = 0; i < cpc; i++) {
                  // 获取单个tag
                  int tag = ByteUtils.bytes2Int(classByte, offset, u1);
                  if (tag == CONSTANT_Utf8_info) {
                      // 如果tag是utf8类型,则获取字符长度
                      int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
                      // offset往后移u1+u2两位
                      offset += (u1 + u2);
                      // 获取字符的值
                      String str = ByteUtils.bytes2String(classByte, offset, len);
                      if (str.equalsIgnoreCase(oldStr)) {
                          // 如果找到了目标类,即java.lang.System
                          byte[] strBytes = ByteUtils.string2Bytes(newStr);
                          byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
                          // 将本来的utf8长度换成新的utf8长度
                          classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
                          // 将本来的字符串替换为新的字符串
                          classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
                          return classByte;
                      } else {
                          // 如果没找到目标类则跳过
                          offset += len;
                      }
                  } else {
                      offset += CONSTANT_ITEM_LENGTH[tag];
                  }
              }
              return classByte;
          }
      
          /**
           * 获取常量池中常量的数量
           *
           * @return 常量池数量
           */
          public int getConstantPoolCount() {
              return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
          }
      }
      
  • HackSystem:自定义的System对象,主要作用是重定义System.out的流对象,其他方法委托给System处理

    • package hotswap;
      
      import java.io.ByteArrayOutputStream;
      import java.io.InputStream;
      import java.io.PrintStream;
      
      /**
       * 为Javaclass劫持java.lang.System提供支持
       * 除了out和err外,其余的都直接转发给System处理
       *
       * @author zzm
       */
      public class HackSystem {
          public final static InputStream in = System.in;
          private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
          public final static PrintStream out = new PrintStream(buffer);
          public final static PrintStream err = out;
      
          public static String getBufferString() {
              return buffer.toString();
          }
      
          public static void clearBuffer() {
              buffer.reset();
          }
      
          public static void setSecurityManager(final SecurityManager s) {
              System.setSecurityManager(s);
          }
      
          public static SecurityManager getSecurityManager() {
              return System.getSecurityManager();
          }
      
          public static long currentTimeMillis() {
              return System.currentTimeMillis();
          }
      
          public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
              System.arraycopy(src, srcPos, dest, destPos, length);
          }
      
          public static int identityHashCode(Object x) {
              return System.identityHashCode(x);
          }
      // 下面所有的方法都与java.lang.System的名称一样
      // 实现都是字节转调System的对应方法
      // 因版面原因,省略了其他方法
      }
      
      
  • ByteUtils:ClassModifier用到的一些Byte相关的方法

    • package hotswap;
      
      /**
       * Bytes数组处理工具
       *
       * @author
       */
      public class ByteUtils {
          public static int bytes2Int(byte[] b, int start, int len) {
              int sum = 0;
              int end = start + len;
              for (int i = start; i < end; i++) {
                  int n = ((int) b[i]) & 0xff;
                  n <<= (--len) * 8;
                  sum = n + sum;
              }
              return sum;
          }
      
          public static byte[] int2Bytes(int value, int len) {
              byte[] b = new byte[len];
              for (int i = 0; i < len; i++) {
                  b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff);
              }
              return b;
          }
      
          public static String bytes2String(byte[] b, int start, int len) {
              return new String(b, start, len);
          }
      
          public static byte[] string2Bytes(String str) {
              return str.getBytes();
          }
      
          public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
              byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
              System.arraycopy(originalBytes, 0, newBytes, 0, offset);
              System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
              System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset - len);
              return newBytes;
          }
      }
      
  • 测试类:用于测试上面的代码

    • package hotswap;
      
      import java.io.*;
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      import java.net.URLClassLoader;
      import java.util.Scanner;
      
      public class App {
      
          public static void main(String[] args) {
              Scanner in = new Scanner(System.in);
              String input;
              byte[] classBytes = null;
              while ((input = in.nextLine()) != "#") {
                  /**
                   * 一个ClassLoader不能重复加载同一个类否则会抛出异常
                   * Exception in thread "main" java.lang.LinkageError: loader hotswap.HotSwapClassLoader @3dd3bcd
                   * attempted duplicate class definition for hotswap.HotSwapClassA. (hotswap.HotSwapClassA is in
                   * unnamed module of loader hotswap.HotSwapClassLoader @3dd3bcd, parent loader 'app')
                   */
                  InputStream inputStream ;
      
                  HotSwapClassLoader classLoader = new HotSwapClassLoader();
                  try {
                      inputStream = new FileInputStream(new File(input));
                      classBytes = new byte[inputStream.available()];
                      inputStream.read(classBytes);
                  } catch (IOException e){
                      System.out.println("读取Class文件出错,重写输入!");
                      continue;
                  }
      
                  ClassModifier modifier = new ClassModifier(classBytes);
                  classBytes = modifier.modifyUTF8Constant("java/lang/System","hotswap/HackSystem");
                  Class hotSwapClass = classLoader.loadByte(classBytes);
                  HackSystem.clearBuffer();
      /*            Constructor constructor = hotSwapClass.getConstructor();
                  Object object = constructor.newInstance();
                  Method method = hotSwapClass.getMethod("hotSwapMethod", null);
                  method.invoke(object, null);*/
                  Method method = null;
                  try {
                      method = hotSwapClass.getMethod("hotSwapStaticMethod");
                      method.invoke(null);
                  } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                      e.printStackTrace();
                  }
                  System.out.println(HackSystem.getBufferString());
              }
      
      
          }
      }
      
  • 测试动态调用类

    package hotswap;
    
    public class HotSwapClassA {
        public HotSwapClassA(){}
        public void hotSwapMethod() {
            System.out.println("A的热调用方法");
        }
        public static void hotSwapStaticMethod(){
            System.out.println("A的热调用静态方法");
        }
    }
    
    
  • 如果还想实现动态编译的功能可以直接使用jdk1.6提供的StringSourceCompiler类,来对字符串代码进行编译。


下一篇: 设计模式-状态模式→

loading...