在排查问题的过程中,想查看内存中的一些参数值,却又没有方法把这些值输出到界面或日志中,又或者定位到某个缓存数据有问题,但缺少缓存的统一管理界面,不得不重启服务才能清理这个缓存。类似的需求有一个共同的特点,那就是只要在服务中执行一段程序代码,就可以定位或排除问题,但就是偏偏找不到可以让服务器执行临时代码的途径,这时候就会希望Java服务器中也有提供类似Groovy Console的功能。

JDK1.6之后提供了Compiler API,可以动态地编译Java程序,虽然这样达不到动态语言的灵活度,但让服务器执行临时代码的需求就可以得到解决了。在JDK1.6之前,也可以通过其他方式来做到,譬如写一个JSP文件上传到服务器,然后在浏览器中运行它,或者在服务端程序中加入一个 Bean Shell Script、 JavaScript等的执行引擎(如Mozilla rhino)去执行动态脚本。我们将使用前面学到的关于类加载及虚拟机执行子系统的知识去实现在服务端执行临时代码的功能。

1、目标

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

  • 不依赖JK版本,能在目前还普遍使用的JDK中部署,也就是使用JDK1.4~JDK1.7都可以运行。
  • 不改变原有服务端程序的部署,不依赖任何第三方类库。
  • 不侵入原有程序,即无须改动原程序的任何代码,也不会对原有程序的运行带来任何影响。
  • 考到 Bean Shell Script或 JavaScript等脚本编写起来不太方便,“临时代码”需要直接支持Java语言。
  • “临时代码”应当具备足够的自由度,不需要依赖特定的类或实现特定的接口。这里写的是“不需要”而不是“不可以”,当“临时代码”需要引用其他类库时也没有限制,只要服务端程序能使用的,临时代码应当都能直接引用。
  • “临时代码”的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及抛出的异常等。

看完上面列出的目标,你觉得完成这个需求需要做多少工作呢?也许答案比大多数人所想的都要简单一些:5个类,250行代码(含注释),大约一个半小时左右的开发时间就可以了,现在就开始编写程序吧!

2、思路

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

  • 如何编译提交到服务器的Java代码?
  • 如何执行编译之后的Java代码?
  • 如何收集Java代码的执行结果?

对于第一个问题,我们有两种思路可以选择,一种是使用 tools.jar包(在Sun JDK/lib目录下)中的 com.sun.tools.javac.Main类来编译Java文件,这其实和使用 Javac命令编译是样的。这种思路的缺点是引入了额外的JAR包,而且把程序“绑死”在Sun的JDK上了,要部署到其他公司的JDK中还得把 tools.jar带上(虽然JRockit和J9虚拟机也有这个JAR包,但它总不是标准所规定必须存在的)。另外一种思路是直接在客户端编译好,把字节码而不是Java代码传到服务端。

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

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

3、实现

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

/*** 为了多次载入执行类而加入的加载器* 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法* 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行加载* zww*/
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[]数组转变为Clas对象。 HotSwapClassLoader中并没有重写 loadclass或 findClass()方法,因此如果不算外部手玉调用 loadByte()方法的话,这个类加载器的类查找范围与它的父类加载器是完全一致的,在被虚拟桃调用时,它会按照双亲委派模型交给父类加载。构造函数中指定为加载 HotSwapClassloader类的类加载器作为父类加载器,这一步是实现提交的执行代码可以访问服务端引用类库的关键。

第二个类是实现将 java.lang.System替换为我们已定义的 HackSystem类的过程,它直接修改符合Class文件格式的byte数组中的常量池部分,将常量池中指定内容的CONSTANT_Utf8_info常量替换为新的字符串,具体代码如下所示。ClassModifier中涉及对byte数组操作的部分,主要是将byte与int和String互相转换,以及把对byte数据的替换操作封装在代码ByteUtils中。

