java手动实现远程执行

简介: 当服务器上发生了bug,但是却因为某一两处日志没有打印,而最终无法定位到具体问题时,是很烦恼的,远程执行可以在不修改服务器程序的前提下为程序提供动态增强的功能,打印出自己想要的信息。

一 需求场景



当服务器上发生了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>");
%>


启动工程可以看到页面输出:


20210115141722810.png


验证方式二:


第一种是将信息输出到了页面中,我们也可以将信息直接输出到控制台,这里只需要建立一个任意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方法即可得到输出:


20210115141944331.png


验证方式三:


这里是使用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调用接口后查看日志,有输出:

20210115142728957.png


五 总结



这里只是一个展示,并未在TestClass类中真正去调用想要调用的方法,只是提供了一种框架,希望能带给看到这些代码的朋友一个启发。


参考资料:



《深入理解java虚拟机-第3版》

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
Java
Java 实现汉字按照首字母分组排序
Java 实现汉字按照首字母分组排序
556 0
|
分布式计算 Java Hadoop
Java实现单词计数MapReduce
本文分享实现单词计数MapReduce的方法
301 0
|
Java 数据安全/隐私保护
JAVA 实现上传图片添加水印(详细版)(上)
JAVA 实现上传图片添加水印(详细版)
921 0
JAVA 实现上传图片添加水印(详细版)(上)
|
存储 IDE Java
命令行下的Java包结构编译与执行
本文介绍命令行下的Java包结构编译与执行方法
124 0
命令行下的Java包结构编译与执行
|
存储 Java
Java实现图书管理系统
本篇文章是对目前Java专栏已有内容的一个总结练习,希望各位小主们在学习完面向对象的知识后,可以阅览本篇文章后,自己也动手实现一个这样的demo来加深总结应用已经学到知识并进行巩固。
373 0
Java实现图书管理系统
|
Java Windows Spring
java实现spring boot项目启动时,重启Windows进程
java实现spring boot项目启动时,重启Windows进程
474 0
|
数据可视化 Java
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
如果要在某一个界面里面添加功能的话,都在一个类中,会显得代码难以阅读,而且修改起来也会很困难,所以我们将游戏主界面、登录界面、以及注册界面都单独编成一个类,每一个类都继承JFrame父类,并且在类中创建方法来来实现页面
456 0
Java实现拼图小游戏(1)—— JFrame的认识及界面搭建
|
网络协议 Java
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
ip地址的分类: 1、ipv4、ipv6 127.0.0.1:4个字节组成,0-255,42亿;30亿都在北美,亚洲就只有4亿 2011年就用尽了。
Java网络编程:UDP/TCP实现实时聊天、上传图片、下载资源等
|
数据可视化 Java 容器
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
注意由于我们计步功能的步数要在重写方法中用到,所以不能将初始化语句写在方法体内,而是要写在成员位置。在其名字的时候也要做到“见名知意”,所以我们给它起名字为step
263 0
Java实现拼图小游戏(7)—— 计步功能及菜单业务的实现
|
Java
Java实现拼图小游戏(7)—— 作弊码和判断胜利
当我们好不容易把拼图复原了,但是一点提示也没有,完全看不出来是成功了,那么我们就需要有判断胜利的功能去弹出“成功”类的图片,以便于玩家选择是重新开始还是退出小游戏
237 0
Java实现拼图小游戏(7)—— 作弊码和判断胜利