转载

深入理解Java虚拟机--实现远程执行功能

在深入理解Java虚拟机一书中,在讲解类加载及其执行子系统一章中有个例子,例子对技术的总结非常的有代表性,这里记录、讲解以下。

我们知道,想要调试运行的代码,比较容易的方法是给服务器上传一个jsp,通过在jsp中的java代码来调试。书中给的例子也是通过jsp来触发,但是其可以在jsp中动态执行我们的java调试代码。通过这个例子可以很好的理解ClassLoader以及字节码结构,尤其是常量池。

先说下例子

书中的例子是,可以在不停服务的情况下,在服务端可以执行我们上传的一段Java代码,并可以获取到代码执行结果。同时还有个要求,即调试类可以被加载多次

设计、思路

  • 本地编写调试的Java代码,并在本地编译成Class文件,上传到服务器
  • 服务器通过自定义ClassLoader来记载调试java文件
  • 调试信息的输出,采用直接修改字节码的方式,把标准输出替换自定义的输出
  • 最后通过jsp来触发类的加载

实现

有四个辅助类来帮助实现这个问题,如下:

  • 首先定义类加载器 HotSwapClassLoader

那么为什么要自定义类加载器呢。如果把测试类放在服务端程序的classpath,系统类加载器也是可以加载的,这个没有问题。但是需求中有一个需要能重复加载类,那系统类加载器就实现不了了。那怎么来重复加载一个类呢,其实就是用不同的类加载器实例就可以了,后面写测试jsp的时候可以看到。

package com.huyeah.jvm;

public class HotSwapClassLoader extends ClassLoader{
       /**
        * 设置父加载器,其实也就是系统加载器,因为HotSwapClassLoader 类是由系统加载器来加载的
        */
	public HotSwapClassLoader() {
		super(HotSwapClassLoader.class.getClassLoader());
	}
	
	/**
	 * 将defindClass暴露出来,供外部调用
	 * @param classByte
	 * @return
	 */
	public Class<?> loadByte(byte[] classByte){
		return defineClass(null,classByte,0,classByte.length);
	}
}
复制代码
  • 字节码修改类 ClassModifier
package com.huyeah.jvm;

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;
	//这块做了一个hash,对应不同常量池中的类型对应的长度,因为utf8类型的长度是不确定的,数组的下表索引对应常量池的tag
	private static final int[] CONSTANT_ITEM_LENGTH = {-1,-1,-1,5,5,9,9,3,3,5,5,5,5};
	//表示1个字节
	private static final int u1 = 1;
	//表示两个字节
	private static final int u2 = 2;
	//传进来的字节数组
	private byte[] classByte;
	
	public ClassModifier(byte[] classByte){
		this.classByte = classByte;
	}
	
	public byte[] modifyUTF8Constant(String oldStr, String newStr){
		//首先获取常量池的长度,从class文件偏移8个字节开始的2个字节表示常量池的长度
		int cpc = getConstantPoolCount();
		//计算常量池中常量的开始地址,也即是8 + 2(偏移+长度占用的2个字节)
		int offset = CONSTANT_POOL_COUNT_INDEX + u2;
		//开始一个一个遍历常量池
		for(int i = 0; i < cpc; i++){
			//先取tag,占用1个字节
			int tag = ByteUtils.bytes2Int(classByte,offset, u1);
			//比较,如果是utf8类型
			if(tag == CONSTANT_Utf8_info){
				//utf8类型tag后的两个字段,表示字符串的长度,因为utf8类型的长度不是固定的,所以字节码用两个字节表示长度
				int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
				//再计算偏移量
				offset += (u1 + u2);
				//根据字符串长度取出utf8字符串
				String str = ByteUtils.bytes2String(classByte, offset, len);
				if(str.equalsIgnoreCase(oldStr)){
					//比较,如果是System类的字符引用,则替换成我们自己写的输出类
					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{
					//如果不是System的符号应用,则继续遍历
					offset += len;
				}
			}else{
				//修改偏移量
				offset += CONSTANT_ITEM_LENGTH[tag];
			}
		}
		return classByte;
	}
	
	public int getConstantPoolCount(){
		return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
	}
}

复制代码

工具类 ByteUtils ,工具类没什么好说的,就是普通的字节与int,String之间的相互转换

package com.huyeah.jvm;

