1.有什么用?
在服务器程序运行的过程中排查问题,需要执行某段程序来查询程序的参数等。
这种临时的执行代码的需求,且不能影响应用的正常运行,不能对应用的代码有任何的侵入,利用该文章的内容你就能实现
2.如何实现
我觉得最大的问题是如何执行我们的临时代码,而且不能停止服务器。
这种情况肯定是需要我们的应用存在某种机制才能在运行时去加载我们指定的代码进行执行,我们可以在应用开发阶段实现该机制,方便在需要的时候用来加载临时代码执行。
但是其实已经有了一个非常天然的东西,那就是JSP,我们可以将我们要执行的代码写在JSP中,以此来实现代码的执行。
如果执行代码简单的话,用这个方式执行没有什么问题,如果需要执行的代码比较复杂,加载的类特别多,就不太适合了。临时代码中被加载的类对于我们的应用程序来说没什么用,但是他加载了又不能被卸载,这会导致内存泄漏(虽然影响极小)。因为我们将代码写在了JSP中,tomcat中加载jsp文件生成的class的是JasperClassLoader(每个JasperClassLoader实例只负责加载一个JSP文件编译出来的class,这主要是为了实现JSP的热替换功能),我们JSP中写的java代码依赖的类是由JsperClassLoader的父类加载器WebAppClassLoader加载的。一个Class对象要想被GC,除非其类加载器被GC,因为WebAppClassLoader的生命周期是一个web应用的开始到卸载,所以说我们利用在JSP里面写的代码加载的类,就不会被卸载,导致内存泄漏。
还有可能的是,万一加载上去的类与应用上的类存在冲突怎么办?
综合以上,其实核心就是,我们应该将我们加载的临时代码的类,和web应用隔离开来,形成一个执行子系统。怎么隔离?这就要用到类加载器了,我们可以自定义自己的类加载器。在每次要执行临时代码的时候,使用自定义类加载器来加载临时代码,每次执行都创建一个新的类加载器实例,这样临时代码的类执行完毕之后能正常的被GC,且也不会与web应用中的类产生冲突
3.代码实现
临时代码执行器,用该类来实现我们临时代码的加载执行
public class JavaClassExecutor {
public static String execute(byte[] classBytes) {
HackSystem.clearBuffer();
ClassModifier modifier = new ClassModifier(classBytes);
byte[] bytes = modifier.modifyUTF8Constant("java/lang/System", "com/HackSystem");
HotSwapClassLoader classLoader = new HotSwapClassLoader();
classLoader.getParent();
// 使用自定义类加载器来加载我们要执行的类,因为这样有利于我们类的卸载,我们远程执行的类对于我们的服务器来说是无用的,所以要将其卸载
// 要想使我们一个被加载的类卸载有几个必要条件,其中就一个条件:必须要加载他的类加载器被GC
Class<?> aClass = classLoader.defineClass("Test", bytes);
try {
// 执行加载的类的方法
Method method = aClass.getMethod("invoke");
method.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {}
return HackSystem.getBufferString();
}
}
自定义类加载器,这里并没有实现一个完整的类加载器
没有去实现findClass()方法,没有去打破双亲委派机制。
也就是说,没有实现我们上述的思路那样的类加载器,并不能实现一个独立的执行子系统
该类加载器加载类的时候依然会使用WebAppClassLoader来进行加载
感兴趣的可以自己实现一个完整的
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
// 因为HotSwapClassLoader是由WebAppClassLoader加载的
// 所以这里就是将我们的类加载器的parent设置为了WebAppClassLoader
super(HotSwapClassLoader.class.getClassLoader());
}
/**
* 暴露出defineClass方法
*/
public Class<?> defineClass(String name, byte[] bytes) {
return super.defineClass(name, bytes, 0, bytes.length);
}
}
为了收集我们临时代码的执行结果,我们必须要想办法,当然我们可以通过日志来实现。
深入JVM虚拟机中提供了一种新的思路,其实我觉得这种方式并不是很方便,但是也可以算是对写代码思路的一种扩展。
书上利用对class文件的修改来实现执行结果的输出,我们在临时代码中可以直接利用System.out或者System.err来进行输出,然后修改Class文件,将Class文件中对System类的引用修改为我们自定义的输出类,我们可以在自定义类中将输出输出到任何地方。
前两个类主要是实现class文件的修改相关的
public class ClassModifier {
/**
* Class文件中常量池的起始偏移
*/
private static final int CONSTANT_POOL_COUNT_INDEX = 8;
/**
* Constant_utf_8常量的tag标志
*/
private static final int CONSTANT_UTF8_INFO = 1;
/**
* 常量池中11种常量所占长度,CONSTANT_UTF-8_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;
}
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.string2byte(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;
}
private int getConstantPoolCount() {
// 常量池最开始位置存储了数组的下标
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
public class ByteUtils {
public static int bytes2Int(byte[] b, int start, int len) {
int sum = 0;
int end = start + len;
if (len > 4) {
throw new IllegalArgumentException();
}
for (int i = start; i < end; i++) {
int n = ((int) b[i]) & 0xff;
sum += n << (--len) * 8;
}
return sum;
}
public static byte[] int2Bytes(int value, int len) {
byte[] bytes = new byte[len];
for (int i = len - 1; i >= 0; i--) {
bytes[i] = (byte) (value & 0xff);
value >>>= 8;
}
return bytes;
}
public static String bytes2String(byte[] b, int start, int len) {
return new String(b, start, len);
}
public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
byte[] bytes = new byte[originalBytes.length + replaceBytes.length - len];
System.arraycopy(originalBytes, 0, bytes, 0, offset);
System.arraycopy(replaceBytes, 0, bytes, offset, replaceBytes.length);
System.arraycopy(originalBytes, offset + len, bytes, offset + replaceBytes.length, originalBytes.length - offset - len);
return bytes;
}
public static byte[] string2byte(String newStr) {
return newStr.getBytes();
}
}
自定义的用来替换System的输出类
public class HackSystem {
public final static InputStream in = System.in;
private static final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public final static PrintStream out = new PrintStream(buffer);
public static final PrintStream err = out;
public static String getBufferString() {
return buffer.toString();
}
public static void clearBuffer() {
buffer.reset();
}
/**
* 余下方法未实现
* 如果使用了System的其他方法就需要实现
* 否则会出现方法找不到的异常
*/
}
需要远程执行的代码
public class Test {
public static void invoke() {
System.out.println("远程调用被执行了");
}
}
JSP文件
<%
FileInputStream fileInputStream = new FileInputStream("target//classes//Test.class");
byte[] b = new byte[fileInputStream.available()];
fileInputStream.read(b);
fileInputStream.close();
String execute = JavaClassExecutor.execute(b);
System.out.println(execute);
%>