深入理解Java虚拟机_09_09_类加载与执行子系统的案例与实战

时光怂恿深爱的人放手 提交于 2020-03-07 18:20:04

9.1 概述

在 Class 文件格式与执行引擎这部分中,用户的程序直接影响的内容并不太多,Class 文件以何种方式才存储,类何时加载、如何连接、以及虚拟机如何能执行字节码指令等都是由虚拟机直接控制的行为。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能。

9.2 案例分析

9.2.1 Tomcat:正统的类加载器架构

主流的 web服务器,都实现了自己定义的类加载。一般功能健全的 web 服务器都要解决以下几个问题:

  • 部署在同一个 web 服务器上的两个 web 应用程序所使用的 Java 类库可以实现相互隔离。两个应用程序可能会依赖三方包的不同版本,所以需要加载不同版本包
  • 服务器需要保证自身的安全不受 web应用程序的影响。服务器所使用的类库应该与应用程序的类库互相独立。
  • 支持 JSP 应用的 web 服务器,大多数都支持 hotswap 功能。

由于上述问题的存在,各种 web 服务器都不约而同的提供了好几个 ClassPath 路径提供用户存放第三方类库,这些路径一般都是以“lib”或“class”命令。被放置到不同目录的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里边的 Java 类库。

在 Tomcat 目录结构中,有 3 组目录(“/common”、“/server”、“/shared/”)可以存放 Java 类库,另外可以加上 web应用程序自身的目录“/WEB-INF/”,一共 4 组。

Tomcat 5.x 是这样的目录结构。6.x 就已经把三个目录合并到一起了。

  • 放置在/common 目录中:类库可被 Tomcat 和所有的 Web 应用程序共同使用。
  • 放置在/server 目录中:类库可以被 Tomcat使用,对所有的 web 应用程序都不可见。
  • 放置在/shared 目录中:类库可被所有的 web 应用程序共同使用,但是对 Tomcat 自己不可见。
  • 放置在/WebApp/WEB-INF 目录中:类库仅仅可以被次 web 应用程序使用,对 Tomcat 和其他 web 应用程序都不可见。

为了支持这套目录结构,Tomcat 自定了多个类加载器,它们按照经典的双亲委派模型来实现。

需要注意 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

JasperLoader 的加载范围仅仅是哪一个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为被丢弃:当服务检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JSP 类加载器来实现 JSP 文件的 hotswap 功能。

如果有10个WEB应用程序都是用Spring来进行组织管理的话,可以把Spring放到Common或Shared目录下(Tomcat5.0)让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户程序显然是放在/WEB-INF目录中的。那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?

我的答案是:Thread.setContextCloassloader,破坏双亲委派模型,打破从上往下加载的模型,从而实现类从下往上加载。

9.2.2 OSGi:灵活的类加载器架构

OSGi 中的每一个模块(称为 bundle)与普通的Java 类库区别不大,两者都是以 jar 格式进行封装,并且内部内存都是 Java package 和 class。但是一个 Bundle 可以声明依赖的 Java package,也可以声明它允许导出发布的 Java package。Bundle 之间的依赖关系从传统的上层模块依赖底层模块转为平级模块之间的关系依赖,而且类库的可见性能得到非常精准的控制,一个模块只有被 Export 过的 package 才可能由外界访问,其他的 package 和 class 将会隐藏起来。另外,OSGi 可以实现模块级别的热插拔功能,当程序升级更新或调试除错时,可以只停运、重新安装后启用程序的其中一部分。

OSGi 的 Bundle 类加载器之间只有规则,没有固定的委派关系。另外,一个 Bundle 类加载器为其他 Bundle 提供服务时,会根据 Export-Package 列表严格控制访问范围。

举个例子:

  • Bundle A:声明发布了 packageA,依赖了 java.* 的包。
  • Bundle B:声明依赖了 packageA 和 packageC ,同时也依赖 java.* 的包。
  • Bundle C:声明发布了 packageC,依赖了 packageA 的包。

