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

本文介绍了如何在不重启服务的情况下,利用自定义类加载器动态执行调试代码,以解决生产环境中遇到的问题。通过创建一个Controller接收并执行客户端传来的类文件,实现了远程调试功能,避免了复杂逻辑排查的困难。同时,文章还探讨了JSP的类加载器思想,并提供了相关代码实现和使用说明。
摘要由CSDN通过智能技术生成

从数据库、redis取了些数据,做了一些运算后,没抛异常,但是就是结果不对

抛了个空指针异常,但是看代码,感觉没问题,是取出来就是空,还是中间什么函数把它改坏了

发现导致一个bug的原因是用了JVM缓存,但是怎么清理呢?难道重启?

redis 数据不对,能不能悄咪咪重新拉一下

好想把某个全局变量打出来看一下?好想执行一个数据库查询,看看他么的结果对不对?

。。。

哎,程序员的世界,从来没有容易二字。 说实话,我们这次要开的后门就是做上面这些事情的,我刚鼓捣出这个时,我感觉这个还挺shock,为啥大佬们不去弄呢,后来我偶然想到,在 周志明大佬的那本 《Java 虚拟机:JVM高级特性与最佳实践》书里,提到过类似的解决思路。就在书的 9.3 节,如下图,这里就提到了类似的需求,就是要在不停服务情况下,动态执行代码,方案其实一直都有:将自己的调试代码写到JSP里,丢到服务器上,然后访问该JSP。

f4ddd958798f3ff4298a8758879b8d43.png

我们要做的事情,其实有点类似JSP,比它好的地方在于:不用把文件手动丢到服务器上,直接上传class就行了。 也正是因为这次的折腾,我才知道,JSP原来还是能做很多事情的。但是笔者在毕业时,JSP应用基本就很少了,大学学了点皮毛而已,工作后更是没用到,但它的类加载器的思想还是值得我们学习的。

二、大体思路与展示

1、思路

我们的目标是,针对一个 spring mvc 开发的部署在tomcat 上的 war 包应用,不重启的情况下,动态执行一些我们的调试代码,调试代码中,只要是原项目能用的东西,我们都可以用。具体的方式是,在项目中 增加一个Controller,该Controller 的接口,主要是接收客户端传过来的调试类的 class 文件,或者去指定的 url 加载调试类的 class,然后用自定义类加载器加载该 class,new出对象,并执行我们指定的方法。

下面我先简单介绍下演示项目:

应用是 spring MVC + spring(演示用,就没有db层),内部有一个测试用的 Controller:

1 // TestController.java2

3 packagecom.remotedebug.controller;4

5 importcom.remotedebug.service.IRedisCacheService;6 importorg.springframework.beans.factory.annotation.Autowired;7 importorg.springframework.web.bind.annotation.RequestMapping;8 importorg.springframework.web.bind.annotation.RequestParam;9 importorg.springframework.web.bind.annotation.RestController;10

11 /**

12 * desc:13 * 测试接口,模拟从redis中获取缓存。当然,实际场景下,看缓存可以直接用工具的,这里就是举个栗子14 *@author: caokunliang15 * creat_date: 2019/6/18 001816 * creat_time: 10:1317 **/

18 @RestController19 public classTestController {20

21 @Autowired22 privateIRedisCacheService iRedisCacheService;23

24 /**

25 * 缓存获取接口26 *@paramcacheKey27 */

28 @RequestMapping("getCache.do")29 publicString getCache(@RequestParam String cacheKey){30 String value =iRedisCacheService.getCache(cacheKey);31 System.out.println(value);32

33 returnvalue;34 }35 }

1 //IRedisCacheServiceImpl.java

2 packagecom.remotedebug.service.impl;3

4 importcom.remotedebug.service.IRedisCacheService;5 importlombok.extern.slf4j.Slf4j;6 importorg.springframework.stereotype.Service;7

8 importjava.util.List;9

10 /**

11 * desc:12 *13 *@author: caokunliang14 * creat_date: 2019/6/18 001815 * creat_time: 10:1716 **/

