非阻塞的Remote Debug(一)

开发的过程中经常会用Debug去跟踪自己的或者是远程的程序,开发测试环境下倒也问题不大,但是系统上线后应该是不会让程序猿直接Debug的,因为会阻塞Server的服务。前段时间在看JVM的虚拟机,里面有非阻塞式的读取服务器的运行时数据的方法,且是无侵入式的,而且这么高大上的东西竟然只有250行代码!具体参考深入理解Java虚拟机(第二版)。

它的基本原理是这样的:

  • 动态的加载本地或者远程的Class文件,反射执行里面的main方法,main方法里用System.out.println()作为输出。

  • 修改Class文件的字节流,替换默认的java.lang.system为自定义的HackSystem,HackSystem里把默认的out对应的console替换为ByteArrayOutputStream的一个buffer,然后从这个Buffer中得到对应的输出。

  • 继承默认的ClassLoader,把protect类型的defineClass放出来,用来从字节流加载Class文件。每次new一个ClassLoader和一个Class,保证Class能够被多次重新加载和回收。

按照这种方式除了能够达到不阻塞Server外,也不会把debug的输出插入到系统的输出。当然,可以通过定义单独的log来实现类似的功能,但是可能会需要修改log配置以及重启Server。

各文件代码:


操作字节流的ByteUtils:

package com.gideon.remotedebug;
/**
 * Bytes数组处理工具
 * @author
 */
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;
    }
}

根据Class文件的数据结构解析字节流的内容,并替换默认的Java/lang/system为自定义的HackSystem

/**
 * 
 */
package com.gideon.remotedebug;
/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 * @author zzm 
 */
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);
    }
}

自定义的HackSystem,关键的地方就是把默认关联到console的out重定向到一个Buffer中,再从buffer中取数据

 package com.gideon.remotedebug;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
/**
 * 为JavaClass劫持java.lang.System提供支持
 * 除了out和err外,其余的都直接转发给System处理
 * 
 * @author zzm
 */
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);
    }
    // 下面所有的方法都与java.lang.System的名称一样
    // 实现都是字节转调System的对应方法
    // 因版面原因,省略了其他方法
}

能够从字节流加载Class文件的ClassLoader,把ClassLoader中默认访问不到的protected方法放出来

 package com.gideon.remotedebug;
/**
 * 为了多次载入执行类而加入的加载器<br>
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
 *
 * @author zzm
 */
public class HotSwapClassLoader extends ClassLoader {
    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader());
    }
    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }
}

能够多次加载并执行Class文件的JavaClassExecuter,每次执行new一个新的HotSwapClassLoader,否则不能再次加载该Class

 package com.gideon.remotedebug;
import java.lang.reflect.Method;
/**
 * JavaClass执行工具
 *
 * @author zzm
 */
public class JavaClassExecuter {
    /**
     * 执行外部传过来的代表一个Java类的Byte数组<br>
     * 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
     * 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息
     * @param classByte 代表一个Java类的Byte数组
     * @return 执行结果
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier cm = new ClassModifier(classByte);
        byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "com/gideon/remotedebug/HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);
        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();
    }
}

Server端需要能够把Class文件读取为字节数组,原书中给的一个简单的示例是在JSP文件中去读取Server端默认路径下的Class文件,原来的JSP文件改掉了,懒得再敲了  


171303_f324_206486.png

转载于:https://my.oschina.net/xhyi/blog/360805

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值