【技巧】Java工程中的Debug信息分級輸出接口及部署模式

UPDATE: 2018.4.6

github倉庫-debug_logger已經發布,並且已經發布了一個版本的測試版jar,歡迎大家使用。如果大家喜歡的話,歡迎Star哦(^▽^)

UPDATE: 2018.4.4

筆者將考慮將這一模塊封裝成一個完整的java第三方包並可能進行開源放送,完成後將會再次發佈最新消息,敬請期待。

-------------------------分割線-------------------------

也許本文的標題你們沒咋看懂。但是,本文將帶大家領略輸出調試的威力

靈感來源

說到靈感,其實是源於筆者在修復服務器的ssh故障時的一個發現。

這個學期初,同袍(容我來一波廣告產品頁面同袍官網)原服務器出現硬件故障,於是筆者連夜更換新服務器,然而在配置ssh的時候遇到了不明原因的連接失敗。於是筆者百度了一番,發現了一些有趣的東西。

首先打開ssh的配置文件

sudo nano /etc/ssh/sshd_config

我們可以發現裏面有這麼幾行

# Logging
LogLevel DEBUG3

這個是做什麼的呢?我們再去看看ssh的日誌文件。

sudo nano /var/log/auth.log

內容如下

Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: read<=0 rfd 190 len 0
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: read failed
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: close_read
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: input open -> drain
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: ibuf empty
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: send eof
Apr  3 01:39:31 tp sshd[29439]: debug3: send packet: type 96
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 180: input drain -> closed
Apr  3 01:39:31 tp sshd[29439]: debug1: Connection to port 4096 forwarding to 0.0.0.0 port 0 requested.
Apr  3 01:39:31 tp sshd[29439]: debug2: fd 122 setting TCP_NODELAY
Apr  3 01:39:31 tp sshd[29439]: debug2: fd 122 setting O_NONBLOCK
Apr  3 01:39:31 tp sshd[29439]: debug3: fd 122 is O_NONBLOCK
Apr  3 01:39:31 tp sshd[29439]: debug1: channel 112: new [forwarded-tcpip]
Apr  3 01:39:31 tp sshd[29439]: debug3: send packet: type 90
Apr  3 01:39:31 tp sshd[29439]: debug3: receive packet: type 91
Apr  3 01:39:31 tp sshd[29439]: debug2: channel 112: open confirm rwindow 2097152 rmax 32768

可以很明顯的看到debug1debug2debug3三個關鍵詞。而當筆者將上面的LogLevel改成了DEBUG1後,debug2debug3的日誌信息就都不再被記錄。

在ssh中,Loglevel決定了日誌文件中究竟顯示什麼樣粒度的debug信息。

於是筆者靈機一動,要是這樣的模式,運用於Java工程的調試,會怎麼樣呢

功能展示

OO2018第三次作業爲例。

筆者在運行時不給程序添加命令行(默認不開啓任何DEBUG信息),然後輸入數據(綠色字爲輸入數據),輸出如下:

筆者在運行時給程序添加了命令行--debug 1(開啓一級DEBUG信息),然後輸入數據,輸出如下:

筆者在運行時給程序添加了命令行--debug 3(開啓三級DEBUG信息),然後輸入數據,輸出如下:

筆者在運行時給程序添加了命令行--debug 3 --debug_show_location(開啓三級DEBUG信息並展示DEBUG位置),然後輸入數據,輸出如下:

筆者在運行時給程序添加了命令行--debug 4 --debug_show_location --debug_package_name "models.lift"(開啓四級DEBUG信息並展示DEBUG位置,並限定只輸出models.lift包內的信息),然後輸入數據,輸出如下:

筆者在運行時給程序添加了命令行--debug 3 --debug_show_location --debug_class_name "Scheduler" --debug_include_children(開啓四級DEBUG信息並展示DEBUG位置,並限定只輸出Scheduler類和其相關子調用內的信息),然後輸入數據,輸出如下:

可以看到,筆者在自己的程序中也實現了一個類似的可調級別和範圍的debug信息系統

