In depth understanding of the case and practice of Java virtual machine class loading and execution subsystem

Posted by dlgilbert on Sat, 07 Mar 2020 11:08:29 +0100

9.1 overview

In the part of Class file format and execution engine, there are not too many contents directly affected by the user's program. How to store the Class file, when to load the Class, how to connect it, and how the virtual machine can execute bytecode instructions are all directly controlled by the virtual machine. The main functions that can be operated by program are bytecode generation and classloader.

9.2 case analysis

9.2.1 Tomcat: the orthodox classloader architecture

The mainstream web servers have implemented their own defined class loading. The following problems should be solved in general web servers with sound functions:

  • Java class libraries used by two web applications deployed on the same web server can be isolated from each other. Two applications may depend on different versions of the three-party package, so different versions of the package need to be loaded
  • The server needs to ensure that its security is not affected by the web application. The class library used by the server should be independent of the application's class library.
  • Most web servers supporting JSP applications support the function of hotswap.

Because of the above problems, all kinds of web servers provide several ClassPath paths for users to store the third-party class library, which are usually based on "lib" or "class" commands. Class libraries placed in different directories have different access scopes and service objects. Generally, each directory will have a corresponding custom class loader to load the Java class libraries placed in it.

In the Tomcat directory structure, there are three groups of directories ("/ common", "server", "shared /") that can store Java class libraries. In addition, you can add the directory "/ WEB-INF /" of the web application itself, four groups in total.

Tomcat 5.x is such a directory structure. 6.x has combined the three directories.

  • In the / common directory: the class library can be used by Tomcat and all Web applications.
  • Placed in the / server directory: the class library can be used by Tomcat and is not visible to all web applications.
  • Placed in the / shared directory: the class library can be used by all web applications, but is not visible to Tomcat itself.
  • Placed in the / WebApp/WEB-INF Directory: the class library can only be used by secondary web applications, not visible to Tomcat and other web applications.

In order to support this set of directory structure, Tomcat has designed several class loaders, which are implemented according to the classic parental delegation model.

It should be noted that there are usually multiple instances of WebApp class loader and JSP class loader. Each Web application corresponds to a WebApp class loader, and each JSP file corresponds to a JSP class loader.

The loading scope of JasperLoader is just the Class compiled by the JSP file. Its purpose is to be discarded: when the service detects that the JSP file has been modified, it will replace the current instance of JasperLoader, and realize the hotswap function of JSP file by creating a new JSP Class loader.

If 10 web applications are organized and managed by spring, you can put spring in the Common or Shared directory (Tomcat 5.0) for these applications to share. Spring needs to manage the classes of user programs, and naturally it needs to be able to access the classes of user programs, which are obviously placed in the / WEB-INF directory. How can spring loaded by CommonClassLoader or SharedClassLoader access user programs that are not in its loading scope?

My answer is: Thread.setContextCloassloader, breaking the parent delegation model, breaking the top-down loading model, so as to implement the class loading from the bottom up.

9.2.2 OSGi: flexible classloader architecture

Each module (called Bundle) in OSGi is not different from the ordinary Java class library. Both modules are packaged in jar format, and the internal memory is Java package and class. However, a Bundle can declare the dependent Java package, or it can also declare that it allows to Export the published Java package. The dependency relationship between bundles has changed from the traditional upper module to the lower module, and the visible performance of the class library has been precisely controlled. Only exported packages of one module can be accessed by the outside world, and other packages and classes will be hidden. In addition, OSGi can realize module level hot plug function. When the program is upgraded or debugged, only part of the program can be enabled after shutdown and reinstallation.

There are only rules between OSGi's Bundle class loaders, and there is no fixed delegation relationship. In addition, when a Bundle class loader provides services for other bundles, the access scope will be strictly controlled according to the export package list.

for instance:

  • Bundle A: it is declared that packageA has been released and depends on java. * packages.
  • Bundle B: declaration depends on packageA and packageC, as well as java. * packages.
  • Bundle C: declare that the package C has been released and depends on the package a.

In this mode, there may also be hidden dangers, such as deadlock. In the class loader object, java.land.ClassLoader.loadClass() is A synchronized method. For example, when two Bundle modules A and B load corresponding classes, deadlock may occur. However, in jdk1.7, A special upgrade has been made for the classloader architecture under non tree inheritance, in order to avoid such deadlock problems from the bottom.

