前言
这篇其实是对一年前的一篇文章的补坑。
@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程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码的更多相关文章
-
@Java web程序员,在保留现场,服务不重启的情况下,执行我们的调试代码(JSP 方式)
一.前言 类加载器实战系列的第六篇(悄悄跟你说,这篇比较水),前面5篇在这里: 实战分析Tomcat的类加载器结构(使用Eclipse MAT验证) 还是Tomcat,关于类加载器的趣味实验 了不得, ...
-
@Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码
一.前言 这篇算是类加载器的实战第五篇,前面几篇在这里,后续会持续写这方面的一些东西. 实战分析Tomcat的类加载器结构(使用Eclipse MAT验证) 还是Tomcat,关于类加载器的趣味实验 ...
-
【腾讯Bugly经验分享】程序员的成长离不开哪些软技能?
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ce8068d4d44a246f72baf2 Dev Club 是一个交流移动 ...
-
PHP笔记——java程序员看懂PHP程序
PHP笔记——java程序员看懂PHP程序 php是一种服务器端脚本语言,类型松散的语言. <?php ?> xml风格 <script language=”ph ...
-
从士兵到程序员再到SOHO程序员 (二)
原文地址: http://blog.huhao.name/blog/2013/12/13/become-a-freelancer-2/ 作者:胡皓 Blog:From Soldier to Progr ...
-
从士兵到程序员再到 SOHO 程序员 (三) - 游击战与阻力
从士兵到程序员再到 SOHO 程序员 (三) - 游击战与阻力 原文地址:http://blog.huhao.name/blog/2014/03/01/become-a-freelancer-3/ 作 ...
-
一个程序员对微信小程序的看法
我们公司用两周的时间开发了一款微信小程序,叫<如e支付>,大家可以去体验一下.由于接口都是写好的,所以开发起来很快.我将从4个不同的角度来介绍我对微信小程序的理解. 1.技术的角度 ...
-
Spring Boot入门教程1、使用Spring Boot构建第一个Web应用程序
一.前言 什么是Spring Boot?Spring Boot就是一个让你使用Spring构建应用时减少配置的一个框架.约定优于配置,一定程度上提高了开发效率.https://zhuanlan.zhi ...
-
Spring Boot后端+Vue前端+微信小程序,完整的开源解决方案!
项目简介 一个小商场系统,包括: 后端:Spring Boot 管理员前端:Vue 用户前端:微信小程序 功能介绍 1.小商城 首页 专题列表.专题详情 分类列表.分类详情 品牌列表.品牌详情 新品首 ...
随机推荐
-
Android 解析Json_fastJson
FastJSON是一个很好的java开源json工具类库,相比其他同类的json类库,它的速度的确是fast,最快!但是文档做得不好,在应用前不得不亲测一些功能. 实际上其他的json处理工具都和 ...
-
写在开始编写Java之前(1)——Java的跨平台性
Java语言之所以比C语言更加实用 是有原因的 Java的一个重要的特点——跨平台性 无论是哪个平台,如Windows.Linus还是Mac系统 Java的语法都是一样的 这个要比C语言用处要广 因为 ...
-
7、8上的cell上的一个按钮,当点击按钮时,要拿到这个cell,可以用代理,也可以用superview
/** cell上的付款按钮事件 */ - (IBAction)paymentButtonClick:(UIButton *)sender { /** * @author SongXing, 15-0 ...
-
20145315 《Java程序设计》实验三实验报告
实验三 敏捷开发与XP实践 实验内容 下载并学会使用git上传代码: 与同学结对,相互下载并更改对方代码,并上传: 实现代码的重载. 实验步骤 下载并用git上传代码: 1.下载并安装好git,在cm ...
-
Openstack的ping不通实例的解决办法
状态:实例在管理平台上正常创建,也能vnc到实例里面使用ifconfig,查看IP得到我们想要的IP,但是在除了计算节点以外的机器ping实例就是不通. 操作:主要为了测试网络51删除,重新创建网络5 ...
-
VB6 GDI+ 入门教程[6] 图片
http://vistaswx.com/blog/article/category/tutorial/page/2 VB6 GDI+ 入门教程[6] 图片 2009 年 6 月 19 日 15条评论 ...
-
Supervisord管理
原文地址:http://blog.csdn.net/fyh2003/article/details/6837970 学习笔记 Supervisord可以通过sudo easy_install supe ...
-
React 精要面试题讲解(五) 高阶组件真解
说明与目录 在学习本章内容之前,最好是具备react中'插槽(children)'及'组合与继承' 这两点的知识积累. 详情请参照React 精要面试题讲解(四) 组合与继承不得不说的秘密. 哦不好意 ...
-
Windbg学习笔记
下载winsdksetup.exe ,双击,选择Debugging Tools for Windows安装. 64位系统抓64位进程dump,用64位windbg来分析.64位系统抓32位进程dump ...
-
我眼中的支持向量机(SVM)
看吴恩达支持向量机的学习视频,看了好几遍,才有一点的理解,梳理一下相关知识. (1)优化目标: 支持向量机也是属于监督学习算法,先从优化目标开始. 优化目标是从Logistics regressi ...