源碼如下(附帶簡要的命令行使用說明,Argument類爲筆者自己封裝的命令行參數管理類,如需要使用請自行封裝):

package helpers.application;

import configs.ApplicationConfig;
import exceptions.application.InvalidDebugLevel;
import exceptions.application.arguments.InvalidArgumentInfo;
import models.application.Arguments;
import models.application.HashDefaultMap;

import java.util.regex.Pattern;

/**
 * debug信息輸出幫助類
 * 使用說明:
 * -D <level>, --debug <level>              設置輸出debug信息的最大級別
 * --debug_show_location                    輸出debug信息輸出位置的文件名和行號
 * --debug_package_name <package_name>      限定輸出debug信息的包名(完整包名,支持正則表達式)
 * --debug_file_name <file_name>            限定輸出debug信息的文件名(無路徑,支持正則表達式)
 * --debug_class_name <class_name>          限定輸出debug信息的類名(不包含包名的類名,支持正則表達式)
 * --debug_method_name <method_name>        限定輸出的debug信息的方法名(支持正則表達式)
 * --debug_include_children                 輸出限定範圍內的所有子調用的debug信息(不加此命令時僅輸出限定範圍內當前層的debug信息)
 */
public abstract class DebugHelper {
    /**
     * debug level
     */
    private static int debug_level = ApplicationConfig.getDefaultDebugLevel();
    
    /**
     * show debug location
     */
    private static boolean show_debug_location = false;
    private static boolean range_include_children = false;
    
    /**
     * 範圍限制參數
     */
    private static String package_name_regex = null;
    private static String file_name_regex = null;
    private static String class_name_regex = null;
    private static String method_name_regex = null;
    
    /**
     * 設置debug level
     *
     * @param debug_level 新的debug level
     * @throws InvalidDebugLevel 非法的debug level拋出異常
     */
    private static void setDebugLevel(int debug_level) throws InvalidDebugLevel {
        if ((debug_level <= ApplicationConfig.getMaxDebugLevel()) && (debug_level >= ApplicationConfig.getMinDebugLevel())) {
            DebugHelper.debug_level = debug_level;
        } else {
            throw new InvalidDebugLevel(debug_level);
        }
    }
    
    /**
     * 設置show debug location
     *
     * @param show_debug_location show_debug_location
     */
    private static void setShowDebugLocation(boolean show_debug_location) {
        DebugHelper.show_debug_location = show_debug_location;
    }
    
    /**
     * 設置debug信息輸出範圍是否包含子調用
     *
     * @param include_children 是否包含子調用
     */
    private static void setRangeIncludeChildren(boolean include_children) {
        range_include_children = include_children;
    }
    
    /**
     * 設置包名正則篩選
     *
     * @param regex 正則表達式
     */
    private static void setPackageNameRegex(String regex) {
        package_name_regex = regex;
    }
    
    /**
     * 設置文件名正則篩選
     *
     * @param regex 正則表達式
     */
    private static void setFileNameRegex(String regex) {
        file_name_regex = regex;
    }
    
    /**
     * 設置類名正則篩選
     *
     * @param regex 正則表達式
     */
    private static void setClassNameRegex(String regex) {
        class_name_regex = regex;
    }
    
    /**
     * 設置方法正則篩選
     *
     * @param regex 正則表達式
     */
    private static void setMethodNameRegex(String regex) {
        method_name_regex = regex;
    }
    
    /**
     * 命令行參數常數
     */
    private static final String ARG_SHORT_DEBUG = "D";
    private static final String ARG_FULL_DEBUG = "debug";
    private static final String ARG_FULL_DEBUG_SHOW_LOCATION = "debug_show_location";
    private static final String ARG_FULL_DEBUG_INCLUDE_CHILDREN = "debug_include_children";
    private static final String ARG_FULL_DEBUG_PACKAGE_NAME = "debug_package_name";
    private static final String ARG_FULL_DEBUG_FILE_NAME = "debug_file_name";
    private static final String ARG_FULL_DEBUG_CLASS_NAME = "debug_class_name";
    private static final String ARG_FULL_DEBUG_METHOD_NAME = "debug_method_name";
    