这种模式下也可能会产生隐患,比如死锁,在类加载器对象的 java.land.ClassLoader.loadClass() 是一个 synchronized 方法,比如两个 Bundle 模块 A 和 B 在互相加载对应的类时,可能会出现死锁。不过在 jdk1.7中为非树状继承关系下的类加载器架构进行了一次专门的升级,目的是从底层避免这类死锁出现的问题。

9.2.3 字节码生成技术与动态代理的实现

字节码生成技术有很多,比如 Javascript、CGlib、ASM 之类的字节码类库,当然还有 javac 编译器。当然了除了 javac 和字节码类库,还有比如 web 服务器中的 JSP 编译器,编译时植入的 AOP 框架、还有很常用的动态代理技术,甚至使用反射时虚拟机都有可能在运行时生成字节码来提高执行速度。

package com.liukai.jvmaction.ch_09;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 9-1 动态代理的简单示例
 */
public class DynamicProxyTest {

  public static void main(String[] args) {
    // 设置保存生成的动态代理类文件到本地属性
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

    IHello iHello = (IHello) DynamicProxy.createProxyObject(new Hello());
    iHello.sayHello();
  }

  interface IHello {

    void sayHello();

  }

  static class Hello implements IHello {

    @Override
    public void sayHello() {
      System.out.println("hello world");
    }

  }

  static class DynamicProxy implements InvocationHandler {

    private Object target;

    public DynamicProxy(Object target) {
      this.target = target;
    }

    public static Object createProxyObject(Object target) {
      DynamicProxy dynamicProxy = new DynamicProxy(target);
      return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                          dynamicProxy);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 注意这里不能输出 proxy 的的相关方法,通过编译出来的 $Proxy0 类,
      // 方法它的相关方法都是调用 代理对象的目标相关方法,在这里调用会造成递归调用该方法,最终导致栈内存溢出
      // System.out.println("welcome" + proxy.toString());
      System.out.println("welcome");
      return method.invoke(target, args);
    }

  }

}

输出结果:
welcome
hello world

我们看下 Proxy.newProxyInstance() 的源码:


############################## Proxy ##############################

    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

    
    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        ... 省略代码
        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);
        ... 省略代码
    }
    
    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }
    
############################## Proxy ##############################

############################## WeakCache ##############################

    public V get(K key, P parameter) {
        ... 省略代码 ...
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        ... 省略代码 ...
    }

############################## WeakCache ##############################

############################## Proxy$ProxyClassFactory ##############################