9.2.3 bytecode generation technology and implementation of dynamic agent

There are many bytecode generation technologies, such as Javascript, CGlib, ASM and other bytecode class libraries, as well as the javac compiler. Of course, in addition to javac and bytecode class libraries, there are also JSP compilers in web servers, AOP framework embedded in compilation, and very common dynamic proxy technology. Even when using reflection time virtual machine, it is possible to generate bytecode at runtime to improve execution speed.

package com.liukai.jvmaction.ch_09;

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

/**
 * 9-1 A simple example of a dynamic agent
 */
public class DynamicProxyTest {

  public static void main(String[] args) {
    // Set save generated dynamic proxy class file to local property
    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 {
      // Note that the related methods of proxy cannot be output here. Through the compiled $Proxy0 class,
      // Method its related methods are all the target related methods that call the proxy object. Calling this method here will cause recursive calls to this method, and eventually lead to stack memory overflow
      // System.out.println("welcome" + proxy.toString());
      System.out.println("welcome");
      return method.invoke(target, args);
    }

  }

}

//Output results:
welcome
hello world

Let's look at the source code of 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
    {
        ... Ellipsis code
        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);
        ... Ellipsis code
    }
    
    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) {
        ... Ellipsis code ...
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        ... Ellipsis code ...
    }

############################## 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) {

            ... Ellipsis code ....
            /*
             * Key code: generate bytecode byte array
             * 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 ##############################

The Proxy.newProxyInstance() method in the above code will see the program load, verify, optimize, synchronize, generate bytecode, display class load and other operations. Finally, it calls the sun.misc.ProxyGenerator.generateProxyClass() method to complete the bytecode generation. This method can generate an array of bytecode bytes [] describing the proxy class at run time. We can generate a proxy class at run time by adding the following code to the main() method.

We can see the generated agent class $Proxy0 in the project's com.liukai.jvmaction.ch_09 directory

//
// 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);
  }

  // The methods of the generated proxy class we send are all delegated to h to execute the relevant methods
    
  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());
    }
  }
}

Three proxy modes of Java

  1. Static proxy (decorator mode)
  2. Dynamic proxy: the proxy object does not need to implement the interface, but the target object must implement the interface, otherwise dynamic proxy cannot be used
  3. Cglib proxy (based on inheritance): cglib proxy, also known as subclass proxy, is to build a subclass object in memory to realize the extension of the function of the target object
  4. The choice of agent in Spring's AOP programming: JDK dynamic agent or CGlib agent

Reference resources: Java's three proxy patterns (Spring dynamic proxy object)

9.2.4 Retrotranslator: cross JDK version

Retrotranslator is a tool called "Java reverse migration". Its purpose is to convert the Class file compiled by JDK 1.5 into a version that can be deployed on JDK 1.4 or 1.3. It processes the bytecode directly through the ASM framework.

9.3 actual combat: realize remote execution function by yourself

We want to know some parameter values in memory, but we can't output these values to the interface or log, or locate a cache data problem, but there is no unified management interface, so we have to restart the service to clean up the buffer. We will use the knowledge of class loading and virtual machine bytecode execution to implement the function of executing temporary code on the server side.

9.3.1 target

  • It does not rely on the JDK version and can be deployed in the currently commonly used JDK version.
  • It does not change the deployment of the original server-side services and does not rely on the third-party class library.
  • Do not invade the original program, that is, do not need to change the code, and will not affect the original program.
  • Write temporary code in the Java language.
  • Temporary code does not depend on a specific class or interface.
  • Temporary code execution results can be returned to the client, including normal return information and exception information.

9.3.2 thinking

Question:

  • How to compile code submitted to the server
    1. Compiling Java files using javac.Main class in tools.jar package
    2. Compile directly on the client side and send bytecode instead of Java code to the server side
  • How to execute compiled code
    1. If you want to execute the compiled code, you can let the Class loader load the Class to generate a Class object, and then execute the main method. At the same time, it supports multiple loading. The submitted Class can also access other Class libraries on the server side. After the Class center is completed, it should be unloaded and recycled normally.
  • How to collect the execution results of Java code
    1. Replace the symbol reference of System.out directly in the executed class with the symbol reference of PrintStream we prepared.

9.3.3 implementation

The first class is used to solve the need that the code of the same class is loaded multiple times.

package com.liukai.jvmaction.ch_09;

/**
 * 9-3 Hot plug classloader
 * <p>
 * Class loader added to load an execution class multiple times.
 * When the defineClass method is developed, the loadByte method is used only when the external display is called.
 * When a virtual machine is called, the loadClass method is still used to load according to the original parent delegation model.
 * </p>
 */
