在深入理解Java虚拟机一书中,在讲解类加载及其执行子系统一章中有个例子,例子对技术的总结非常的有代表性,这里记录、讲解以下。
我们知道,想要调试运行的代码,比较容易的方法是给服务器上传一个jsp,通过在jsp中的java代码来调试。书中给的例子也是通过jsp来触发,但是其可以在jsp中动态执行我们的java调试代码。通过这个例子可以很好的理解ClassLoader以及字节码结构,尤其是常量池。
书中的例子是,可以在不停服务的情况下,在服务端可以执行我们上传的一段Java代码,并可以获取到代码执行结果。同时还有个要求,即调试类可以被加载多次
有四个辅助类来帮助实现这个问题,如下:
那么为什么要自定义类加载器呢。如果把测试类放在服务端程序的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); } } 复制代码
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; } } 复制代码
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); } } 复制代码
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(); } } 复制代码
这个测试文件,可以在本地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); } } 复制代码
<%@ 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" 复制代码