private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // prefix for all proxy class names
        private static final String proxyClassNamePrefix = "$Proxy";

        // next number to use for generation of unique proxy class names
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            ... 省略代码 ....
            /*
             * 关键代码:生成字节码 byte 数组
             * Generate the specified proxy class. 
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }
    }
############################## Proxy$ProxyClassFactory ##############################

上述代码中的 Proxy.newProxyInstance() 方法会看到程序进行加载、验证、优化、同步、生成字节码、显示类加载等操作。最后它调用了 sun.misc.ProxyGenerator.generateProxyClass() 方法来完成字节码生成动作。这个方法可以在运行时产生一个描述代理类的字节码 byte[] 数组。我们通过在 main() 方法加入以下代码可以在运行时产生代理类。

我们在项目的 com.liukai.jvmaction.ch_09 目录下中就会看到生成的代理类 $Proxy0

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.liukai.jvmaction.ch_09;

import com.liukai.jvmaction.ch_09.DynamicProxyTest.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class $Proxy0 extends Proxy implements IHello {
  private static Method m1;
  private static Method m3;
  private static Method m2;
  private static Method m0;

  public $Proxy0(InvocationHandler var1) throws  {
    super(var1);
  }

  // 我们发下生成的代理类的方法,都是委托了 h 来执行相关方法
    
  public final boolean equals(Object var1) throws  {
    try {
      return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
    } catch (RuntimeException | Error var3) {
      throw var3;
    } catch (Throwable var4) {
      throw new UndeclaredThrowableException(var4);
    }
  }

  public final void sayHello() throws  {
    try {
      super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
      throw var2;
    } catch (Throwable var3) {
      throw new UndeclaredThrowableException(var3);
    }
  }

  public final String toString() throws  {
    try {
      return (String)super.h.invoke(this, m2, (Object[])null);
    } catch (RuntimeException | Error var2) {
      throw var2;
    } catch (Throwable var3) {
      throw new UndeclaredThrowableException(var3);
    }
  }

  public final int hashCode() throws  {
    try {
      return (Integer)super.h.invoke(this, m0, (Object[])null);
    } catch (RuntimeException | Error var2) {
      throw var2;
    } catch (Throwable var3) {
      throw new UndeclaredThrowableException(var3);
    }
  }

  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
      m3 = Class.forName("com.liukai.jvmaction.ch_09.DynamicProxyTest$IHello").getMethod("sayHello");
      m2 = Class.forName("java.lang.Object").getMethod("toString");
      m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    } catch (NoSuchMethodException var2) {
      throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
      throw new NoClassDefFoundError(var3.getMessage());
    }
  }
}

Java 的三种代理模式

  1. 静态代理(装饰器模式)
  2. 动态代理:代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理
  3. Cglib代理(基于继承的方式实现):Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.
  4. 在Spring的AOP编程中代理的选择方式: JDK 动态代理或者 CGlib 代理

参考:Java的三种代理模式(Spring动态代理对象)

9.2.4 Retrotranslator:跨越 JDK 版本

Retrotranslator 是一种名为“Java 逆向移植”的工具,它的作用是将 JDK 1.5 编译出来的 Class 文件转变为可以在 JDK 1.4 或 1.3 上部署的版本。它会把在编译阶段进行处理的动作通过 ASM 框架直接对字节码进行处理。

9.3 实战:自己动手实现远程执行功能

我们在做程序维护时会出现排除问题时,想知道内存中的一些参数值,却又没有办法把这些值输出到界面或者日志中,又或者定位某个缓存数据有问题、但缺少统一管理界面,不得不重启服务才能清理这个缓冲。我们将使用类加载以及虚拟机字节码执行引子系统的知识去实现在服务器端执行临时代码的功能。

9.3.1 目标

  • 不依赖 JDK 版本,能在目前普遍使用的 JDK 版本中部署。
  • 不改变原有服务器端服务的部署,不依赖第三方类库。
  • 不侵入原有程序,即无需改动代码,也不会对原有程序造成影响。
  • 使用 Java 语言编写临时代码。
  • 临时代码不依赖特定类或接口。
  • 临时代码执行结果能返回到客户端,结果包括正常返回信息和异常信息。

9.3.2 思路

问题:

  • 如何编译提交到服务器的代码
    1. 使用 tools.jar 包中的 javac.Main 类编译 Java 文件
    2. 直接在客户端编译好,把字节码而不是 Java 代码传到服务器端
  • 如何执行编译后的代码
    1. 让想执行编译后的代码,可以让类加载器加载这个类生成 Class 对象,然后执行 main 方法。同时支持多出加载,提交的类还能访问服务器端的其他类库,类之心完毕后要正常卸载和回收。
  • 如何收集 Java 代码的执行结果
    1. 直接在执行的类中对 System.out 的符号引用替换为我们准备的 PrintStream 的符号引用。

9.3.3 实现

第一个类用于解决同一个类的代码被多次加载的需求。

package com.liukai.jvmaction.ch_09;

/**
 * 9-3 热插拔类加载器
 * <p>
 * 为了多次载入执行类而加入的类加载器。
 * 把 defineClass 方法开发出来,只有外部显示调用时才会使用到 loadByte 方法。
 * 有虚拟机调用时,任然按照原先的双亲委派模型则使用 loadClass方法进行加载。
 * </p>
 */
public class HotSwapClassLoader extends ClassLoader {

  public HotSwapClassLoader() {
    // 这里实际使用的父类是web 服务器的加载器,我这边用的是 tomcat,它的加载器是 ParallelWebappClassLoader
    super(HotSwapClassLoader.class.getClassLoader());
  }

  public Class loadByte(byte[] classByte) {
    return defineClass(null, classByte, 0, classByte.length);
  }

}