    /**
     * 爲程序命令行添加相關的讀取參數
     *
     * @param arguments 命令行對象
     * @return 添加完讀取參數的命令行對象
     * @throws InvalidArgumentInfo 非法命令行異常
     */
    public static Arguments setArguments(Arguments arguments) throws InvalidArgumentInfo {
        arguments.addArgs(ARG_SHORT_DEBUG, ARG_FULL_DEBUG, true, String.valueOf(ApplicationConfig.getDefaultDebugLevel()));
        arguments.addArgs(null, ARG_FULL_DEBUG_SHOW_LOCATION, false);
        arguments.addArgs(null, ARG_FULL_DEBUG_INCLUDE_CHILDREN, false);
        arguments.addArgs(null, ARG_FULL_DEBUG_PACKAGE_NAME, true);
        arguments.addArgs(null, ARG_FULL_DEBUG_FILE_NAME, true);
        arguments.addArgs(null, ARG_FULL_DEBUG_CLASS_NAME, true);
        arguments.addArgs(null, ARG_FULL_DEBUG_METHOD_NAME, true);
        return arguments;
    }
    
    /**
     * 根據程序命令行進行DebugHelper初始化
     *
     * @param arguments 程序命令行參數解析結果
     * @throws InvalidDebugLevel DebugLevel非法
     */
    public static void setSettingsFromArguments(HashDefaultMap<String, String> arguments) throws InvalidDebugLevel {
        DebugHelper.setDebugLevel(Integer.valueOf(arguments.get(ARG_FULL_DEBUG)));
        DebugHelper.setShowDebugLocation(arguments.containsKey(ARG_FULL_DEBUG_SHOW_LOCATION));
        DebugHelper.setRangeIncludeChildren(arguments.containsKey(ARG_FULL_DEBUG_INCLUDE_CHILDREN));
        DebugHelper.setPackageNameRegex(arguments.get(ARG_FULL_DEBUG_PACKAGE_NAME));
        DebugHelper.setFileNameRegex(arguments.get(ARG_FULL_DEBUG_FILE_NAME));
        DebugHelper.setClassNameRegex(arguments.get(ARG_FULL_DEBUG_CLASS_NAME));
        DebugHelper.setMethodNameRegex(arguments.get(ARG_FULL_DEBUG_METHOD_NAME));
    }
    
    /**
     * 判斷debug level是否需要打印
     *
     * @param debug_level debug level
     * @return 是否需要打印
     */
    private static boolean isLevelValid(int debug_level) {
        return ((debug_level <= DebugHelper.debug_level) && (debug_level != ApplicationConfig.getNoDebugLevel()));
    }
    
