一 需求场景
当服务器上发生了bug,但是却因为某一两处日志没有打印,而最终无法定位到具体问题时,是很烦恼的,远程执行可以在不修改服务器程序的前提下为程序提供动态增强的功能,打印出自己想要的信息。
二 在程序实现的过程中,我们需要解决以下3个问题
·如何编译提交到服务器的Java代码?
·如何执行编译之后的Java代码?
·如何收集Java代码的执行结果?
第一个问题的解决:
需要自己实现一个上传文件的接口,供编译好的class文件上传至服务器,这个class文件就是最终被执行的文件,这个文件可以为你提供你想要的服务输出能力。
第二个问题的解决:
让类加载器加载这个类生成一个Class对象,然后反射调用一下某个方法就可以了。
第二个问题的解决:
这里采用直接输出到日志的方式。
三 实现
1.HotSwapClassLoader.java
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类的类加载器作为父类加载器,这一步是实现提交的执行代码可以访问服务端引用类库的关键.
2.HackSystem.java
public class HackSystem { public final static InputStream in = System.in; public 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替换为我们自己定义的HackSystem类的过程,它直接修改符合Class文件格式的byte[]数组中的常量池部分,将常量池中指定内容的CONSTANT_Utf8_info常量替换为新的字符串,具体代码如下面的代码清[3.ClassModifier.java]所示。ClassModifier中涉及对byte[]数组操作的部分,主要是将byte[]与int和String互相转换,以及把对byte[]数据的替换操作封装在代码清单[4.ByteUtils.java]所示的ByteUtils中
3.ClassModifier.java
public class ClassModifier { private static final int CONSTANT_POOL_COUNT_INDEX = 8; private static final int CONSTANT_Utf8_info = 1; private static final int[] CONSTANT_ITEM_LEGTH = {-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.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_LEGTH[tag]; } } return classByte; } public int getConstantPoolcount(){ return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX,u2); } }
经过ClassModifier处理后的byte[]数组才会传给HotSwapClassLoader.loadByte()方法进行类加载,byte[]数组在这里替换符号引用之后,与客户端直接在Java代码中引用HackSystem类再编译生成的Class是完全一样的。这样的实现既避免了客户端编写临时执行代码时要依赖特定的类(不然无法引入HackSystem),又避免了服务端修改标准输出后影响到其他程序的输出.
4.ByteUtils.java
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; } }
4.JavaclassExecuter.java
4个支持类已经讲解完毕,我们来看看最后一个类JavaclassExecuter,它是提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作。JavaclassExecuter只有一个execute()方法,用输入的符合Class文件格式的byte[]数组替换掉java.lang.System的符号引用后,使用HotSwapClassLoader加载生成一个Class对象,由于每次执行execute()方法都会生成一个新的类加载器实例,因此同一个类可以实现重复加载。然后反射调用这个Class对象的main()方法,如果期间出现任何异常,将异常信息打
印到HackSystem.out中,最后把缓冲区中的信息作为方法的结果来返回。JavaclassExecuter的实现代码
如下所示:
public class JavaclassExecuter { public static String execute(byte[] classByte){ HackSystem.clearBuffer(); ClassModifier cm = new ClassModifier(classByte); byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System","sgcc/supplier/service/front/shops/controller/remoteexecute/tmppak/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 (Exception e) { e.printStackTrace(); } return HackSystem.getBufferString(); } }
四 验证
验证方式一:
任意写一个Java类,内容无所谓,只要向System.out输出信息即可,取名为TestClass,放到服务器C盘的根目录中。然后建立一个JSP文件写上如下所示的内容,就可以在浏览器中看到这个类的运行结果了,该种验证需要建立一个普通的web工程,将4个支持类以及一个jsp代码放入工程中,jsp代码直接放入index.jsp即可。上面是用于测试TestClass.java类信息以及jsp的信息。
public class TestClass{ public static void main(String[] args){ System.out.println("这是第一句。。。"); System.out.println("这是第二句。。。"); System.out.println("这是第三句。。。"); } }
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.lang.*" %> <%@ page import="java.io.*" %> <%@ page import="com.esgcc.*" %><%--这里传入的是4个支持类的路径--%> <% 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>"); %>
启动工程可以看到页面输出:
验证方式二:
第一种是将信息输出到了页面中,我们也可以将信息直接输出到控制台,这里只需要建立一个任意java工程,将4个支持类和下面的代码放进去尽可以,实现代码其实大同小异:
public class MainEntrance { public MainEntrance() { } public static void main(String[] args) { try { InputStream is = new FileInputStream("c:/TestClass.class"); byte[] b = new byte[is.available()]; is.read(b); is.close(); JavaclassExecuter.execute(b); } catch (IOException var3) { var3.printStackTrace(); } } }
执行上面的main方法即可得到输出:
验证方式三:
这里是使用SpringBoot中新增一个接口,使用接口触发执行,将如上4个支持类代码上传到服务器,同时上传被调用的TestClass.class到opt目录下,接口实现代码如下:。
代码如下:
@Slf4j @RestController @RequestMapping("/remote") public class RemoteExecuteController { /** * @param * @return */ @PostMapping(value = "/execute") @ApiOperation(value = "远程", notes = "远程") @ApiImplicitParams({ @ApiImplicitParam(name = "jsonParams", value = "参数集合: \n" + "{\n" + "}", required = true, dataTypeClass = String.class, paramType = "query"), @ApiImplicitParam(name = "userToken", value = "token", required = true, dataTypeClass = String.class, paramType = "header") }) public String remoteExecute(String jsonParams) { try { log.info("::::::::::::::::::::::进入执行接口::::::::::::::::::::::"); InputStream is = new FileInputStream("/opt/TestClass.class"); byte[] b = new byte[is.available()]; is.read(b); is.close(); log.info("::::::::::::::::::::::远程执行日志打印开始::::::::::::::::::::::"); log.info(JavaclassExecuter.execute(b)); log.info("::::::::::::::::::::::远程执行日志打印结束::::::::::::::::::::::"); } catch (IOException e) { e.printStackTrace(); log.error(e.getMessage()); } return "success"; } }
通过postman调用接口后查看日志,有输出:
五 总结
这里只是一个展示,并未在TestClass类中真正去调用想要调用的方法,只是提供了一种框架,希望能带给看到这些代码的朋友一个启发。
参考资料:
《深入理解java虚拟机-第3版》