这篇实际上是对一年前的一篇文章的补坑。html
@Java Web 程序员,咱们一块儿给程序开个后门吧:让你在保留现场,服务不重启的状况下,执行咱们的调试代码java
当时,就是在spring mvc应用里定义一个api,而后api里,进行以下定义:git
/** * 远程debug,读取参数中的class文件的路径,而后加载,并执行其中的方法 */ @RequestMapping("/remoteDebugByUploadFile.do") @ResponseBody public String remoteDebugByUploadFile(@RequestParam String className, @RequestParam String methodName, MultipartFile file)
你们看上面的注释,就是读取文件流,这个文件流里包含了咱们要远程执行的代码;className和methodName,分别指定这个文件的类名和debug方法的方法名。程序员
若是你们看得一脸懵的话,也不要紧,下面我基于这次改版升级后的应用给你们举个例子。redis
假设我有下面这样一个controller。spring
@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的实现。api
/** * 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); } }
bean.getCount("-2")
,这里的-2这个参数,我是随便传的,这个不重要。咱们但愿,把这个代码丢到服务器上去执行,而后看3处打印出来的日志,不就能够判断,getCount这一步是否出错了吗?因此,你们明白了咱们要作的事情没?
写一个调试文件(文件里尽可能只是查看操做,若是要作那种对数据库、缓存进行修改的话,要慎重一点,代码写稳一点),传到服务端的api,api执行这段代码。而后,咱们能够查看服务端的日志,来帮助咱们排查问题。
这篇文章,之因此等了这么久,就是一年前,那时候只能上传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文件的字节码数组。
你们再仔细看看咱们的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); } }
上面类加载器好了,基本的代码就有了:
/** * 新建一个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); }
/** * 新建对象 */ Object debugClassInstance; try { debugClassInstance = myDebugClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); }
咱们的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(); }
咱们这一步很简单,调用就好了。
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文件,我感受我是暂时搞不出来的。多亏了有这么多优秀的前辈,咱们才能走得更远。
你们若有问题,可加群讨论。