这个类作用就是公开父类的 defineClass() 方法,把字节码数组加载为 Class 对象。还有构造函数中指定了 HotSwapClassLoader 的类加载器作为父类加载器,这一步是为了实现提交的代码可以访问服务器引用类库的关键,如果默认无参数构造器则默认使用 Java 的 app 应用程序加载器,它是加载不到 tomcat 中我们项目当中的类的,这一点需要注意。

第二个类是实现将 java.lang.System 替换为我们自定义的 HackSystem 类的过程,它直接修改了 Class 文件格式中的 byte[] 数组中的常量池部分,将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串。

package com.liukai.jvmaction.ch_09;

/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 */
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常量的内容
   * <p>
   *   注意该方法是在将内存中的 class 文件的字符串进行修改
   * </p>
   *
   * @param oldStr 修改前的字符串
   * @param newStr 修改后的字符串
   * @return 修改结果
   */
  public byte[] modifyUTF8Constant(String oldStr, String newStr) {
    int cpc = getConstantPoolCount();
    // 常量池的位移索引
    int offset = CONSTANT_POOL_COUNT_INDEX + u2;
    // 开始遍历寻找目标字符串
    for (int i = 0; i < cpc; i++) {
      // 搜索常量中常量类型 tag 为 1,即 CONSTANT_Utf8_info 的数据
      int tag = ByteUtils.bytes2Int(classByte, offset, u1);
      if (tag == CONSTANT_Utf8_info) {
        // utf8 类型的长度
        int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
        offset += (u1 + u2);
        // 将字节码转化为字符串
        String str = ByteUtils.bytes2String(classByte, offset, len);
        // 检查是否匹配的目标字符串
        if (str.equalsIgnoreCase(oldStr)) {
          // 将新的字符串转为字节码数组
          byte[] strBytes = ByteUtils.string2Bytes(newStr);
          // 字节码的长度属性
          byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
          // 替换旧字符串的字节码长度属性
          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);
  }

}

字节码工具类,这里涉及到 int2byte[] 以及 byte[]2int 等转换,用的的知识有计算器数据二进制的存储、二进制位运算、十六进制的表示,以及 jvm 虚拟机内存数据存储(高位补齐的方式)

package com.liukai.jvmaction.ch_09;

/**
 * Bytes数组处理工具
 */
public class ByteUtils {

  /*
  1. 以 byte a = (byte)0xa3; 为例
    首先byte类型是8 bit (8个0/1) , "A3"  --> 1010 0011 ,  jvm虚拟机内部存储的是int类型(4字节 32bit)时是直接将最高位(就是最左边)的值补到高位的24bit,
    于是“A3”  --> 1111 1111 1111  1111 1111 1111 1010 0011 (这里是将最高位的1 补到24bit高位).
    这个二进制表示是计算机内部存储地表示,而计算机内部是以补码的方式存储地,也就是说这个不是真正的值,只有将这个转换成原码才是真正的值。

    补码: 1111 1111 1111  1111 1111 1111 1010 0011    —>   -1
    反码: 1111 1111 1111  1111 1111 1111 1010 0010    —>  除最高位  全取反
    原码 :1000 0000 0000 0000 0000 0000 0101 1101   ——> -(64+16+8+4+1) = -93

    这就是为什么“A3”的byte直接赋值给int后会是 -93的原因。

    而我们经过之前的分析会发现,当byte的最高位为1时,也就是以上的情况时,byte的值就会从正整数变为负整数了自然也就是错了。
    当byte的最高位为0时,因为byte存储时补齐24bit高位时用的是0补了24个0 值也就没有变,数值也就是正常的。

    >>>>
    计算机内存存储方式:
    简单说明一下原码,反码,补码。
    正整数的原码,反,补码都是一样的,不用进行转换。
    负整数的原码,最高位时1,这个不变,将除最高位意外的全部取反,1变成0 ,0变成1,就是反码了,然后将反码+1(以二进制的形式计算)得到的就是补码。
    那么从补码返回到原码就是先-1,然后将除最高位以外的都取反得到原码。

  2. &运算符  | 运算符
    & 这个其实也比较的简单:
    1 & 1  --> 1
    (1 & 0)  (0 & 1)  (0 & 0)  --> 0
    也就是说只有1 & 1 时候才为1 只要有0 结果都是0

    | 这个就相反的了
    0 | 0 --> 0
    (1 | 0)  (0 | 1)  (1 | 1)  --> 1
    也就是说只有0 | 0 时候才为0 只要有1 结果都是1

    3. &0xFF的意义
    其实到了这个时候,我们都明白就是要将当byte最高位为1时,补的24bit高位的1转换成0 那么值就是正确的。
    而 &0xFF(这是个int类型的值 )这个操作就是起到这个作用(0xFF -->  0000 0000 0000 0000 0000 0000 1111 1111)(谢谢 评论里的回复):

    1111 1111 1111  1111 1111 1111 1010 0011 
    &
    0000 0000 0000 0000 0000 0000 1111 1111
    >>结果为:
    0000 0000 0000 0000 0000 0000 1010 0011

    这个的计算结果就是 24bit高位全部为0 ,而8bit低位保持原样。

    嗯,byte 转成 int 就完了。

    还有一个int 转换成 byte的情况,不过呢因为byte只有8bit因此需要byte[4]数组存储这个int,我就记录一下:

    int temp = 1009020;
    byte[0] = (byte)(temp >> 24 & 0xFF);
    byte[1] = (byte)(temp >> 16 & 0xFF);
    byte[3] = (byte)(temp >> 8 & 0xFF);
    byte[4] = (byte)(temp  & 0xFF);
    //0为高位 2,3,4依次为低位

    原理其实就是将32bit , 以8bit为一段分割了一下, 也就是 >> 8( 倍数)移位了一下,然后其他就如之前一样。

    记录一下几个重要的点

    * 计算机内部存储时是以补码的形式存储,如果是负整数,需要转换
    * jvm虚拟机存储byte类型值是以4个字节存的,也就是会在24bit高位补byte最高位的值
    * &运算符的规则

 */

  public static int bytes2Int(byte[] b, int start, int len) {

    int sum = 0;
    int end = start + len;
    // 比如这里的常量数十六进制表示方式为 0x4E20,十进制表示为 20000,二进制表示为 0100 1110 0010 0000
    // 这里的第一个 byte 十六进制表示为 0x4e,二进制表示为 0100 1110,第二个 byte 十六进制为 0x20,二进制位 0010 0000,我们要对其合并为 0x4E20 表示,
    // 注意位运算是按照二进制方式计算,我们要对第一个 byte 进行位移 8 位,得到的值用二进制表示为 0100 1110 0000 0000,十进制为 19968,
    // 再与第二个 byte 值 二进制表示为 0010 0000,十进制为 32 ,进行十进制的表示与加法计算,最终得到值 19968 + 32 = 20000
    for (int i = start; i < end; i++) {
      // 这里的作用是 byte 转为 int,将 byte值 进行 &0xff 即可将高 24位值设置为 0
      int n = ((int) b[i]) & 0xff;
      // 左移 --len * 8
      n <<= (--len) * 8;
      sum = n + sum;
    }
    return sum;
  }

  public static byte[] int2Bytes(int value, int len) {
    // 这里是将 int 值转为 byte 数组,因为一个 int 用了 32 位即 4byte 存储,所最多需要将 int 用 4 个 byte 存储。即 byte[4]
    // 将 32 位的 int 拆分为 4 个 8 位 的 byte,
    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;
  }

}

Java 中 byte 转换 int 的相关知识参考一下博客:

java中byte与int的转换原理

经过 ClassModifier 处理后的 byte[] 数组才会传给 HotSwapClassLoader 的 loadByte() 方法进行加载,byte[] 数组在这里替换符号引用之后,与客户端直接在 Java 代码中引用 HackSystem 类再编译生成的 Class 是完全一样的。这样实现的即避免了客户端编写临时执行代码时要依赖的特定类(不然无法引用 HackSystem),又避免了服务端修改标准输出后影响程序的输出。

最后一个类就是前面提到过的代替 java.lang.System 的 HackSystem,主要是替换了 out 和 err 两个静态变量改成了使用 ByteArrayOutputStream 作为打印目标的同一个 PrintStream 对象,以及增加清理和获取 ByteArrayOutputStream 的数据方法。

package com.liukai.jvmaction.ch_09;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

/**
 * 为JavaClass劫持java.lang.System提供支持
 * 除了out和err外,其余的都直接转发给System处理
 */
public class HackSystem {

  private static final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

  public  static final PrintStream out = new PrintStream(buffer);

  public  static PrintStream err = out;

  public static String getBufferString() {
    return buffer.toString();
  }

  public static void clearBuffer() {
    buffer.reset();
  }
  // 下面所有的方法都与java.lang.System的名称一样
  // 实现都是字节转调System的对应方法
  // 因版面原因,省略了其他方法
}

最后一个类 JavaClassExecuter 它是提供给外部调用的入口,进行对 Class 字节码文件的修改加载与执行的动作。

package com.liukai.jvmaction.ch_09;

import java.lang.reflect.Method;

/**
 * JavaClass执行工具
 */
public class JavaClassExecuter {

  /**
   * 执行外部传过来的代表一个Java类的Byte数组<br>
   * 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
   * 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息
   *
   * @param classByte 代表一个Java类的Byte数组
   * @return 执行结果
   */
  public static String execute(byte[] classByte) {
    // 清理出自定义类的输出流缓冲
    HackSystem.clearBuffer();
    // 修改字节码中的标准输出为自定义输出类的符号引用
    ClassModifier cm = new ClassModifier(classByte);
    byte[] modiBytes = cm
      .modifyUTF8Constant("java/lang/System", "com/liukai/jvmaction/ch_09/HackSystem");
    // 创建自定义加载类加载字节码为 Class 对象
    HotSwapClassLoader loader = new HotSwapClassLoader();
    Class clazz = loader.loadByte(modiBytes);
    try {
      // 通过反射执行字节码类的 main 方法
      Method method = clazz.getMethod("main", new Class[] {String[].class});
      method.invoke(null, new String[] {null});
    } catch (Throwable e) {
      // 将异常信息输出到自定义输出流中
      e.printStackTrace(HackSystem.out);
    }
    // 返回自定义输出流的信息
    return HackSystem.getBufferString();
  }

}

9.3.4 验证

我们建立一个空的 web 项目,将上述的几个类放入 web 项目中,同时需要写一个 Java 类命名为 HelloMyClass.java 并且在其 main() 方法中使用 System.put.println() 方法输出一些信息即可。再创建一个 JSP 文件,用于读取指定目录下的 class 文件,比如 C 盘下的 xxx.class 文件。然后调用 JavaClassExecuter 的 execute 方法,并且打印方法执行的信息。

测试类 HelloMyClass 代码如下:

package com.liukai.jvmaction.ch_09;

public class HelloMyClass {

  public static void main(String[] args) {
    System.out.println("hello 我是一个不需要服务器重启就是可以随时修改与执行的临时代码!可以随时的改变这个代码,然后把编译好的 class 文件传到指定服务的位置,让解析器随时加载与执行!");
  }

}

test.jsp 文件如下所示:

<%@ page contentType="text/html;charset=UTF-8"%>
<%--这里使用 utf8编码,防止class 文件中的中文输出会乱码--%>
<%@ page import="com.liukai.web.*" %>
<%@ page import="java.io.FileInputStream" %>
<%@ page import="java.io.InputStream" %>

<%
    // 执行这个目标 class 文件
    InputStream is = new FileInputStream("/Users/liukai/IdeaProjects/myproject/jvm-action/build/classes/java/main/com/liukai/jvmaction/ch_09/HelloMyClass.class");
    byte[] b = new byte[is.available()];
    is.read(b);
    is.close();

    out.println("<textarea style='width:1000;height=800'>");
    out.println(JavaClassExecuter.execute(b));
    out.println("</textarea>");
%>

启动项目,访问test.jsp 输出如下:

9.4 本章小结

本章我们更加深入的了解 Java 虚拟机类加载机制,以及字节码相关的技术,通过实战案例,更加深入的了解了 Class 文件结构的常量池数据,十六进制、二进制表示以及二进制的位运算、Java 虚拟机中数据的存储模式(高位补齐),同时还回顾了传统的创建 web 项目的方式,JSP 技术以及相关类加载原理,

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!