博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
字节码执行引擎-类加载及执行子系统的案例与实战
阅读量:6280 次
发布时间:2019-06-22

本文共 8089 字,大约阅读时间需要 26 分钟。

  hot3.png

自己动手实现远程执行功能

##目标

首先 ,在实现“在服务端执行临时代码”这个需求之前,先来明确一下本次实战的具体目标 ,我们希望最终的产品是这样的:

  • 不依赖JDK版本,能在目前还普遍使用的JDK中部署,也就是使用JDK1.4〜JDK1.7都可以运行。
  • 不改变原有服务端程序的部署,不依赖任何第三方类库。 -不侵入原有程序,即无须改动原程序的任何代码,也不会对原有程序的运行带来任何影响。 -考到BeanShell Script或JavaScript等脚本编写起来不太方便,“临时代码”需要直接支持Java语言。 -“临时代码”应当具备足够的自由度,不需要依赖特定的类或实现特定的接口。这里写的 是“不需要”而不是“不可以” ,当“临时代码”需要引用其他类库时也没有限制,只要服务端程序能使用的,临时代码应当都能直接引用。
  • “临时代码” 的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及拋出的异常等。 看完上面列出的目标,你觉得完成这个需求需要做多少工作呢?也许答案比大多数人所想的都要简单一些:5个类 ,250行代码(含注释),大约一个半小时左右的开发时间就可以了 ,现在就开始编写程序吧!

思路

在程序实现的过程中,我们需要解决以下3个问题:

  • 如何编译提交到服务器的Java代码?
  • 如何执行编译之后的Java代码?
  • 如何收集Java代码的执行结果?
  • 对于第一个问题,我们有两种思路可以选择,一种是使用tools.jar包 (在SunJDK/lib目录下)中的com.sun.tools.javac.Main类来编译Java文件,这其实和使用Javac命令编译是一样的。 这种思路的缺点是引入了额外的JAR包 ,而且把程序“綁死”在Sun的JDK上了,要部署到其他公司的JDK中还得把tools.jar带上(虽然JRockit和J9虚拟机也有这个JAR包,但它总不是标准所规定必须存在的)。另外一种思路是直接在客户端编译好,把字节码而不是Java代码传到服务端,这听起来好像有点投机取巧,一般来说确实不应该假定客户端一定具有编译代码的能力,但是既然程序员会写Java代码去给服务端排查问题,那么很难想象他的机器上会连编译Java程序的环境都没有。

对于第二个问题,简单地一想:要执行编译后的Java代码,让类加载器加载这个类生成一个Class对象 ,然后反射调用一下某个方法就可以了(因为不实现任何接口,我们可以借用一下Java中人人皆知的“main() ”方法)。但我们还应该考虑得更周全些:一段程序往往不是编写、运行一次就能达到效果,同一个类可能要反复地修改、提交、执行。另外,提交上去的类要能访问服务端的其他类库才行。还有 ,既然提交的是临时代码,那提交的Java类在执行完后就应当能卸载和回收。

最后的一个问题,我们想把程序往标准输出(Systemout)和标准错误输出( Systemerr ) 中打印的信息收集起来,但标准输出设备是整个虚拟机进程全局共享的资源,如桌使用System.setOut()/System.setErr()方法把输出流重定向到自己定义的PrintStream对象上固然可以收集输出信息,但也会对原有程序产生影响:会把其他线程向标准输出中打印的信息也收集了。虽然这些并不是不能解决的问题,不过为了达到完全不影响原程序的目的 ,我们可以采用另外一种办法,即直接在执行的类中把对System.out的符号引用替换为我们准备的PnntStream的符号引用,依赖前面学习的知识,做到这一点并不困难。

实现

在程序实现部分,我们主要看一下代码及其注释。首先看看实现过程中需要用到的4个支持类。第一个类用于实现“同一个类的代码可以被多次加载”这个需求,即用于解决【目标】中列举的第2个问题的HotSwapClassLoader,具体程序如代码。

HotSwapClassLoader的实现

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

ClassModifier的实现

package cn.zvc.classloader;/** * 修改Class文件,暂时只提供修改常量池常量的功能 */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);    }}

ByteUtils的实现

package cn.zvc.classloader;/** * 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;    }}
package cn.zvc.classloader;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.io.PrintStream;/** * 为JavaClass劫持java.lang.System提供支持 * 除了out和err外,其余的都直接转发给System处理 *  */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的对应方法    // 因版面原因,省略了其他方法}

JavaClassExecuter的实现

package cn.zvc.classloader;import java.lang.reflect.Method;/** * JavaClass执行工具 * * @author zzm */public class JavaClassExecuter {    /**     * 执行外部传过来的代表一个Java类的Byte数组
* 将输入类的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", "org/fenixsoft/classloading/execute/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(); }}

验证

<%@ 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(""); %>

转载于:https://my.oschina.net/zvc/blog/824842

你可能感兴趣的文章
[SilverLight]DataGrid实现批量输入(like Excel)(补充)
查看>>
秋式广告杀手:广告拦截原理与杀手组织
查看>>
翻译 | 摆脱浏览器限制的JavaScript
查看>>
闲扯下午引爆乌云社区“盗窃”乌云币事件
查看>>
02@在类的头文件中尽量少引入其他头文件
查看>>
JAVA IO BIO NIO AIO
查看>>
input checkbox 复选框大小修改
查看>>
网吧维护工具
查看>>
BOOT.INI文件参数
查看>>
vmstat详解
查看>>
新年第一镖
查看>>
unbtu使用笔记
查看>>
OEA 中 WPF 树型表格虚拟化设计方案
查看>>
Android程序开发初级教程(一) 开始 Hello Android
查看>>
使用Gradle打RPM包
查看>>
“我意识到”的意义
查看>>
淘宝天猫上新辅助工具-新品填表
查看>>
再学 GDI+[43]: 文本输出 - 获取已安装的字体列表
查看>>
nginx反向代理
查看>>
操作系统真实的虚拟内存是什么样的(一)
查看>>