    /**
     * 判斷棧信息是否合法
     *
     * @param trace 棧信息
     * @return 棧信息是否合法
     */
    private static boolean isTraceValid(StackTraceElement trace) {
        try {
            Class cls = Class.forName(trace.getClassName());
            String package_name = (cls.getPackage() != null) ? cls.getPackage().getName() : "";
            boolean package_name_mismatch = ((package_name_regex != null) && (!Pattern.matches(package_name_regex, package_name)));
            boolean file_name_mismatch = ((file_name_regex != null) && (!Pattern.matches(file_name_regex, trace.getFileName())));
            boolean class_name_mismatch = ((class_name_regex != null) && (!Pattern.matches(class_name_regex, cls.getSimpleName())));
            boolean method_name_mismatch = ((method_name_regex != null) && (!Pattern.matches(method_name_regex, trace.getMethodName())));
            return !(package_name_mismatch || file_name_mismatch || class_name_mismatch || method_name_mismatch);
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
    
    /**
     * 判斷棧範圍是否合法
     *
     * @return 棧範圍是否合法
     */
    private static boolean isStackValid(StackTraceElement[] trace_list) {
        for (int i = 1; i < trace_list.length; i++) {
            StackTraceElement trace = trace_list[i];
            if (isTraceValid(trace)) return true;
        }
        return false;
    }
    
    /**
     * 判斷限制範圍是否合法
     *
     * @return 限制範圍是否合法
     */
    private static boolean isRangeValid(StackTraceElement[] trace_list, StackTraceElement trace) {
        if (range_include_children)
            return isStackValid(trace_list);
        else
            return isTraceValid(trace);
    }
    
    
    /**
     * debug信息輸出
     *
     * @param debug_level debug level
     * @param debug_info  debug信息
     */
    public static void debugPrintln(int debug_level, String debug_info) {
        if (isLevelValid(debug_level)) {
            StackTraceElement[] trace_list = new Throwable().getStackTrace();
            StackTraceElement trace = trace_list[1];
            if (isRangeValid(trace_list, trace)) {
                String debug_location = String.format("[%s : %s]", trace.getFileName(), trace.getLineNumber());
                System.out.println(String.format("[DEBUG - %s] %s %s",
                        debug_level, show_debug_location ? debug_location : "", debug_info));
            }
        }
    }
}

在一開始做好基本的配置後(命令行解析程序請自行編寫),調用起來也是非常簡單:

DebugHelper.debugPrintln(2, String.format("Operation request %s pushed in.", operation_request.toString()));

靜態方法debugPrintln的第一個參數表示debug level,這也將決定在當前debug級別下是否輸出這一debug信息。而第二個參數則表示debug信息。

實際運用

說了這些,那麼這一系統如何進行實際運用呢?

如何根據debug信息找出bug在哪

筆者的程序中,最大的debug level是4,在關鍵位置上近乎每幾行語句就會輸出相應的調試信息,展示相關計算細節。而且使用--debug_show_location命令行時還可以顯示debug信息打印方法的調用位置。

而一般的bug無非是幾種情況:

  • crash 在出現crash的時候,筆者的程序由於debug信息間隔很短,所以只需要--debug_show_location參數就可以相當精確地定位到crash的位置
  • wrong answer 在結果不符合預期的時候,可以和正確結果進行比對,並找到第一條開始出現錯誤的輸出,然後將這條輸出在全部的帶有debug信息的輸出中進行文本查找,並根據查找到的位置查看上下文的計算過程細節。也可以做到層層細化debug信息,最終找到錯誤所在的位置。

簡單來說,在上面的效果展示圖中我們可以看到,只要開啓--debug_show_location就可以查看debug信息打印的代碼位置。例如,筆者程序中(文件Scheduler.java中)有這麼一塊:

可以看到在上面的--debug 3 --debug_show_location圖中,就有Scheduler.java : 59的輸出信息。

當我們在debug的時候,先是根據輸出的信息判斷是哪一步的debug信息開始出現錯誤,然後就可以根據debug信息中提供的位置來將bug位置縮小到一個很小的範圍。(例如:Scheduler.java : 59的輸出還是正確的,到了Scheduler.java : 70這一行就出現了錯誤,那麼可以基本確定bug就在Scheduler.java60-70行之間)。

如何合理佈置debug信息輸出位置

說到這裏,問題來了,究竟如何合理高效地佈置debug信息的輸出呢?

很顯然,過少的輸出根本無助於編程者快速的找到問題;而過多的信息則會導致有用的沒用的全混在一起,也一樣無助於編程者解決bug。

目前筆者採用的策略

目前筆者還是在手動添加輸出點。

筆者根據自己對於自己程序的模塊化了解,例如:

  • 有哪些區域(包、類等)包含大量的、複雜的計算(這意味着,這些區域很有可能將成爲debug階段調試的焦點區域)
  • 對於一個稍微複雜的方法(實際上符合代碼規範的程序不應該有單個過於複雜的方法),每一部分的代碼都有其相對獨立的意義

則我們可以按照如上的標準,在各個關鍵位置上進行debug信息輸出

例如,對於程序(程序僅做演示):

    public static void main(String[] args) {
        try {
            initialize(args);
            
            // initialize the standard input
            Scanner sc = new Scanner(System.in);
            
            // check if there an available line in the standard input
            if (!sc.hasNextLine()) {
                throw new Exception("No available line detected!");
            }
            
            // get a line from the standard input
            String line = sc.nextLine();
            
            // initialize the regular expression objects
            Pattern pattern = Pattern.compile("(\\+|-|)\\d+(\\.\\d+)?");
            Matcher matcher = pattern.matcher(line);
            
            // get the numbers from the input line
            ArrayList<Double> array = new ArrayList<>();
            while (matcher.find()) {
                array.add(Double.parseDouble(matcher.group(0)));
            }
            
            // if there is no value in the string
            if (array.size() == 0) {
                throw new Exception(String.format("No value detected in input - \"%s\".", line));
            }
            
            // calculate the average value of the array
            double average = 0;
            for (double value : array) {
                average += value;
            }
            average /= array.size();
            
            // calculate the variance value of the array
            double variance = 0;
            for (double value : array) {
                variance += Math.pow((value - average), 2.0);
            }
            variance /= array.size();
            
            // output the result;
            System.out.println(String.format("Variance : %.2f", variance));
            
        } catch (Exception e) {  // exception detected
            System.out.println(String.format("[ERROR - %s] %s", e.getClass().getName(), e.getMessage()));
            System.exit(1);
        }
    }

這是一個簡單的demo,用途是從字符串中抽取數,並計算方差。運行效果如下:

我們來分析一下程序。首先,很明顯,程序的結構分爲如下幾個部分:

  • 嘗試從標準輸入讀入一行字符串
  • 正則表達式分離數字
  • 計算平均值
  • 根據平均值計算方差

我們可以按照這幾個基本模塊,來設置level 1的debug信息輸出,就像這樣:

    public static void main(String[] args) {
        try {
            initialize(args);
            
            // initialize the standard input
            Scanner sc = new Scanner(System.in);
            
            // check if there an available line in the standard input
            if (!sc.hasNextLine()) {
                throw new Exception("No available line detected!");
            }
            
            // get a line from the standard input
            String line = sc.nextLine();
            DebugHelper.debugPrintln(1, String.format("Line detected : \"%s\"", line));
            
            // initialize the regular expression objects
            Pattern pattern = Pattern.compile("(\\+|-|)\\d+(\\.\\d+)?");
            Matcher matcher = pattern.matcher(line);
            
            // get the numbers from the input line
            ArrayList<Double> array = new ArrayList<>();
            while (matcher.find()) {
                array.add(Double.parseDouble(matcher.group(0)));
            }
            DebugHelper.debugPrintln(1,
                    String.format("Array detected : [%s]",
                            String.join(", ",
                                    array
                                            .stream()
                                            .map(number -> number.toString())
                                            .collect(Collectors.toList())
                            )
                    )
            );
            
            // if there is no value in the string
            if (array.size() == 0) {
                throw new Exception(String.format("No value detected in input - \"%s\".", line));
            }
            
            // calculate the average value of the array
            double average = 0;
            for (double value : array) {
                average += value;
            }
            average /= array.size();
            DebugHelper.debugPrintln(1, String.format("Average value : %s", average));
            
            // calculate the variance value of the array
            double variance = 0;
            for (double value : array) {
                variance += Math.pow((value - average), 2.0);
            }
            variance /= array.size();
            DebugHelper.debugPrintln(1, String.format("Variance value : %s", variance));
            
            // output the result;
            System.out.println(String.format("Variance : %.2f", variance));
            
        } catch (Exception e) {  // exception detected
            System.out.println(String.format("[ERROR - %s] %s", e.getClass().getName(), e.getMessage()));
            System.exit(1);
        }
    }

然後我們將--debug參數設置爲1,輸出如下:

如果接下來,在這裏面發現有不對(如果真的能的話)。

我們首先可以想到,最有可能出現錯誤的就是計算密集的平均值和方差計算部分。想進一步排查的話,可以在其計算循環內添加level 2的debug信息輸出:

    public static void main(String[] args) {
        try {
            initialize(args);
            
            // initialize the standard input
            Scanner sc = new Scanner(System.in);
            
            // check if there an available line in the standard input
            if (!sc.hasNextLine()) {
                throw new Exception("No available line detected!");
            }
            
            // get a line from the standard input
            String line = sc.nextLine();
            DebugHelper.debugPrintln(1, String.format("Line detected : \"%s\"", line));
            
            // initialize the regular expression objects
            Pattern pattern = Pattern.compile("(\\+|-|)\\d+(\\.\\d+)?");
            Matcher matcher = pattern.matcher(line);
            
            // get the numbers from the input line
            ArrayList<Double> array = new ArrayList<>();
            while (matcher.find()) {
                array.add(Double.parseDouble(matcher.group(0)));
            }
            DebugHelper.debugPrintln(1,
                    String.format("Array detected : [%s]",
                            String.join(", ",
                                    array
                                            .stream()
                                            .map(number -> number.toString())
                                            .collect(Collectors.toList())
                            )
                    )
            );
            
            // if there is no value in the string
            if (array.size() == 0) {
                throw new Exception(String.format("No value detected in input - \"%s\".", line));
            }
            
            // calculate the average value of the array
            double average = 0;
            for (double value : array) {
                average += value;
                DebugHelper.debugPrintln(2, String.format("present number : %s, present sum : %s", value, average));
            }
            average /= array.size();
            DebugHelper.debugPrintln(1, String.format("Average value : %s", average));
            
            // calculate the variance value of the array
            double variance = 0;
            for (double value : array) {
                variance += Math.pow((value - average), 2.0);
                DebugHelper.debugPrintln(2, String.format("present number : %s, present part : %s", value, variance));
            }
            variance /= array.size();
            DebugHelper.debugPrintln(1, String.format("Variance value : %s", variance));
            
            // output the result;
            System.out.println(String.format("Variance : %.2f", variance));
            
        } catch (Exception e) {  // exception detected
            System.out.println(String.format("[ERROR - %s] %s", e.getClass().getName(), e.getMessage()));
            System.exit(1);
        }
    }

然後我們將--debug參數設置爲2,輸出如下:

可以看到連每一次的計算步驟也都顯示了出來。然而,如果我們修復了一個局部區域的level 2bug,然後需要暫時關閉level 2信息的輸出的話,是不是需要刪除level 2輸出呢?

不需要!直接將命令行改回--debug 1即可

綜上,demo雖然略簡單了些,但是大致就是這樣一個部署輸出點的過程:

  • 評估程序debug核心區域
  • 在關鍵位置層層細化添加輸出點

可行的自動化部署思路

基於方法依存分析的簡單出入口參數部署

說到一種比較易於實現的傻瓜化部署方式,當然是在所有函數入口的時候輸出參數值信息,並且在出口處輸出返回值信息。

這樣的做法優點很明顯:

  • 對於開發者而言原理簡單,十分易於操作
  • 對於代碼規範較好的項目,即便如此簡單的自動部署模式也可以獲得很好的debug效果

不過缺點也一樣很明顯:

  • 盲目部署,資源浪費嚴重 正是由於原理過於簡單,所以自動部署程序並不會判斷真正會有debug需求的區域在哪,而是盲目的將所有的方法都加上debug信息。雖然一定程度上可以通過調節debug level來緩解debug信息混亂的情況。但是這無疑還是會對整個系統造成很多不必要的時空資源浪費
  • 難以兼容代碼規範性較差的項目 正是因爲在代碼規範的項目中表現較好,所以這也意味着,對於代碼規範不那麼好甚至較差的項目中實際效果將無法得到保證。例如很多初學者和算法競賽選手的最愛(以下是他們的完整程序源碼):
public abstract class Main {
    public static void main(String[] args) {
        /*
            do somthing inside
            about 1,000+ lines
        */
    }
}

如果只在出入口進行輸出的話,則可以說是毫無意義的。

  • 難以針對性展示覆雜結構化對象 這件事也是這個策略所必須考慮的。有的參數類型容易展示,例如intdoubleString等;有的展示稍微麻煩,但是還算可以展示,例如ArrayListHashMap等;而有些對象則是非常複雜且難以展示的,例如線程對象、DOM元素、網絡協議配置對象等。不僅如此,就算都能展示,要是輸入數據是一個極其龐大的HashMap(比如有數十萬條的key),如果盲目的一口氣輸出來的話,不僅會給debug信息的展示效果帶來很大的干擾,而且如此大量的IO還會令本就不充裕的IO資源雪上加霜(在這個算法已經相當發達的時代,IO往往是程序性能的主要瓶頸),而反過來想想,想發掘出究竟哪些是有效信息,似乎又不那麼容易做到。

顯然,這樣一個傻瓜化的策略,還需要很多的改進纔可能趨於成熟。

基於語法樹和Javadoc API的部署策略

筆者之前稍微瞭解過一些語法樹相關的概念。實際上,基於編譯器的語法樹常常被用於代碼查重,甚至稍微高級一點的代碼混淆技巧也難以倖免(以C++爲例,#define、拆分函數等一般的混淆技術在基於語法樹的代碼查重面前已經難以矇混過關)。

筆者對編譯原理等一些更細的底層原理實際上並不是很瞭解,只是對此類東西有一些感性的認識。筆者在想,既然語法樹具有這樣的特性,那麼能不能基於編譯器語法樹所提供的程序結構信息,結合Javadoc API提供的方法接口信息,來進行更加準確有效一些的debug信息輸出點自動部署呢?(甚至,如果條件允許的話,可以考慮收集一些用戶數據再使用RNN進行有監督學習,可能效果會更貼近實際)

如何合理設置debug level

目前筆者採用的策略

如上文所述,筆者目前是根據自己的程序採用層層細化的方式來手動部署debug信息輸出。

所以筆者在debug level的手動設定問題上基本也在遵循層層細化的原則。此處不再贅述。

可行的自動化部署思路

基於方法依存關係分析的簡單debug層級判定

目前筆者想到的一種較爲可行的debug level自動生成策略,是根據方法之間的依存關係。

我們可以以函數入口點方法(筆者的程序中一般爲Main.main)爲根節點,再基於語法樹(或者實在不行手寫一個基於文本的文法分析也行)分析出根節點方法中調用的其他方法來作爲子節點。以此類推構建起來一棵樹(同時可能需要處理拓撲結構上的環等結構)。然後結合包、類圖信息等的分析進行一些調整,最終建立起完整的樹,之後再對於整個樹的層次結構採用聚類等方式進行debug level的分類。

當然,這一切目前還只是停留在構想階段,真正的實施,還有很長的路要走。Keep hungry!

優勢

綜上,這一系統的主要優點如下:

  • 整個過程完全不依賴debugger,或者也可以說,只需要文本編輯器+編譯器/解釋器,就可以進行高效率的調試。這很符合程序猿的無鼠標編碼習慣
  • 一般人在使用debugger的時候,思路很容易陷入局部而觀察不到更大的範圍。這也容易導致一些邏輯層面的bug變得難以被發現。而debug信息完整的輸出調試則可以將整個計算的邏輯過程展現給編程者,兼顧了局部和整體。
  • 便於拆除 當需要將整個項目的debug信息輸出全部拆除時,由於輸出接口唯一,所以非常好找,可以通過文本正則替換的方式一次性清除輸出點。
  • 此外,輸出調試在多線程程序的調試中也有很大的優勢。筆者實測,多線程的程序在debugger中常常會變得匪夷所思,一般的debugger並不能很好的支持多線程的情況。而輸出調試則不存在這一問題,只會按照時間順序進行輸出,而且也正是這一特性,輸出調試也可以很好的展現線程的掛起、阻塞等過程

筆者在本次作業中,debug全程使用這一系統,配合文本搜索工具(即便是linux cli下也是可以使用grep的),定位一個bug的位置平均只需要一分鐘不到,調試效率可以說超過了很多使用debugger的使用者

事實證明,輸出調試也是可以發揮出巨大威力的

還是那句老話,以人爲本適合自己,適合團隊,能提高效率創造效益的,就是最好的

ex: 我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章