17 @Service18 @Slf4j19 public class IRedisCacheServiceImpl implementsIRedisCacheService {20

21 @Override22 publicString getCache(String cacheKey) {23 String target = null;24 //----------------------前面有复杂逻辑--------------------------

25 String count =getCount(cacheKey);26 //----------------------后面有复杂逻辑,包括对 count 进行修改--------------------------

27 if (Integer.parseInt(count) > 1){28 target = "abc";29 }else{30 //一些业务逻辑,但是忘记给 target 赋值31 //.....

32 }33

34 returntarget.trim();35 }36

37 @Override38 publicString getCount(String cacheKey){39 //假设是从redis 读取缓存,这里简单起见,假设value的值就是cacheKey

40 returncacheKey;41 }42 }

注意上面的实现类,getCache 方法,就是简单地去调用了 getCount 方法,然后做了一些复杂计算,在 else 分支,我们没给  target  赋值,所以 在 34 行调用 target.trim 时会抛NPE。我们这时候排查问题时,如果能够调用 getCount 看到返回的值是多少,就好了!

知道了getCount 返回值,我们就可以接着看到底是返回的值有问题,还是是因为后面的逻辑有问题了。 常规情况下,我们是没办法的,只能肉眼看了,或者本地调试,但本地调试,取到的数据又不是真实环境的,很可能不能复现。

我们现在就可以写一段下面这样的代码,放到服务器上执行,就可以将我们需要的信息打出来了:

importcom.remotedebug.service.IRedisCacheService;importcom.remotedebug.utils.SpringContextUtils;importlombok.extern.slf4j.Slf4j;

@Slf4jpublic classRemoteDebugTest {public voiddebug(){

IRedisCacheService bean= SpringContextUtils.getBean(IRedisCacheService.class);

String value = bean.getCount("user.count.userIdxxx");

log.info("value:{}", value );

}public static voidmain(String[] args) {newRemoteDebugTest().debug();

}

}

ps:这里的 SpringContextUtils 只是一个简单的工具类,spring 容器会把自己赋值给  SpringContextUtils 中一个静态变量,方便我们在一些不被spring 管理的bean中获取 bean。

那要怎么才能让服务器执行我们的  RemoteDebugTest  的 debug 方法呢,你可能想到了,我们再加一个 Controller 就行了:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 package com.remotedebug.controller;

2

3 import com.remotedebug.utils.LocalFileSystemClassLoader;

4 import com.remotedebug.utils.MyReflectionUtils;

5 import com.remotedebug.utils.UploadFileStreamClassLoader;

6 import lombok.extern.slf4j.Slf4j;

7 import org.springframework.stereotype.Controller;

8 import org.springframework.web.bind.annotation.RequestMapping;

9 import org.springframework.web.bind.annotation.RequestParam;

10 import org.springframework.web.bind.annotation.ResponseBody;

11 import org.springframework.web.multipart.MultipartFile;

12

13 import java.io.InputStream;

14

15 /**

16 * desc:

17 * 原理:自定义类加载器,根据入参加载指定的调试类,调试类中需要引用webapp中的类,所以需要把webapp的类加载器作为parent传给自定义类加载器。

18 * 这样就可以执行 调试类中的方法,调试类中可以访问 webapp中的类,所以通过 spring 容器的静态引用来获取spring中的bean,然后就可以执行很多业务方法了。

19 * 比如获取系统的一些状态、执行service/dao bean中的方法并打印结果(如果方法是get类型的操作,则可以获取系统状态,或者模拟取redis/mysql库中的数据,如果

20 * 为update类型的service 方法,则可以用来改变系统状态,在不用重启的情况下,进行一定程度的热修复。

21 * @author : caokunliang

22 * creat_date: 2018/10/19 0019

23 * creat_time: 14:02

24 **/

25 @Controller

26 @Slf4j

