zoukankan      html  css  js  c++  java
  • @Spring Boot程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码

    前言

    这篇其实是对一年前的一篇文章的补坑。

    @Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码

    当时,就是在spring mvc应用里定义一个api,然后api里,进行如下定义:

        /**
         * 远程debug,读取参数中的class文件的路径,然后加载,并执行其中的方法
         */
        @RequestMapping("/remoteDebugByUploadFile.do")
        @ResponseBody
        public String remoteDebugByUploadFile(@RequestParam String className, @RequestParam String methodName, MultipartFile file)
    

    大家看上面的注释,就是读取文件流,这个文件流里包含了我们要远程执行的代码;className和methodName,分别指定这个文件的类名和debug方法的方法名。

    如果大家看得一脸懵的话,也没关系,下面我基于此次改版升级后的应用给大家举个例子。

    假设我有下面这样一个controller。

        @Autowired
        private IRedisCacheService iRedisCacheService;
    
        /**
         * 缓存获取接口
         * @param cacheKey
         */
        @RequestMapping("getCache.do")
        public String getCache(@RequestParam String  cacheKey){
            String value = iRedisCacheService.getCache(cacheKey);
            System.out.println(value);
    
            return value;
        }
    

    里面就是调用了一个IRedisCacheService的getCache方法。

    结果,上面这个api的结果不符预期,然后我们看看上面的这个getCache的实现。

    
    
        /**
         * desc:
         *
         * @author : xxx
         * creat_date: 2019/6/18 0018
         * creat_time: 10:17
         **/
        @Service
        @Slf4j
        public class IRedisCacheServiceImpl implements IRedisCacheService {
            Random random = new Random();
        
            @Override
            public String getCache(String cacheKey) {
                String target = null;
                // 1 
                String count = getCount(cacheKey);
                // ----------------------后面有复杂逻辑--------------------------
                if (Integer.parseInt(count) > 1){
                    target = "abc";
                }else {
                    // 一些业务逻辑,但是忘记给 target 赋值
                    // .....
                }
        
                return target.trim();
            }
        
            @Override
            public String getCount(String cacheKey){
                // 假设是从redis 读取缓存,这里简单起见,假设value的值就是cacheKey
                return String.valueOf(random.nextInt(20));
            }
        }
        
    
    
    
    

    这里的1处,调用了另一个方法getCount,因为getCount没有日志,也没有打印getCount的返回值。问题可能是getCount返回的不对,也可能是后续的逻辑,把这个返回值改了。现在要排查问题,怎么办呢?

    本地调试?麻烦。本地环境和测试环境也不一样,本地能不能重现问题,都是个问题。

    大家可以使用阿里出的arthas,但我们这里采用另一种方法。

    写个调试文件:

    package com.learn;
    
    import com.remotedebug.service.IRedisCacheService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    
    public class TempDebug {
        public static final Logger log = LoggerFactory.getLogger(TempDebug.class);
    	
      	// 1
        @Autowired
        private IRedisCacheService bean;
    
    	// 2
        public void debug() {
            String count = bean.getCount("-2");
            // 3
            log.info("result:{}", count);
        }
    
    }
    
    • 1处,注入了一个bean,我们需要调用这个bean的getCount
    • 2处,我们定义了一个debug方法,里面调用了bean.getCount("-2"),这里的-2这个参数,我是随便传的,这个不重要。我们希望,把这个代码丢到服务器上去执行,然后看3处打印出来的日志,不就可以判断,getCount这一步是否出错了吗?

    所以,大家明白了我们要做的事情没?

    写一个调试文件(文件里尽量只是查看操作,如果要做那种对数据库、缓存进行修改的话,要慎重一点,代码写稳一点),传到服务端的api,api执行这段代码。然后,我们可以查看服务端的日志,来帮助我们排查问题。

    效果展示:

    api中大致的步骤

    • 编译上传来的debug用途的java文件为class文件,获取其class文件的字节数组
    • 定义一个类加载器,从我们第一步拿到的class文件的字节数组中,加载为一个class
    • 对class进行反射,创建出对象
    • (可选)对对象中的field进行注入(如果field上定义了autowired注解)
    • 调用对象的指定方法,如前面的例子,就是调用debug方法

    步骤1:编译java文件为class文件

    这篇文章,之所以等了这么久,就是一年前,那时候只能上传class文件;当时就想过直接上传java,服务端自动编译,奈何技术问题没搞定,所以后来就拖着了。

    这次是怎么搞定了编译问题呢?差不多是直接拷贝了阿里的arthas代码中的相关的几个文件,只要有以下几个步骤,具体请大家克隆源码查看。

    • new 一个 com.taobao.arthas.compiler.DynamicCompiler

      DynamicCompiler dynamicCompiler = new DynamicCompiler(this.getClass().getClassLoader());
      

    • 添加要编译的类的源码

      String javaSource;
      try {
        javaSource = IOUtils.toString(inputStream, Charset.defaultCharset());
      }
      dynamicCompiler.addSource(className, javaSource);
      
    • 编译

      Map<String, byte[]> byteCodes = dynamicCompiler.buildByteCodes();
      

      这个返回的map,key就是类名,value就是class文件的字节码数组。

    步骤2:定义一个类加载器,加载为Class对象

    大家再仔细看看我们的debug代码:

        @Autowired
        private IRedisCacheService bean;
    
    
        public void debug() {
            String count = bean.getCount("-2");
            log.info("result:{}", count);
        }
    

    这里面,是用到了我们的应用中的类的,比如上面这个bean。这个bean,在spring boot里,假设是由类加载器A加载的,那我们加载我们这段debug代码,应该怎么加载呢?还是用类加载器A?

    ok,没问题。类加载器A,加载了我们的TempDebug这个类。那,假设我改动了一点代码:

        public void debug() {
          	//1 xxxxxx
          	....
            String count = bean.getCount("-2");
            log.info("result:{}", count);
        }
    

    这里1处,改了点代码,再次debug,那么,类加载器A还能加载我们的类吗?不能,因为已经缓存了这个类了,不会再次加载。

    所以,我们干脆定义一个一次性的类加载器,每次用了就丢。我这里的方法,就是定义一个类加载器A的child。所谓的child,就是符合双亲委派,这个类加载器,除了加载我们的bug类,其他的类,全部丢给parent。

        public UploadFileStreamClassLoader(InputStream inputStream, String className, ClassLoader parentWebappClassLoader) {
            super(parentWebappClassLoader);
            this.className = className;
            // 1
            this.inputStream = inputStream;
        }
    
        @Override
        protected Class<?> findClass(String name)  {
            // 2
            byte[] data = getData();
            // 4
            return defineClass(className,data,0,data.length);
        }
    
        private byte[] getData(){
            try {
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] bytes = new byte[2048];
                int num = 0;
                // 3
                while ((num = inputStream.read(bytes)) != -1){
                    byteArrayOutputStream.write(bytes, 0,num);
                }
    
                return byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                log.error("read stream failed.{}",e);
                throw new RuntimeException(e);
            }
        }
    
    • 1处,把前面编译好的class的字节数组流,传进来
    • 2处,重载了findClass,所以,我们是符合双亲委派的,这里,直接去getData,也就是获取字节流数组
    • 3处,调用defineClass,生成Class对象。

    上面类加载器好了,基本的代码就有了:

            /**
             * 新建一个classloader,该classloader的parent,为当前线程的classloader
             */
            InputStream inputStream = new ByteArrayInputStream(compiledClassByteArray);
            UploadFileStreamClassLoader myClassLoader = new UploadFileStreamClassLoader(inputStream, className, classloader);
            Class<?> myDebugClass = null;
            try {
                myDebugClass = myClassLoader.loadClass(className);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
    

    步骤3:反射class,生成对象

            /**
             * 新建对象
             */
            Object debugClassInstance;
            try {
                debugClassInstance = myDebugClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
    

    步骤4:对autowired field,注入bean

    我们的service中,实现了ApplicationContextAware接口,让框架给我们注入了:

    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
            this.applicationContext = applicationContext;
        }
    

    获取要注入的字段

    		/**
             * 查看对象中的@autowired字段,注入值
             */
            Field[] declaredFields = myDebugClass.getDeclaredFields();
            Set<Field> set = null;
            if (declaredFields != null) {
                set = Arrays.stream(declaredFields)
                        .filter(f -> f.isAnnotationPresent(Autowired.class))
                        .collect(Collectors.toSet());
            }
    

    注入字段

            /**
             * 注入字段
             */
            try {
                log.info("start to inject fields set:{}",set);
                for (Field field : set) {
                    Class<?> fieldClass = field.getType();
                    Object bean = applicationContext.getBean(fieldClass);
                    field.setAccessible(true);
                    field.set(debugClassInstance,bean);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    

    步骤5:万事俱备,只欠东风

    我们这一步很简单,调用就行了。

            try {
                myDebugClass.getMethod(methodName).invoke(debugClassInstance);
            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
    
    		log.info("结束执行:{}中的方法:{}", className, methodName);
    

    完整代码

    https://gitee.com/ckl111/remotedebug

    总结

    感谢arthas,不然的话,编译java为class文件,我感觉我是暂时搞不出来的。多亏了有这么多优秀的前辈,我们才能走得更远。

    大家如有问题,可加群讨论。

  • 相关阅读:
    React Native 安卓 程序运行报错: React Native version mismatch(转载)
    RN用蓝牙接入热敏打印机和智能电子秤(转载)
    安装加密用包
    React Native 调用 Web3(1.x) 的正确姿势
    Unable to resolve module crypto
    点击<tr>表格行元素进行跳转
    Phonegap环境配置
    登录记住密码功能的实现
    php+sqlserver实现分页效果
    php日期格式转换
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/12988410.html
Copyright © 2011-2022 走看看