In understanding the Java Virtual Machine, in the chapter on Class loading and its execution subsystem, there is an example that is representative of the summary of the technology, which is documented and explained below.

We know that an easy way to debug running code is to upload a JSP to the server and debug using Java code in the JSP. The examples given in the book are also triggered by JSPS, but they dynamically execute our Java debugging code in JSPS. This example provides a good understanding of classLoaders and bytecode structures, especially constant pools.

Let me give you an example

The example in the book is that a piece of Java code uploaded by us can be executed on the server side under the condition of non-stop service, and the code execution result can be obtained. There is also a requirement that the debug class can be loaded multiple times

Design, ideas

  • Java code written locally for debugging, compiled locally into a Class file and uploaded to the server
  • The server uses a custom ClassLoader to log debug Java files
  • To output debugging information, directly modify the bytecode to replace the standard output with the customized output
  • Finally, JSP triggers the loading of the class

implementation

There are four helper classes to help with this problem, as follows:

  • First define a class loader, HotSwapClassLoader.

So why custom class loaders? If you put the test classes in the server’s classpath, the system class loader can be loaded without a problem. But there is a requirement to be able to reload classes that the system class loader cannot implement. How to load a class repeatedly is to use a different class loader instance, as you will see later when writing the test JSP.

package com.huyeah.jvm; Public class HotSwapClassLoader extends ClassLoader{/** ** sets the parent of the HotSwapClassLoader classHotSwapClassLoader() { super(HotSwapClassLoader.class.getClassLoader()); } /** * expose defindClass for external calls to * @param classByte * @return*/ public Class<? > loadByte(byte[] classByte){returndefineClass(null,classByte,0,classByte.length); }}Copy the code
  • Bytecode modification class ClassModifier
package com.huyeah.jvm; Public class modifier {// Start offset in class file private static final int CONSTANT_POOL_COUNT_INDEX = 8; Private static final int Constant_utf8_info = 1; private static final int Constant_utf8_info = 1; // Create a new onehash, corresponding to the length of the different types in the constant pool, because the length of the UTF8 type is uncertain, Private static final Int [] CONSTANT_ITEM_LENGTH = {-1,-1,-1,5,5,9,9,3,3,5,5,5,5,5}; Private static final int u1 = 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){ Int CPC = getConstantPoolCount(); // Calculate the starting address of the constant in the constant pool, i.e. 8 + 2 (offset + length 2 bytes) int offset = CONSTANT_POOL_COUNT_INDEX + u2; // Start iterating through the constant pool one by onefor(int i = 0; i < cpc; Int tag = byteutils.bytes2int (classByte,offset, u1); // Compare if it is utf8 typeif(tag == CONSTANT_Utf8_info){// two fields after the utF8 tag, indicating the length of the string, because the utF8 length is not fixed, Int len = byteutils.bytes2int (classByte, offset + u1, u2); // Calculate offset += (u1 + u2); Utf8 String String STR = byteutils. bytes2String(classByte, offset, len);if(str.equalsignorecase (oldStr)){// Compare, if it is a character reference of the System class, with our own output 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{// If the System symbol is not applied, continue traversing offset += len; }}else{// Modify offset offset += CONSTANT_ITEM_LENGTH[tag]; }}return classByte;
	}
	
	public int getConstantPoolCount() {returnByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); }}Copy the code

Utility class ByteUtils, there’s nothing to say about utility classes, just ordinary bytes and ints and String conversions, right

package com.huyeah.jvm; Public class ByteUtils {/** * bytes converted to integers * @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; // Shift 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);
		returnnewBytes; }}Copy the code
  • The main purpose of HackSystem is to be able to use standard output in the debug class and retrieve its contents. The author does this by modifying the bytecode of the test class and replacing symbolic references to the System class with custom classes. Very clever idea!!
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; Private static ByteArrayOutputStream buffer = new ByteArrayOutputStream(); public final static PrintStream out = new PrintStream(buffer); public final static PrintStream err = out; public static StringgetBufferString() {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){
		returnSystem.identityHashCode(x); }}Copy the code
  • Finally, it provides an entry class and assembles the logic 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(); // Modify the bytecode ClassModifier cm = new ClassModifier(classByte); Byte [] modiBytes = cm.modifyUTF8Constant("java/lang/System"."com/huyeah/jvm/HackSystem"); HotSwapClassLoader = new HotSwapClassLoader(); HotSwapClassLoader(); Class CLZ = loader.loadByte(modiBytes); // Load, by this method, avoids parent delegate mode. Method = clz.getMethod()"main", new Class[]{String[].class});
			method.invoke(null, new String[]{null});
		} catch (Throwable e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 

		returnHackSystem.getBufferString(); }}Copy the code

Debugging Java files

This test file, can be compiled in the local javAC, and then uploaded to the server specified directory, JSP load when the specified file can be loaded. This debug file can be written casually and can refer to server-side project classes, because the parent of the custom class loader is the system class loader, so classes will not be missing.

For example, HelloWorld is a class in your project. Copy it and put it in the debug class directory. In order for the debug class to compile, you can call the methods in HelloWorld. I could have used reflection, but I did that just to validate the class loading process.

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); }}Copy the code

The JSP file to test


<%@ 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(");
	out.println(JavaClassExecuter.execute(b));
	out.println("</textarea>"); % >Copy the code

conclusion

As you can see above, this example mainly uses classloading, bytecode modification techniques. We can say that the technology is low-level, need the class loading process, class file format. These two parts are also very important. If you are wondering about the bytecode modification part, you need to have a detailed understanding of the bytecode file structure. In fact, you can use the JAVap tool provided by the JDK to see the structure of the class file. Constant pool = Constant pool = Constant pool = Constant pool = 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."
      
       ":()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 
      
   #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 // "
      
       ":()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."
      
       ":()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"


Copy the code