public class HotSwapClassLoader extends ClassLoader {

  public HotSwapClassLoader() {
    // The actual parent class used here is the web server loader. I use tomcat, and its loader is ParallelWebappClassLoader
    super(HotSwapClassLoader.class.getClassLoader());
  }

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

}

The function of this Class is to open the defineClass() method of the parent Class and load the bytecode array as a Class object. In addition, the Class loader of HotSwapClassLoader is specified as the parent Class loader in the constructor. This step is to realize that the submitted code can access the key of the server reference Class library. If the default parameterless constructor is Java app Application Loader by default, it can't load the classes in our project in tomcat, which needs to be noted.

The second Class is to implement the process of replacing java.lang.System with our custom HackSystem Class. It directly modifies the constant pool part of the byte [] array in the Class file format, and replaces the constant ﹣ utf8 ﹣ info constant of the specified content in the constant pool with a new string.

package com.liukai.jvmaction.ch_09;

/**
 * Modify the Class file. Only constant pool constants can be modified temporarily
 */
public class ClassModifier {

  /**
   * Class Start offset of constant pool in file
   */
  private static final int CONSTANT_POOL_COUNT_INDEX = 8;

  /**
   * CONSTANT_Utf8_info tag flag of constant
   */
  private static final int CONSTANT_Utf8_info = 1;

  /**
   * The length of 11 constants in the constant pool, except constant type utf8 info, because it is not fixed length
   */
  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;
  }

  /**
   * Modify the content of the constant? Utf8? Info constant in the constant pool
   * <p>
   *   Note that this method is to modify the string of the class file in memory
   * </p>
   *
   * @param oldStr String before modification
   * @param newStr Modified string
   * @return Modification result
   */
  public byte[] modifyUTF8Constant(String oldStr, String newStr) {
    int cpc = getConstantPoolCount();
    // Displacement index of constant pool
    int offset = CONSTANT_POOL_COUNT_INDEX + u2;
    // Start traversing to find the target string
    for (int i = 0; i < cpc; i++) {
      // Search for data of constant type tag 1 in constant, i.e. constant ﹣ utf8 ﹣ info
      int tag = ByteUtils.bytes2Int(classByte, offset, u1);
      if (tag == CONSTANT_Utf8_info) {
        // Length of utf8 type
        int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
        offset += (u1 + u2);
        // Convert bytecode to string
        String str = ByteUtils.bytes2String(classByte, offset, len);
        // Check for matching target strings
        if (str.equalsIgnoreCase(oldStr)) {
          // Convert new string to bytecode array
          byte[] strBytes = ByteUtils.string2Bytes(newStr);
          // Length attribute of bytecode
          byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
          // Replace bytecode length property of old string
          classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
          // Replace bytecode of old string
          classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
          return classByte;
        } else {
          offset += len;
        }
      } else {
        offset += CONSTANT_ITEM_LENGTH[tag];
      }
    }
    return classByte;
  }

  /**
   * Get the number of constants in the constant pool
   *
   * @return Number of constant pools
   */
  public int getConstantPoolCount() {
    return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
  }

}

Bytecode tool class, which involves int2byte [] and byte[]2int conversion, uses the knowledge of binary storage of calculator data, binary bit operation, hexadecimal representation, and memory data storage of jvm virtual machine (high-order complement method)

package com.liukai.jvmaction.ch_09;

/**
 * Bytes Array processing tools
 */
public class ByteUtils {