27 public class RemoteDebugController {

28

29

30 /**

31 * 远程debug,读取参数中的class文件的路径,然后加载,并执行其中的方法

32 */

33 @RequestMapping("/remoteDebug.do")

34 @ResponseBody

35 public String remoteDebug(@RequestParam String className ,@RequestParam String filePath, @RequestParam String methodName) throws Exception {

36 /**

37 * 获取当前类加载器,当前类肯定是放在webapp的web-inf下的classes,这个类所以是由 webappclassloader 加载的,所以这里获取的就是这个

38 */

39 ClassLoader webappClassloader = this.getClass().getClassLoader();

40 log.info("webappClassloader:{}",webappClassloader);

41

42

43 /**

44 * 用自定义类加载器,加载参数中指定的filePath的class文件,并执行其方法

45 */

46 log.info("开始执行:{}中的方法:{}",className,methodName);

47 LocalFileSystemClassLoader localFileSystemClassLoader = new LocalFileSystemClassLoader(filePath, className, webappClassloader);

48 Class> myDebugClass = localFileSystemClassLoader.loadClass(className);

49 MyReflectionUtils.invokeMethodOfClass(myDebugClass, methodName);

50

51 log.info("结束执行:{}中的方法:{}",className,methodName);

52

53 return "success";

54

55 }

56

57

58 /**

59 * 远程debug,读取参数中的class文件的路径,然后加载,并执行其中的方法

60 */

61 @RequestMapping("/remoteDebugByUploadFile.do")

62 @ResponseBody

63 public String remoteDebugByUploadFile(@RequestParam String className, @RequestParam String methodName, MultipartFile file) throws Exception {

64 if (className == null || file == null || methodName == null) {

65 throw new RuntimeException("className,file,methodName must be set");

66 }

67

68 /**

69 * 获取当前类加载器,当前类肯定是放在webapp的web-inf下的classes,这个类所以是由 webappclassloader 加载的,所以这里获取的就是这个

70 */

71 ClassLoader webappClassloader = this.getClass().getClassLoader();

72 log.info("webappClassloader:{}",webappClassloader);

73

74 /**

75 * 用自定义类加载器,加载参数中指定的class文件,并执行其方法

76 */

77 log.info("开始执行:{}中的方法:{}",className,methodName);

78 InputStream inputStream = file.getInputStream();

79 UploadFileStreamClassLoader myClassLoader = new UploadFileStreamClassLoader(inputStream, className, webappClassloader);

80 Class> myDebugClass = myClassLoader.loadClass(className);

81 MyReflectionUtils.invokeMethodOfClass(myDebugClass, methodName);

82 log.info("结束执行:{}中的方法:{}",className,methodName);

83

84

85 return "success";

86

87 }

88

89

90 /**

91 * 远程debug,读取参数中url指定的class文件的路径,然后加载,并执行其中的方法

92 */

93 @RequestMapping("/remoteDebugByURL.do")

94 @ResponseBody

95 public String remoteDebugByURL(@RequestParam String className,@RequestParam String url, @RequestParam String methodName) throws Exception {

96 if (className == null || url == null || methodName == null) {

97 throw new RuntimeException("className,url,methodName must be set");

98 }

99

100 /**

101 * 获取当前类加载器,当前类肯定是放在webapp的web-inf下的classes,这个类所以是由 webappclassloader 加载的,所以这里获取的就是这个

102 */

103 ClassLoader webappClassloader = this.getClass().getClassLoader();

104 log.info("webappClassloader:{}",webappClassloader);

105

106 /**

107 * 用自定义类加载器,加载参数中指定的class文件,并执行其方法

108 */

109 log.info("开始执行:{}中的方法:{}",className,methodName);

110 UploadFileStreamClassLoader myClassLoader = new UploadFileStreamClassLoader(url, className, webappClassloader);

111 Class> myDebugClass = myClassLoader.loadClass(className);

112 MyReflectionUtils.invokeMethodOfClass(myDebugClass, methodName);

113 log.info("结束执行:{}中的方法:{}",className,methodName);

114

115

116 return "success";

117 }

118 }

View Code

在这个 Controller 中,一共提供了三种方式,先说最直接的,就是通过上传 class 文件,这个很简单,只要有一个接口工具(如 postman)就可以。 Controller 中会 用自定义类加载器,去加载 文件流 代表的class,然后 new出对象,调用方法就行了。

2.效果展示

我的应用部署在 192.168.19.13上,Tomcat 端口为 8081,如下:

[root@localhost apache-tomcat-8.0.41]# ll webapps/total9336drwxr-xr-x. 14 root root 4096 Jun 19 11:39docs

drwxr-xr-x. 6 root root 4096 Jun 19 11:39examples

drwxr-xr-x. 5 root root 4096 Jun 19 11:39 host-manager

drwxr-xr-x. 5 root root 4096 Jun 19 11:39manager

drwxr-xr-x. 4 root root 4096 Jun 19 13:48remotedebug-rw-r--r--. 1 root root 9531510 Jun 19 13:47remotedebug.war

drwxr-xr-x. 3 root root 4096 Jun 19 11:39 ROOT

