前言

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

@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文件,我感觉我是暂时搞不出来的。多亏了有这么多优秀的前辈,我们才能走得更远。

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

@Spring Boot程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码的更多相关文章

  1. @Java web程序员,在保留现场,服务不重启的情况下,执行我们的调试代码(JSP 方式)

    一.前言 类加载器实战系列的第六篇(悄悄跟你说,这篇比较水),前面5篇在这里: 实战分析Tomcat的类加载器结构(使用Eclipse MAT验证) 还是Tomcat,关于类加载器的趣味实验 了不得, ...

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

    一.前言 这篇算是类加载器的实战第五篇,前面几篇在这里,后续会持续写这方面的一些东西. 实战分析Tomcat的类加载器结构(使用Eclipse MAT验证) 还是Tomcat,关于类加载器的趣味实验 ...

  3. 【腾讯Bugly经验分享】程序员的成长离不开哪些软技能?

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ce8068d4d44a246f72baf2 Dev Club 是一个交流移动 ...

  4. PHP笔记——java程序员看懂PHP程序

    PHP笔记——java程序员看懂PHP程序   php是一种服务器端脚本语言,类型松散的语言. <?php   ?>       xml风格 <script language=”ph ...

  5. 从士兵到程序员再到SOHO程序员 (二)

    原文地址: http://blog.huhao.name/blog/2013/12/13/become-a-freelancer-2/ 作者:胡皓 Blog:From Soldier to Progr ...

  6. 从士兵到程序员再到 SOHO 程序员 (三) - 游击战与阻力

    从士兵到程序员再到 SOHO 程序员 (三) - 游击战与阻力 原文地址:http://blog.huhao.name/blog/2014/03/01/become-a-freelancer-3/ 作 ...

  7. 一个程序员对微信小程序的看法

      我们公司用两周的时间开发了一款微信小程序,叫<如e支付>,大家可以去体验一下.由于接口都是写好的,所以开发起来很快.我将从4个不同的角度来介绍我对微信小程序的理解. 1.技术的角度   ...

  8. Spring Boot入门教程1、使用Spring Boot构建第一个Web应用程序

    一.前言 什么是Spring Boot?Spring Boot就是一个让你使用Spring构建应用时减少配置的一个框架.约定优于配置,一定程度上提高了开发效率.https://zhuanlan.zhi ...

  9. Spring Boot后端+Vue前端+微信小程序,完整的开源解决方案!

    项目简介 一个小商场系统,包括: 后端:Spring Boot 管理员前端:Vue 用户前端:微信小程序 功能介绍 1.小商城 首页 专题列表.专题详情 分类列表.分类详情 品牌列表.品牌详情 新品首 ...

随机推荐

  1. Spring Boot 整合 Dubbo和Zookeeper

    Spring Boot 整合 Dubbo和Zookeeper Spring Boot 整合 Dubbo和Zookeeper 环境介绍 Zookeeper 安装 启动 Dubbo admin 搭建 创建 ...

  2. idea使用maven在install时跳过测试

    在右边1处点开maven面板,然后选中2,会发现test已经×掉了,再进行install时将跳过test

  3. go 模板详说

    模板是我们常用的手段用于动态生成页面,或者用于代码生成器的编写等.比如把数据库的表映射成go语言的struct,这些体力活,写个代码生成器是最合适不过的了. 示例例把表转成 struct : 当然这篇 ...

  4. java23种设计模式——泡MM版

    一.创建型模式 1.FACTORY:追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,固然口味有所不同,但不管你带MM往麦当劳或肯德基,只管向服务员说;来四个鸡翅就行了.麦当劳和肯德 ...

  5. python学习之break和continue在for循环中的使用(案例:打印出10以内的偶数,并且只要前三个偶数)

    运行程序,break是整个程序都跳出 continue则表示跳过当前一次循环,然后继续执行循环

  6. 3-JVM垃圾回收算法和垃圾收集器

    垃圾回收算法和垃圾收集器 1.什么是垃圾回收 对于内存当中无用的对象进行回收,如何去判断一个对象是不是无用的对象. 引用计数法: 每个对象中都会存储一个引用计数,每增加一个引用就+1,消失一个引用就- ...

  7. Nginx|构建简单的文件服务器(mac) 续-FastDFS安装(mac)|文件存储方案

    目录 Nginx|构建简单的文件服务器(mac) 1 所需安装包 2 安装fastdfs-nginx-module-master 3 安装Nginx Nginx|构建简单的文件服务器(mac) 续上文 ...

  8. Spring整合Durid数据源

    Spring整合Durid数据源 一.导入依赖 <!-- Druid数据源 --> <dependency> <groupId>com.alibaba</gr ...

  9. SpringData:关联查询

    一.查询方式 1.导航式查询 使用“对象.属性”   进行查询:对于多的查询, 默认就是延迟加载,添加注解@Transactional 在OneToMany 注解中需要添加属性   fetch:值:F ...

  10. sqli-labs之Page-3

    第三十八关:堆叠注入 $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; /* execute multi query */ ...