@Spring Boot程序員,咱們一塊兒給程序開個後門吧:讓你在保留現場,服務不重啓的狀況下,執行咱們的調試代碼

前言

這篇實際上是對一年前的一篇文章的補坑。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);
    }

}
  • 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文件,我感受我是暫時搞不出來的。多虧了有這麼多優秀的前輩,咱們才能走得更遠。

你們若有問題,可加羣討論。

相關文章
相關標籤/搜索