這篇實際上是對一年前的一篇文章的補坑。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文件,我感受我是暫時搞不出來的。多虧了有這麼多優秀的前輩,咱們才能走得更遠。
你們若有問題,可加羣討論。