目标
首先 在实现“在服务端执行临时代码”这个需求之前 先来明确一下本次实战的具体目标 我们希望最终的产品是这样的:
不依赖JDK版本 能在目前还普遍使用的JDK中部署 也就是使用JDK 1.4~JDK 1.7都可以运行
不改变原有服务端程序的部署 不依赖任何第三方类库
不侵入原有程序 即无须改动原程序的任何代码 也不会对原有程序的运行带来任何影响
考虑到BeanShell Script或JavaScript等脚本编写起来不太方便 “临时代码”需要直接支持Java语言
“临时代码”应当具备足够的自由度 不需要依赖特定的类或实现特定的接口 这里写的是“不需要”而不是“不可以” 当“临时代码”需要引用其他类库时也没有限制 只要服务端程序能使用的 临时代码应当都能直接引用
“临时代码”的执行结果能返回到客户端 执行结果可以包括程序中输出的信息及抛出的异常等
实现
/**
* 为了多次载入执行类而加入的加载器<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);
}
}
HotSwapClassLoader所做的事情仅仅是公开父类(即java.lang.ClassLoader)中的protected方法defineClass() 我们将会使用这个方法把提交执行的Java类的byte[]数组转变为Class对象 HotSwapClassLoader中并没有重写loadClass()或findClass()方法 因此如果不算外部手工调用loadByte()方法的话 这个类加载器的类查找范围与它的父类加载器是完全一致的 在被虚拟机调用时 它会按照双亲委派模型交给父类加载 构造函数中指定为加载HotSwapClassLoader类的类加载器作为父类加载器 这一步是实现提交的执行代码可以访问服务端引用类库的关键
第二个类是实现将java.lang.System替换为我们自己定义的HackSystem类的过程 它直接修改符合Class文件格式的byte[]数组中的常量池部分 将常量池中指定内容的CONSTANT_Utf8_info常量替换为新的字符串
ClassModifier中涉及对byte[]数组操作的部分 主要是将byte[]与int和String互相转换 以及把对byte[]数据的替换操作封装在ByteUtils中
/**
* 修改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);
}
}
/**
* 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;
}
}
经过ClassModifier处理后的byte[]数组才会传给HotSwapClassLoader.loadByte()方法进行类加载 byte[]数组在这里替换符号引用之后 与客户端直接在Java代码中引用HackSystem类再编译生成的Class是完全一样的 这样的实现既避免了客户端编写临时执行代码时要依赖特定的类(不然无法引入HackSystem) 又避免了服务端修改标准输出后影响到其他程序的输出
最后一个类就是前面提到过的用来代替java.lang.System的HackSystem 这个类中的方法看起来不少 但其实除了把out和err两个静态变量改成使用ByteArrayOutputStream作为打印目标的同一个PrintStream对象 以及增加了读取 清理ByteArrayOutputStream中内容的getBufferString()和clearBuffer()方法外 就再没有其他新鲜的内容了 其余的方法全部都来自于System类的public方法 方法名字 参数 返回值都完全一样 并且实现也是直接转调了System类的对应方法而已 保留这些方法的目的 是为了在Sytem被替换成HackSystem之后 执行代码中调用的System的其余方法仍然可以继续使用 HackSystem的实现如下
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的对应方法
// 因版面原因,省略了其他方法
}
我们来看看最后一个类JavaClassExecuter 它是提供给外部调用的入口 调用前面几个支持类组装逻辑 完成类加载工作 JavaClassExecuter只有一个execute()方法 用输入的符合Class文件格式的byte[]数组替java.lang.System的符号引用后 使用HotSwapClassLoader加载生成一个Class对象 由于每次执行execute()方法都会生成一个新的类加载器实例 因此同一个类可以实现重复加载 然后 反射调用这个Class对象的main()方法 如果期间出现任何异常 将异常信息打印到HackSystem.out中 最后把缓冲区中的信息作为方法的结果返回 JavaClassExecuter的实现代码如下
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", "自己的路径/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();
}
}
验证
如果只是测试的话 那么可以任意写一个Java类 内容无所谓 只要向System.out输出信息即可 取名为TestClass 同时放到服务器C盘的根目录中 然后 建立一个JSP文件并加入如下所示的内容,就可以在浏览器中看到这个类的运行结果了
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="自己的路径.*" %>
<%
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>");
%>