@Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码
一、前言
这篇算是类加载器的实战第五篇,前面几篇在这里,后续会持续写这方面的一些东西。
实战分析Tomcat的类加载器结构(使用Eclipse MAT验证)
了不得,我可能发现了Jar 包冲突的秘密进入正文,不知道你有没有这样的时候,在线上或者测试环境,报了个bug。这个 bug 可能是:
- 从数据库、redis取了些数据,做了一些运算后,没抛异常,但是就是结果不对
- 抛了个空指针异常,但是看代码,感觉没问题,是取出来就是空,还是中间什么函数把它改坏了
- 发现导致一个bug的原因是用了JVM缓存,但是怎么清理呢?难道重启?
- redis 数据不对,能不能悄咪咪重新拉一下
- 好想把某个全局变量打出来看一下?好想执行一个数据库查询,看看他么的结果对不对?
- 。。。
哎,程序员的世界,从来没有容易二字。 说实话,我们这次要开的后门就是做上面这些事情的,我刚鼓捣出这个时,我感觉这个还挺shock,为啥大佬们不去弄呢,后来我偶然想到,在 周志明大佬的那本 《Java 虚拟机:JVM高级特性与最佳实践》书里,提到过类似的解决思路。就在书的 9.3 节,如下图,这里就提到了类似的需求,就是要在不停服务情况下,动态执行代码,方案其实一直都有:将自己的调试代码写到JSP里,丢到服务器上,然后访问该JSP。
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/ total 9336 drwxr-xr-x. 14 root root 4096 Jun 19 11:39 docs drwxr-xr-x. 6 root root 4096 Jun 19 11:39 examples drwxr-xr-x. 5 root root 4096 Jun 19 11:39 host-manager drwxr-xr-x. 5 root root 4096 Jun 19 11:39 manager drwxr-xr-x. 4 root root 4096 Jun 19 13:48 remotedebug -rw-r--r--. 1 root root 9531510 Jun 19 13:47 remotedebug.war drwxr-xr-x. 3 root root 4096 Jun 19 11:39 ROOT
我们在本地写好一个测试文件,(可以直接在 工程 里面写,这样才方便引用工程的类,不然还要自己敲 import 路径,那也太傻了),写好后,右键 执行下 main,触发编译操作。
执行main,肯定会报错,这是不用说的,但我们只需要 class 而已:
我们去 target 目录下,找到编译出来的 class,然后用 接口工具调用,如下:
下面我们看看执行结果:
然后我再改下测试类的debug方法:
public void debug(){ IRedisCacheService bean = SpringContextUtils.getBean(IRedisCacheService.class); String value = bean.getCount("123456789"); log.info("value:{}", value ); }
再次执行:
三、源码解析
代码我放在交友网站了,欢迎fork。
https://github.com/cctvckl/remotedebug
类结构如下:
我们重点分析 remoteDebugByUploadFile :
/** * 远程debug,读取参数中的class文件的路径,然后加载,并执行其中的方法 */ @RequestMapping("/remoteDebugByUploadFile.do") @ResponseBody public String remoteDebugByUploadFile(@RequestParam String className, @RequestParam String methodName, MultipartFile file) throws Exception { if (className == null || file == null || methodName == null) { throw new RuntimeException("className,file,methodName must be set"); } /** * 获取当前类加载器,当前类肯定是放在webapp的web-inf下的classes,这个类所以是由 webappclassloader 加载的,所以这里获取的就是这个 */ ClassLoader webappClassloader = this.getClass().getClassLoader(); log.info("webappClassloader:{}",webappClassloader); /** * 用自定义类加载器,加载参数中指定的class文件,并执行其方法 */ log.info("开始执行:{}中的方法:{}",className,methodName); InputStream inputStream = file.getInputStream(); UploadFileStreamClassLoader myClassLoader = new UploadFileStreamClassLoader(inputStream, className, webappClassloader); Class<?> myDebugClass = myClassLoader.loadClass(className); MyReflectionUtils.invokeMethodOfClass(myDebugClass, methodName); log.info("结束执行:{}中的方法:{}",className,methodName); return "success"; }
其中,14,15行,主要获取当前的 webappclassloader 加载器,该加载器,通俗来讲,就是加载应用目录下的 web-inf/lib 和 web-inf/classes。 21 行,主要获取文件流; 22行,将流、要加载的class的类名、webappclassloader 作为参数,来生成 自定义的类加载器,其中 webappclassloader 将作为 我们自定义类加载器的 双亲加载器。 23行,用自定义类加载器加载我们的类; 24行,用加载类反射,生成对象,并执行 methodName指定的方法。
重点代码在 UploadFileStreamClassLoader,我们看一下:
package com.remotedebug.utils; import lombok.extern.slf4j.Slf4j; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; /** * desc: * * @author : caokunliang * creat_date: 2019/6/13 0013 * creat_time: 10:19 **/ @Slf4j public class UploadFileStreamClassLoader extends ClassLoader { /** * 要加载的class的类名 */ private String className; /** * 要加载的调试class的流,可以通过客户端文件上传,也可以通过传递url来获取 */ private InputStream inputStream; /** * * @param inputStream 要加载的class 的文件流 * @param className 类名 * @param parentWebappClassLoader 父类加载器 */ public UploadFileStreamClassLoader(InputStream inputStream, String className, ClassLoader parentWebappClassLoader) { super(parentWebappClassLoader); this.className = className; this.inputStream = inputStream; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { 46 byte[] data = getData(); try { String s = new String(data, "utf-8"); // log.info("class content:{}",s); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } 54 return defineClass(className,data,0,data.length); } private byte[] getData(){ try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] bytes = new byte[2048]; int num = 0; 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); } } }
重点关注 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; @Slf4j public class RemoteDebugTest { public void debug(){ IRedisCacheService bean = SpringContextUtils.getBean(IRedisCacheService.class); String value = bean.getCount("123456789"); log.info("value:{}", value ); } }
在加载这些类时,我们自定义的类加载器会先委托给父类加载器加载,而且,我们自定义的类加载器自身也加载不了这些类。这里有个关键点在于,我们为什么要把 应用的当前类加载器传入作为自定义加载器的父加载器呢,因为不同类加载器加载出来的 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文件,要支持文件上传,需要进行以下配置:
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="utf-8" /> <property name="maxUploadSize" value="10485760000" /> <property name="maxInMemorySize" value="40960" /> </bean>
同时,我这边的环境不知道为啥,还需要修改web.xml(我们其他项目中都没配这个,尴尬):
<servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:/remotedebug-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <multipart-config> <location></location> <max-file-size>20848820</max-file-size> <max-request-size>418018841</max-request-size> <file-size-threshold>1048576</file-size-threshold> </multipart-config> </servlet>
-
remoteDebugByURL @RequestParam String className,@RequestParam String url, @RequestParam String methodName
该接口,可接受一个网络url,从url 去加载指定的class。
五、总结
一开始没想鼓捣这个,只是后边学了类加载器后,感觉是不是可以利用其来做点什么,于是想到了这个。因为热替换,是不可能在同一个类加载器实例中重复加载同一个类的,所以目前的热替换都是连根拔起,将类加载器一起换掉。在 web 应用中,web-inf下的classes和lib 都由唯一的一个类加载器加载,要替换其中的单个类,暂时没想到什么办法,但是我就感觉,可以用一个单独的类加载器去加载指定的一个位置(不同于 web-inf的位置),然后每次不用这个类,就把加载器一起丢了就行。然后一开始不知道可行,直到做出来试了后,发现确实没有问题,理论上也能解释。 后来,我在和同事讨论的过程中,感觉我做的这个东西,和JSP很像,然后又想到 在周志明的那本书里,好像有过类似的案例,去看了下,果然如此。。。
哈哈,好吧,我还以为是很新鲜的东西,原来大佬早就玩过了,JSP更是出现了不知道多少年了,只是以前没怎么玩过JSP。
这个方法,也是适用于 spring boot 的,只是需要稍微修改一下,后续我再稍微改改,发个spring boot 的版本出来。类加载器这个东西还是挺有用,后续我会继续更新这方面的文章,包括 SPI、osgi(皮毛),各类框架中 类加载器的应用等,也希望和大家多多交流,共同交流才能一起进步嘛。
源码再发一下,在这里: https://github.com/cctvckl/remotedebug
不同于之前的文章,这次排版改了下,比如字体变大了,有些段落换了颜色,大家觉得比默认的好看还是不好看?
- JAVA与.NET的相互调用——通过Web服务实现相互调用(附原代码)
- JAVA程序变更自动重载而不重启服务之JAVAREBEL
- tomcat+java的web程序持续占cpu问题调试
- 正常调试一个web项目的java代码
- windows 任务计划程序执行 bat ,重启服务
- java程序员第八课 tomcat与web程序结构与Http协议
- 将Java程序打包成可执行文件jar包,然后执行jar包,不引用外部包的情况
- JAVA与.NET的相互调用——通过Web服务实现相互调用(附原代码)
- 如何用 C 注册 windows 服务程序 (分析 Java Service Wrapper 代码)
- tomcat+java的web程序持续占cpu问题调试
- tomcat+java的web程序持续占cpu问题调试
- JDK扩展DCEVM让WEB程序完全不重启调试
- 【Java】finally代码块不被执行的情况总结
- windows + myeclipse 调试 linux + tomcat 的java web服务 配置方式
- 在web页面上放了一些服务器按钮,在各个按钮的单击事件中都有代码!可不知道哪里出问题了,怎么操作都不能触发这些事件,好象代码一点都不执行!根本没办法调试!请高手指点一二!谢谢!
- JavaWeb体系结构的理解-9.程序调试与发布
- 使用axis2插件来生成gsoap发布的Web服务的java客户端代码
- 【Java】—— java Web 启动时自动执行代码的几种方式(总有些代码需要在虚拟机启动时执行)
- VS2013 调试x64程序报"Windows Web服务框架错误"问题
- Java程序导致服务死机的情况