我们在本地写好一个测试文件,(可以直接在 工程 里面写,这样才方便引用工程的类,不然还要自己敲 import 路径,那也太傻了),写好后,右键 执行下 main,触发编译操作。

执行main,肯定会报错,这是不用说的,但我们只需要 class 而已:

bfaff867179de84f72e86e431fca4178.png

我们去 target 目录下,找到编译出来的 class,然后用 接口工具调用,如下:

20e14a51abb959106a33c4addf2a6842.png

下面我们看看执行结果:

093dc450459b854da59447c640ad6dab.png

然后我再改下测试类的debug方法:

public voiddebug(){

IRedisCacheService bean= SpringContextUtils.getBean(IRedisCacheService.class);

String value= bean.getCount("123456789");

log.info("value:{}", value );

}

再次执行:

ca4137196f5fc9cbbf9c4cfe1258b915.png

三、源码解析

代码我放在交友网站了,欢迎fork。

类结构如下:

eb0eee32a3e28bbf097cb0a44c47dd4d.png

我们重点分析 remoteDebugByUploadFile :

1 /**

2 * 远程debug,读取参数中的class文件的路径,然后加载,并执行其中的方法3 */

4 @RequestMapping("/remoteDebugByUploadFile.do")5 @ResponseBody6 public String remoteDebugByUploadFile(@RequestParam String className, @RequestParam String methodName, MultipartFile file) throwsException {7 if (className == null || file == null || methodName == null) {8 throw new RuntimeException("className,file,methodName must be set");9 }10

11 /**

12 * 获取当前类加载器,当前类肯定是放在webapp的web-inf下的classes,这个类所以是由 webappclassloader 加载的,所以这里获取的就是这个13 */

14 ClassLoader webappClassloader = this.getClass().getClassLoader();15 log.info("webappClassloader:{}",webappClassloader);16

17 /**

18 * 用自定义类加载器,加载参数中指定的class文件,并执行其方法19 */

20 log.info("开始执行:{}中的方法:{}",className,methodName);21 InputStream inputStream =file.getInputStream();22 UploadFileStreamClassLoader myClassLoader = newUploadFileStreamClassLoader(inputStream, className, webappClassloader);23 Class> myDebugClass =myClassLoader.loadClass(className);24 MyReflectionUtils.invokeMethodOfClass(myDebugClass, methodName);25 log.info("结束执行:{}中的方法:{}",className,methodName);26

27

28 return "success";29

30 }

其中,14,15行,主要获取当前的 webappclassloader 加载器,该加载器,通俗来讲,就是加载应用目录下的 web-inf/lib 和 web-inf/classes。 21 行,主要获取文件流; 22行,将流、要加载的class的类名、webappclassloader 作为参数,来生成 自定义的类加载器,其中 webappclassloader 将作为 我们自定义类加载器的 双亲加载器。 23行,用自定义类加载器加载我们的类; 24行,用加载类反射,生成对象,并执行 methodName指定的方法。

重点代码在 UploadFileStreamClassLoader,我们看一下:

1 packagecom.remotedebug.utils;2

3 importlombok.extern.slf4j.Slf4j;4

5 importjava.io.ByteArrayOutputStream;6 importjava.io.IOException;7 importjava.io.InputStream;8 importjava.io.UnsupportedEncodingException;9 importjava.net.URL;10 importjava.net.URLConnection;11

12 /**

13 * desc:14 *15 *@author: caokunliang16 * creat_date: 2019/6/13 001317 * creat_time: 10:1918 **/

19 @Slf4j20 public class UploadFileStreamClassLoader extendsClassLoader {21 /**

22 * 要加载的class的类名23 */

24 privateString className;25 /**

26 * 要加载的调试class的流,可以通过客户端文件上传,也可以通过传递url来获取27 */

28 privateInputStream inputStream;29

30 /**

31 *32 *@paraminputStream 要加载的class 的文件流33 *@paramclassName 类名34 *@paramparentWebappClassLoader 父类加载器35 */

36 publicUploadFileStreamClassLoader(InputStream inputStream, String className, ClassLoader parentWebappClassLoader) {37 super(parentWebappClassLoader);38 this.className =className;39 this.inputStream =inputStream;40 }41

42

43

44 @Override45 protected Class> findClass(String name) throwsClassNotFoundException {46 byte[] data = getData();47 try{48 String s = new String(data, "utf-8");49 //log.info("class content:{}",s);

50

51 } catch(UnsupportedEncodingException e) {52 e.printStackTrace();53 }54 return defineClass(className,data,0,data.length);55 }56

57 private byte[] getData(){58 try{59 ByteArrayOutputStream byteArrayOutputStream = newByteArrayOutputStream();60 byte[] bytes = new byte[2048];61 int num = 0;62 while ((num = inputStream.read(bytes)) != -1){63 byteArrayOutputStream.write(bytes, 0,num);64 }65

66 returnbyteArrayOutputStream.toByteArray();67 } catch(Exception e) {68 log.error("read stream failed.{}",e);69 throw newRuntimeException(e);70 }71 }72 }

