在阅读《深入理解Java虚拟机》第二版,一书中。第九章看到的demo。
这个小东西实现的功能,就是让服务器可以在运行时期动态的执行代码,打印日志等, 而不需要修改服务器代码,重启服务器。一开始以为是比较高深的东西,但是实际上,确实不算很难。
书中提供了 5 个类用于动态执行代码。使用了一个 JSP 文件来作为触发执行操作的入口,并且获取程序的执行结果。需要说明的是这5个类以及这个 jsp 文件都是需要放在项目的代码中的。
jsp 文件中的内容就是代用那5个类的代码,去加载临时文件,执行其中的代码,然后将执行的结果显示到页面上
具体执行步骤就是
- 本地编写好临时代码并且编译成 class 文件
- 将编译好的 class 文件上传到服务器上的指定位置
- 访问 jsp 页面,执行代码并且返回代码的执行结果
这个服务器上的指定位置是在 jsp 文件中指定的,这个 jsp 文件就是一个触发加显示的作用,实际上你用一个接口去实现也是可以的。然后这个我觉得是不适合放在生产环境中的,比较有风险。
放上代码:
package com.java.test.load;
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 byte2String(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 com.java.test.load;
/**
* 修改class 文件,暂时只提供修改常量池常量的功能
*/
public class ClassModifier {
/**
* 常量池的起始偏移
*/
private static final int CONSTANT_POLL_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 = getConstantPollCount();
int offset = CONSTANT_POLL_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.byte2String(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;
}
/**
* 获取常量池中常量的数量
*/
public int getConstantPollCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POLL_COUNT_INDEX, u2);
}
}
package com.java.test.load;
import java.io.*;
import java.nio.channels.Channel;
import java.util.Properties;
/**
* 为JavaClass 劫持 java.lang.System 提供支持
* 除了 out 和 err 外,其余的都直接转发给System 处理
*/
public class HackSystem {
public final static InputStream in = System.in;
private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public static final 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);
}
public static void setIn(InputStream in) {
System.setIn(in);
}
public static void setOut(PrintStream out) {
System.setOut(out);
}
public static void setErr(PrintStream err) {
System.setErr(err);
}
public static Console console() {
return System.console();
}
public static Channel inheritedChannel() throws IOException {
return System.inheritedChannel();
}
public static long nanoTime() {
return System.nanoTime();
}
public static Properties getProperties() {
return System.getProperties();
}
public static String lineSeparator() {
return System.lineSeparator();
}
public static void setProperties(Properties props) {
System.setProperties(props);
}
public static String getProperty(String key) {
return System.getProperty(key);
}
public static String getProperty(String key, String def) {
return System.getProperty(key, def);
}
public static String setProperty(String key, String value) {
return System.setProperty(key, value);
}
public static String clearProperty(String key) {
return System.clearProperty(key);
}
public static String getenv(String name) {
return System.getenv(name);
}
public static java.util.Map<String, String> getenv() {
return System.getenv();
}
public static void exit(int status) {
System.exit(status);
}
public static void gc() {
System.gc();
}
public static void runFinalization() {
System.runFinalization();
}
public static void runFinalizersOnExit(boolean value) {
System.runFinalizersOnExit(value);
}
public static void load(String filename) {
System.load(filename);
}
public static void loadLibrary(String libname) {
System.loadLibrary(libname);
}
public static String mapLibraryName(String libname) {
return System.mapLibraryName(libname);
}
}
package com.java.test.load;
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
}
package com.java.test.load;
import java.lang.reflect.Method;
/**
* JavaClass 执行工具
*/
public class JavaClassExecuter {
/**
* 执行外部传过来的代表一个Java类的byte 数组
* 将输入类的byte数组中代表 java.lang.System 的 CONSTANT_Utf8_info 常量修改为劫持后的 HackSystem类
* 执行方法为该类的 static main(String[] args) 方法,输出结果为该类向 System.out/err 输出的信息
*
* @param classByte 代表一个 Java 类的数组
* @return 执行结果
*/
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "com/java/test/load/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();
}
return HackSystem.getBufferString();
}
}
那个 jsp 文件的代码就不放上来了,因为我在写的时候是用 spring boot 提供了一个接口,下面放下这个接口:
@GetMapping("/execute")
public String execute() throws IOException {
FileInputStream fileInputStream = new FileInputStream("D://Test.class");
byte[] bytes = new byte[fileInputStream.available()];
fileInputStream.read(bytes);
fileInputStream.close();
return JavaClassExecuter.execute(bytes);
}