在服务器动态执行一段临时代码,用于查看应用程序内存中的一些参数值、或者定位和排除一些线上问题;

  • 使用 BTrace、Arthas 之类的 JVMTI 工具动态修改程序中一部分代码;
  • 使用 JDK 6 之后提供的 Compiler API,动态编译 Java 程序;
  • 写一个 JSP 文件上传到服务器,然后在浏览器中运行它;或者在应用程序中调用 BeanShell Script、JavaScript 等执行引擎动态执行脚本;
  • 在应用程序中内置动态执行的功能;


文章目录

  • 1. 目的
  • 2. 思路
  • 3. 实现
  • 4. 验证


1. 目的

产品功能说明

  • 不受限于 JDK 版本特性(如 JVMTI),在 JDK 1.4 以上版本可以运行;
  • 不改变原服务端程序的部署,不依赖任何第三方库;
  • 不侵入原有程序(不改动原有程序的代码,也不影响原有程序的运行效果);
  • 不引入 BeanShell Script 和 JavaScript 等与 Java 对象交互不便的脚本,临时代码直接支持 Java 语言;
  • 临时代码不依赖特定的类或不必要实现特定的接口,只要是服务端程序能使用的类和接口,临时代码都可以直接引用;
  • 临时代码的执行效果直接返回给客户端,执行结果包括程序输出的信息以及抛出的异常等;

2. 思路

面临的问题

  • 编译提交到服务器的 Java 代码;

在服务器上编译,在 JDK 6 以后可以使用 Compiler API,在 JDK 6 以前可以使用 tools.jar 包(JAVA_HOME/lib)的 com.sun.tools.javac.Main 类来编译 Java 文件;需要引入特定版本 JDK 的类库,而且要将之部署到服务器上;

在客户端编译,然后将字节码上传服务器,但需要客户端有变异代码的能力;

  • 执行编译之后的 Java 代码;

实现一个类加载器加载临时代码类,然后反射调用某个方法(可以直接执行 main(),避免需要继承接口);同一个类要可以反复修改、提交、执行、卸载、回收;可以访问服务端其他类库;

  • 收集 Java 代码执行结果;

在执行的临时代码类中把 System.out 的符号引用替换成自己实现的 PrintStream 的服务号引用,将临时代码的 System.out 和 System.err 的输出单独按指定方式输出(不影响原程序的输出;标准输出设备是整个 VM 进程的全局共享资源,若使用 System.setOut()、System.setErr() 重定向输出,会影响原程序的输出);

3. 实现

  • 通过继承 ClassLoader 类实现一个 HotSwapClassLoader,不重写 loadClass() 和 findClass()(保障正常情况下按照双亲委派模型进行原程序的类加载);开放 ClassLoader 中的 defineClass(),用于将临时代码的 byte[] 数组转变成 Class 对象;
/**
 * 用于重复载入临时代码;
 * 开放 defineClass(),外部可以显示调用 loadByte();
 * JVM 调用时使用原双亲委派机制调用 loadClass() 进行类加载;
 *
 * @author Aurelius-Shu
 * @since 2023-02-15
 */
public class HotSwapClassLoader extends ClassLoader {
    public HotSwapClassLoader() {
        // 指定加载 HotSwapClassLoader 的类即为父类加载器;
        super(HotSwapClassLoader.class.getClassLoader());
    }

    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }
}
  • 通过修改 Class 文件的常量池,替换符号引用(将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串),将 java.lang.System 替换成自定义的 HackSystem 类;避免了临时代码在编写是无法引入 HackSystem,或者修改影响到原程序标准输出的问题;
package edu.aurelius.jvm.classloading;

/**
 * 修改 Class 文件的常量池
 *
 * @author Aurelius-Shu
 * @since 2023-02-15
 */
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常量的内容 * @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++) {
            int tag = ByteUtils.bytes2Int(classByte, offset, u1);
            if (tag == CONSTANT_Utf8_info) {
                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);
    }
}
package edu.aurelius.jvm.classloading;

/**
 * Bytes 数组处理工具
 *
 * @author Aurelius-Shu
 * @since 2023-02-15
 */
public class ByteUtils {
    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 类用于劫持 java.lang.System;替换 out,err 为 ByteArrayOutputStream,并增加读取、清理 Stream 的功能;其他全部转发给 System 处理;

package edu.aurelius.jvm.classloading;

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

/**
 * 用于劫持 java.lang.System,除了替换 out, err,其他全部直接转发给 System;
 *
 * @author Aurelius-Shu
 * @since 2023-02-15
 */
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 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 currentTimeMillis() {
        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);
    }
}

实现一个 JavaClassExecutor,用于接收临时代码,并利用上面实现的工具读取、修改、加载、执行这段代码,然后将结果通过 HackSystem 输出;

package edu.aurelius.jvm.classloading;

import java.lang.reflect.Method;

/**
 * Java Class 执行工具
 *
 * @author Aurelius-Shu
 * @since 2023-02-15
 */
public class JavaClassExecutor {
    /**
     * 执行代表一个 Java 类的 byte[] 数组
     *
     * @param classByte 代表一个 Java 类的 byte[] 数组
     * @return 执行结果
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier cm = new ClassModifier(classByte);
        byte[] modifiedBytes = cm.modifyUTF8Constant("java/lang/System", "edu/aurelius/jvm/classloading/HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modifiedBytes);
        try {
            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();
    }
}

4. 验证

任意写一个含有 System.out 标准输出的 TestClass 类,编译后放到服务器端指定路径下(也可以调用本地编译工具编译后直接通过 Class 文件文本转化生成 byte[] 数组);创建一个测试 JSP 页面,用浏览器访问,可见 TestClass 的运行结果;

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
    InputStream is = new FileInputStream("c:/TestClass.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>");
%>

参考资料:

  • [1]《深入理解 Java 虚拟机》