  /*
  1. Take byte a = (byte)0xa3; for example
    First, the byte type is 8 bits (8 0 / 1), "A3" -- > 1010 0011. When the internal storage of the jvm virtual machine is int type (4 bytes 32 bit), the value of the highest bit (that is, the left most bit) is directly filled to the value of the high bit 24,
    So "A3" - > 1111 1111 1111 1111 1111 1111 1010 0011 (here is to fill the highest 1 to the high 24bit)
    The binary representation is the representation of internal storage of the computer, and the internal storage of the computer is in the form of complement, that is to say, this is not the real value, only when this is converted into the original code is the real value.

    Complement: 1111 1111 1111 1111 1111 1111 1010 0011 - > 1
    Inverse code: 1111 1111 1111 1111 1111 1111 1010 0010 - > reverse all except the highest order
    Original code: 10 million 0000 0000 0000 0101 1101 - > - (64 + 16 + 8 + 4 + 1) = - 93

    This is why the byte of "A3" will be - 93 when assigned directly to int.

    We will find that when the highest bit of byte is 1, that is to say, in the above case, the value of byte will change from a positive integer to a negative integer, which is naturally wrong.
    When the highest bit of byte is 0, because when byte stores and complements the high bit of 24 bits, it uses 0 to supplement 24 zero values, so the value is normal.

    >>>>
    Computer memory storage mode:
    Briefly explain the original code, inverse code and complement code.
    The source code, inverse code and complement code of positive and integer are the same, so no conversion is needed.
    The original code of a negative integer, when it is at the highest position, is the same. If you reverse all except the highest position, 1 becomes 0, 0 becomes 1, that is the inverse code. Then you can get the complement code by adding the inverse code + 1 (calculated in binary form).
    Then returning the complement to the original code is - 1, and then get the original code by reversing everything except the highest bit.

  2. &Operator | operator
    & This is also relatively simple:
    1 & 1  --> 1
    (1 & 0)  (0 & 1)  (0 & 0)  --> 0
    That is to say, only 1 & 1 is 1. If there is 0, the result is 0

    | This is the opposite
    0 | 0 --> 0
    (1 | 0)  (0 | 1)  (1 | 1)  --> 1
    That is to say, only 0 | 0 is 0. As long as there is 1, the result is 1

    3. &0xFF Significance
    In fact, at this time, we all know that when the highest bit of byte is 1, the 1 of the 24 bit high bit of complement is converted to 0, then the value is correct.
    And & 0xff (this is an int type value) is the operation (0xff > 0000 0000 0000 0000 0000 1111 1111) (thank you for your reply in the comment):

    1111 1111 1111  1111 1111 1111 1010 0011 
    &
    0000 0000 0000 0000 0000 0000 1111 1111
    >>The result is:
    0000 0000 0000 0000 0000 0000 1010 0011

    The result of this calculation is that the high bits of 24 bits are all 0, while the low bits of 8 bits remain the same.

    Well, it's over when byte becomes int.

    There is also a case where an int is converted to a byte. However, since byte is only 8 bits, the byte[4] array is needed to store this int. let me record:

    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 High 2,3,4 are low

    In fact, the principle is to divide 32bit into 8bit segments, that is to say, shift > > 8 (multiple), and then others are the same as before.

    Record some important points

    * The internal storage of the computer is in the form of complement. If it is a negative integer, it needs to be converted
    * jvm The value of byte type stored in virtual machine is stored in 4 bytes, that is, the highest byte value will be filled in the high bit of 24bit
    * &Rules for operators

 */

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

    int sum = 0;
    int end = start + len;
    // For example, the decimal representation of the constant is 0x4E20, the decimal representation is 20000, and the binary representation is 0100 1110 0010 0000
    // Here, the first byte hexadecimal is 0x4e, the binary is 0100 1110, the second byte hexadecimal is 0x20, and the binary bit is 0010 0000. We want to merge it into 0x4E20,
    // Note that the bit operation is calculated in binary mode. We need to shift the first byte by 8 bits, and the value obtained is expressed as 0100 1110 0000 0000 in binary system and 19968 in decimal system,
    // Then the binary representation of the second byte value is 0010 0000, and the decimal value is 32. The decimal representation and addition calculation are carried out, and the final value is 19968 + 32 = 20000
    for (int i = start; i < end; i++) {
      // The function here is to convert byte to int, and set the high 24 bit value to 0 by & 0xff
      int n = ((int) b[i]) & 0xff;
      // Move left -- len * 8
      n <<= (--len) * 8;
      sum = n + sum;
    }
    return sum;
  }

  public static byte[] int2Bytes(int value, int len) {
    // Here is the conversion of int value to byte array, because an int is stored in 32-bit, that is, 4 bytes, and the maximum number of ints needs to be stored in 4 bytes. Namely byte[4]
    // Split the 32-bit int into 4 8-bit byte s,
    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) {
    // Create a new bytecode array
    byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
    // Copy the data before the replaced bytecode
    System.arraycopy(originalBytes, 0, newBytes, 0, offset);
    // Set the bytecode replaced to the new byte array
    System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
    // Copy the data after the replaced bytecode to the new byte array
    System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length,
                     originalBytes.length - offset - len);
    return newBytes;
  }

}