重点关注 46 行和 54行,46行主要是 从流中读取字节,转为字节数组; 54行主要是将字节数组代表的 class 加载到虚拟机中。另外,这里我们只覆盖了 findClass,是遵循双亲委派模型的,可以注意到,我们的测试类中,import了一些工程的类,比如:

1 import com.remotedebug.service.IRedisCacheService;

2 import com.remotedebug.utils.SpringContextUtils;

3 import lombok.extern.slf4j.Slf4j;4

5

6 @Slf4j7 public classRemoteDebugTest {8 public voiddebug(){9 IRedisCacheService bean = SpringContextUtils.getBean(IRedisCacheService.class);10 String value = bean.getCount("123456789");11

12 log.info("value:{}", value );13 }14

15 }

在加载这些类时,我们自定义的类加载器会先委托给父类加载器加载,而且,我们自定义的类加载器自身也加载不了这些类。这里有个关键点在于,我们为什么要把 应用的当前类加载器传入作为自定义加载器的父加载器呢,因为不同类加载器加载出来的 class,不能互转,所以我们必须用 同一个类加载器实例。

四、使用说明

上面详细讲述了代码实现,这里,汇总一下,我们这边一共提供了三个接口:

remoteDebug.do 参数:@RequestParam String className ,@RequestParam String filePath, @RequestParam String methodName

该接口,主要是从本地文件系统加载 filepath 指定的文件,所以这个接口,需要先把class 文件 上传到 服务器的某个路径下。

remoteDebugByUploadFile.do 参数: @RequestParam String className, @RequestParam String methodName, MultipartFile file

该接口,可以直接上传class文件,要支持文件上传,需要进行以下配置:

同时,我这边的环境不知道为啥,还需要修改web.xml(我们其他项目中都没配这个,尴尬):

DispatcherServlet

org.springframework.web.servlet.DispatcherServlet

contextConfigLocation

classpath*:/remotedebug-servlet.xml

1

20848820

418018841

1048576

remoteDebugByURL  @RequestParam String className,@RequestParam String url, @RequestParam String methodName

该接口,可接受一个网络url,从url 去加载指定的class。

8dd9fdcc75a5c7fdc7665f2b34836149.png

五、总结

一开始没想鼓捣这个,只是后边学了类加载器后,感觉是不是可以利用其来做点什么,于是想到了这个。因为热替换,是不可能在同一个类加载器实例中重复加载同一个类的,所以目前的热替换都是连根拔起,将类加载器一起换掉。在 web 应用中,web-inf下的classes和lib 都由唯一的一个类加载器加载,要替换其中的单个类,暂时没想到什么办法,但是我就感觉,可以用一个单独的类加载器去加载指定的一个位置(不同于 web-inf的位置),然后每次不用这个类,就把加载器一起丢了就行。然后一开始不知道可行,直到做出来试了后,发现确实没有问题,理论上也能解释。 后来,我在和同事讨论的过程中,感觉我做的这个东西,和JSP很像,然后又想到 在周志明的那本书里,好像有过类似的案例,去看了下,果然如此。。。

哈哈,好吧,我还以为是很新鲜的东西,原来大佬早就玩过了,JSP更是出现了不知道多少年了,只是以前没怎么玩过JSP。

这个方法,也是适用于 spring boot 的,只是需要稍微修改一下,后续我再稍微改改,发个spring boot 的版本出来。类加载器这个东西还是挺有用,后续我会继续更新这方面的文章,包括 SPI、osgi(皮毛),各类框架中 类加载器的应用等,也希望和大家多多交流,共同交流才能一起进步嘛。

不同于之前的文章,这次排版改了下,比如字体变大了,有些段落换了颜色,大家觉得比默认的好看还是不好看?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值