/*** 修改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;}public byte[] modifyUTF8Constant(String oldStr, String newStr) {int cpc = getConstantPoolCount();   //常量的数量int offset = CONSTANT_POOL_COUNT_INDEX + u2;    //CONSTANT_POOL 起始位置for (int i = 0; i < cpc; i++) {int tag = ByteUtils.bytes2Int(classByte, offset, u1);   //获取常量型if (tag == CONSTANT_Utf8_info) {    //判断常量型类型是否是CONSTANT_Utf8_infoint 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*/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;for (int i = start; i < end; i++) {// 因为当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,// 就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位// 如果b[i]为负数时:例如:10000001 & 11111111  ==》 1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001int 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) {// ||        |~offset|    ~len   ||      ||byte[] newBytes = new byte[originalBytes.length - len + replaceBytes.length];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 处理*/
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();}//下面所有的方法都与 java.lang.System 的名称一样//实现都是字节调System的对应方法//因版面原因,省略其他方法}

至此,4个支持类已经讲解完毕,我们来看看最后一个类 JavaClassExecuter,它是提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作。 JavaClassExecuter只有一个 execute()方法,用输入的符合Class件格式的byte数组替换 java. lang System的
符号引用后,使用 HotSwapClassLoader加载生成一个 Class对象,由于每次执行 execute方法都会生成一个新的类加载器实例,因此同一全类以实现重复加载。然后,反射调用这个Class对象的 main()方法,如果期间出现任何异常,将异常信息打印到 HackSystem.out中最后把缓冲区中的信息作为方法的结果返回。 JavaClassExecuter的实现代码如下。

/*** JavaClass 执行工具*/
public class JavaClassExecuter {/*** 执行外部传过来的代表一个Java类的byte数组* 将输入类byte数组中代表 java.lang.System的CONTANT_Utf8_info常量修改为劫持后的HackSystem类* 执行方法为该类的 main 方法,输出结构为该类向System.out/err输出的信息* @param classByte* @return*/public static String execute(byte[] classByte) {HackSystem.clearBuffer();ClassModifier classModifier = new ClassModifier(classByte);//修改Class字节码,把HackSystem 替代 Systembyte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "org.swift.framework.RemotePlugin.HackSystem");HotSwapClassLoader loader = new HotSwapClassLoader();Class clazz = loader.loadByte(modiBytes);try {//调用其main方法Method method = clazz.getMethod("main", new Class[] { String[].class});method.invoke(null, new String[] {null});} catch (Exception e) {e.printStackTrace(HackSystem.out);}return HackSystem.getBufferString();}}

4、验证

远程执行功能的编码到此就完成了,接下来就要检验一下我们的劳动成果了。如果只是测试的话,那么可以任意写一个Java类,内容无所谓,只要向 System.out输出信息即可,取名为 TestClass,同时放到服务器C盘的根目录中。然后,建立一个JSP文件并加入如下代码,就可以在浏览器中看到这个类的运行结果了。

public class TestClass {public static void main(String[] args) {System.out.println("this is a test class");}
}
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.swift.framework.RemotePlugin.*" %>
<%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>");
%>

自己动手实现远程执行功能相关推荐

  1. java远程执行功能_Java远程连接Linux服务器并执行命令及上传文件功能

    Java远程连接Linux服务器并执行命令及上传文件功能 发布于 2020-3-6| 复制链接 摘记:  最近再开发中遇到需要将文件上传到Linux服务器上,至此整理代码笔记.此种连接方法中有考虑到并 ...

  2. java调用子系统代码_深入理解JAVA虚拟机-Idea远程执行本地Java代码 - Java 技术驿站-Java 技术驿站...

    今天在看深入理解JAVA虚拟机的9.3节,作者实现了一个远程执行功能.这个功能可以在远程服务器中临时执行一段程序代码,而去不依赖jdk版本,不改变原有服务端程序的部署,不依赖任何第三方库,不入侵原有的 ...

  3. 【原创】c#如何实现RTU远程数据采集功能及RTU在水利工程中的运用

    好久没有动手去写博客了,近两年时间忙碌着工作,未曾回过头细数做过的路程,感觉有点思想颓废了,之前一直从事的水利工程类开发及实施工作,考虑各方面情况下,从11年开始转行投入到电力智能电网的软件开发工作, ...