Refer to the following blog for the knowledge of byte conversion int in Java:

The conversion principle of byte and int in java

The byte [] array processed by ClassModifier will be transferred to the loadByte() method of HotSwapClassLoader for loading. After replacing the symbol reference, the byte [] array is exactly the same as the Class generated by the client directly referencing the HackSystem Class in Java code and then compiling. This implementation not only avoids the specific classes that the client needs to rely on when writing temporary execution code (otherwise, it cannot refer to HackSystem), but also avoids the impact of the server modifying the standard output on the program output.

The last class is the HackSystem that replaces java.lang.System mentioned earlier. It mainly replaces the two static variables of out and err, and changes to the same PrintStream object using ByteArrayOutputStream as the print target, as well as increasing the data method of cleaning and obtaining ByteArrayOutputStream.

package com.liukai.jvmaction.ch_09;

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

/**
 * Support for Java class hijacking java.lang.System
 * Except for out and err, the rest are directly forwarded to the System for processing
 */
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();
  }
  // All of the following methods have the same name as java.lang.System
  // Implementation is the corresponding method of byte transfer System
  // For layout reasons, other methods are omitted
}

The last Class, javaclassexecutor, is the entry for external calls to modify, load and execute Class bytecode files.

package com.liukai.jvmaction.ch_09;

import java.lang.reflect.Method;

/**
 * JavaClass Execution tool
 */
public class JavaClassExecuter {

  /**
   * Execute the Byte array representing a Java class passed from outside < br >
   * Modify the constant ﹣ utf8 ﹣ info constant representing java.lang.System in the byte array of the input class to the hijacked HackSystem class
   * The execution method is the static main(String[] args) method of the class, and the output result is the information output by the class to System.out/err
   *
   * @param classByte Byte array representing a Java class
   * @return results of enforcement
   */
  public static String execute(byte[] classByte) {
    // Clean up the output stream buffer of the custom class
    HackSystem.clearBuffer();
    // Modify standard output in bytecode to symbol reference of custom output class
    ClassModifier cm = new ClassModifier(classByte);
    byte[] modiBytes = cm
      .modifyUTF8Constant("java/lang/System", "com/liukai/jvmaction/ch_09/HackSystem");
    // Create custom load Class load bytecode as Class object
    HotSwapClassLoader loader = new HotSwapClassLoader();
    Class clazz = loader.loadByte(modiBytes);
    try {
      // main method to execute bytecode class by reflection
      Method method = clazz.getMethod("main", new Class[] {String[].class});
      method.invoke(null, new String[] {null});
    } catch (Throwable e) {
      // Output exception information to custom output stream
      e.printStackTrace(HackSystem.out);
    }
    // Returns information about a custom output stream
    return HackSystem.getBufferString();
  }

}

9.3.4 verification

We need to write a Java class named HelloMyClass.java and use the System.put.println() method in its main() method to output some information. Create a JSP file to read the class file under the specified directory, such as the xxx.class file under the C disk. Then we call the execute method of JavaClassExecuter and print the information executed by the method.

The test class HelloMyClass code is as follows:

package com.liukai.jvmaction.ch_09;

public class HelloMyClass {

  public static void main(String[] args) {
    System.out.println("hello I am a temporary code that can be modified and executed at any time without server restart! You can change this code at any time, and then compile it class File to the specified service location, so that the parser can load and execute at any time!");
  }

}

The test.jsp file is as follows:

<%@ page contentType="text/html;charset=UTF-8"%>
<%--Use here utf8 Coding, preventing class Chinese output in the file will be garbled--%>
<%@ page import="com.liukai.web.*" %>
<%@ page import="java.io.FileInputStream" %>
<%@ page import="java.io.InputStream" %>

<%
    // Execute this target class file
    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>");
%>

Start the project and access test.jsp to output the following:

9.4 summary of this chapter

In this chapter, we have a deeper understanding of the Java virtual machine Class loading mechanism and bytecode related technologies. Through practical cases, we have a deeper understanding of constant pool data of Class file structure, hexadecimal and binary representation, binary bit operation, storage mode (high-order completion) of data in Java virtual machine, and also a review of traditional web creation Project mode, JSP technology and related Class loading principle,

Topics: Programming Java JSP Tomcat Spring