public class ByteUtils {
	/**
	 * 字节转换成整形
	 * @param b
	 * @param start
	 * @param len
	 * @return
	 */
	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;
	}
}

复制代码
  • 自己的输出类 HackSystem 这个类的主要目的就是可以在调试类中使用标准输出,并且可以获取其内容,作者是通过修改测试类的字节码,把System类的符号引用替换成自定义的类来实现的。思路非常巧妙!!
package com.huyeah.jvm;

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

public class HackSystem {
	public final static InputStream in = System.in;
	//主要是这句,将out的输出,输出到了数组输出流,这样我们可以在数组输出流中获取到内容
	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 currentTimeMills(){
		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);
	}
}
复制代码
  • 最后对外提供入口类,组装逻辑 JavaClassExecuter
package com.huyeah.jvm;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class JavaClassExecuter {
	
	
	public static String execute(byte[] classByte) {
		HackSystem.clearBuffer();
		//修改字节码
		ClassModifier cm = new ClassModifier(classByte);
		//替换系统的System为自定义的类
		byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "com/huyeah/jvm/HackSystem");
		//每次都重新实现了类加载器实例,所以可以重复的加载测试类
		HotSwapClassLoader loader = new HotSwapClassLoader();
		//加载,通过这个方法,避开了双亲委托模式
		Class clz = loader.loadByte(modiBytes);
		
		try {
			//反射调用入口方法
			Method method = clz.getMethod("main", new Class[]{String[].class});
			method.invoke(null, new String[]{null});
		} catch (Throwable e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 

		return HackSystem.getBufferString();
	}

}

复制代码

调试java文件

这个测试文件,可以在本地javac编译好,然后上传到服务器的指定目录,jsp在加载时加载指定文件即可。 这个调试文件可以随便的编写,可以引用服务端项目的类,因为自定义类加载器的父加载器是系统类加载器,所以不会出现类找不到的情况。

我这举个例子,HelloWorld是项目中的类,将其copy出来,放到了调试类的目录中,为了是调试类能编译通过,之后就可以调用HelloWorld中的方法了。当时也可以使用反射来,我这么做的目的就是为了验证下类加载的过程。

package com.sxt.io;

import com.huyeah.jvm.HelloWorld;

public class TestClass {	
	public static void main(String []args) {
		System.out.println("----");
		System.out.println("======aa");
		System.out.println("------------" + HelloWorld.a);
	}
}

复制代码

测试的jsp文件

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="com.huyeah.jvm.*" %>
 
<%
	InputStream is = new FileInputStream("/Users/zxw/develop/server_workspace/IO_study01/src/com/sxt/io/TestClass.class");
	byte[] b = new byte[is.available()];
	is.read(b);
	is.close();
	
	out.println("<textarea style='width:1000;heigth=2000'>");
	out.println(JavaClassExecuter.execute(b));
	out.println("</textarea>");
%>
复制代码

总结

从上面可以看出,这个实例主要是使用了类加载、字节码修改的技术。可以说技术偏底层,需要类加载的过程,class文件的格式。这两部分也是非常重要的内容。 如果对字节码修改部分有疑问的话,需要详细的了解下字节码文件结构,其实可以通过jdk提供的javap工具来看看class文件的结构的。 这块举个例子如下: Constant pool就是常量池,我们修改的地方也是这块。

Classfile /Users/zxw/develop/server_workspace/IO_study01/src/com/sxt/io/TestClass.class
  Last modified 2019-12-17; size 470 bytes
  MD5 checksum 5af1034b41c6867f7c0c783a49c7bb1a
  Compiled from "TestClass.java"
public class TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #19            // ----this is test class out println
   #4 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #22            // ======
   #6 = Class              #23            // TestClass
   #7 = Class              #24            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               TestClass.java
  #16 = NameAndType        #8:#9          // "<init>":()V
  #17 = Class              #25            // java/lang/System
  #18 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #19 = Utf8               ----this is test class out println
  #20 = Class              #28            // java/io/PrintStream
  #21 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V
  #22 = Utf8               ======
  #23 = Utf8               TestClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String ----this is test class out println
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #5                  // String ======
        13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 7: 16
}
SourceFile: "TestClass.java"


复制代码
原文  https://juejin.im/post/5df8731be51d4557e94feac4
正文到此结束
Loading...