  4. c#如何实现RTU远程数据采集功能及RTU在水利工程中的运用

    好久没有动手去写博客了,近两年时间忙碌着工作,未曾回过头细数做过的路程,感觉有点思想颓废了,之前一直从事的水利工程类开发及实施工作,考虑各方面情况下,从11年开始转行投入到电力智能电网的软件开发工作, ...

  5. SSH 远程执行任务

    SSH 是 Linux 下进行远程连接的基本工具,但是如果仅仅用它来登录那可是太浪费啦!SSH 命令可是完成远程操作的神器啊,借助它我们可以把很多的远程操作自动化掉!下面就对 SSH 的远程操作功能进 ...

  6. 远程恢复服务器,Hyper-V主机启用“远程桌面”功能

    运行Hyper-V的远程计算机必须启用"远程桌面"功能,并授权用户访问权限,授权 后的用户即可通过"远程桌面"工具连接到远程计算机.下面以Windows Ser ...

  7. 结合批处理功能,配置SQL Server 2005,使其打开远程连接功能

    参考微软这篇 如何配置 SQL Server 2005 以允许远程连接的文章,http://support.microsoft.com/kb/914277#top 我结合批命令,可以实现一站式配置 S ...

  8. python执行bat文件_Python中调用PowerShell、远程执行bat文件实例

    python调用本地powershell方法 1.现在准备一个简陋的powershell脚本,功能是测试一个IP列表哪些可以ping通: function test_ping($iplist) { f ...

  9. wordpress漏洞_WordPress XSS漏洞可能导致远程执行代码(RCE)

    原作者: Ziyahan Albeniz 在2019年3月13日,专注于静态代码分析软件的RIPS科技公司发布了他们在所有版本的WordPress 5.1.1中发现的跨站点脚本(XSS)漏洞的详细信息 ...

最新文章

  1. python备份cisco交换机_1.自动备份思科交换机配置
  2. 整合SharePoint MOSS 和SQL Server 2005 reporting service(一)
  3. 国二c语言考试分值,计算机二级C语言题型和评分标准
  4. OpenShift 4 之 Image Registry、Image 和 ImageStream 概念和相关操作
  5. ~~单链表(数据结构)
  6. 【codevs5037】线段树练习4加强版
  7. Jenkinsfile脚本实现master、slave节点(agent)共享内容
  8. AutoCAD 2019 mac中文
  9. 运放放大倍数计算公式_运算放大器基本电路大全(转)
  10. Android样式系列:自定义按钮样式
  11. excel编程系列基础:认识VBA的编辑器VBE
  12. Sequence and Swaps
  13. 保护站点上已存在另一个具有相同实例 UUID的虚拟机_化合物晶形专利权利要求的表征及保护范围探讨...
  14. 最短路算法整理 七七八八的总结
  15. 杭州端点网络java开发实习生笔试题自我反省
  16. 【samba】Ubuntu samba的安装及使用方法
  17. 28.深入浅出MYSQL的优化
  18. 深入浅出谈人脸识别技术
  19. 对话Neo4j首席科学家Jim Webber:图数据库江湖5年后将尘埃落定
  20. 笔试归来,若有所悟(转)

热门文章

  1. 基于组态王和三菱PLC的modbus仿真(七)——RS指令
  2. Matlab 批量保存图片
  3. 规则引擎drools系列(一)
  4. lo linux 环回端口,本地环回接口lo The Loopback Network Interface lo--用Enki学Linux系列(2)...
  5. 【转】基于C#的接口基础教程之五(1)
  6. 2022 GopherChina大会紧急通知!
  7. 浑身尖刺的服务可用性守护者——hystrix熔断器实践记录
  8. Studio3t 过期激活办法/以及重新设置使用日期的脚本不可用解决办法/Studio 3T无限激活原创
  9. 10.26 node.js day01
  10. Python-玩转数据-python网络编程