程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論與實踐:平衡測試,第2部分:編寫和優化bug檢測器

Java理論與實踐:平衡測試,第2部分:編寫和優化bug檢測器

編輯:關於JAVA

這一簡短系列的 第 1 部分 介紹了如何進行有效測試,它構建了 FindBugs 插件來查找一個簡單的 bug 模式(只需調用 System.gc() 即可)。Bug 模式會 標識有問題的編碼實踐,它們常常位於 bug 所在的區域。當然,並不是所有出 現 bug 模式的地方都一定出現 bug,但這並不能抹殺 bug 模式檢測器的巨大作 用。一個有效 bug 模式檢測器的主要功能是發現更高百分比的可疑代碼,使該 模式具有更大的使用價值。創建 bug 模式檢測器可以提高使用價值;創建檢測 器之後,無論是現在還是將來,您都可以在您需要的任何代碼上運行它,並且您 可能對發現的問題感到驚訝。例如,第 1 部分 中的簡單檢測器顯示了對 System.gc() 的調用,在 JDK 1.4.2 中,它隱藏在 JPEG 圖像 I/O 庫中。

編寫檢測器可以查找對特定靜態方法的調用,這並不困難,但是大多數 的 bug 檢測器都包含相當多的分析和實現。在這一期的文章中,您將開發一個 稱為 RuntimeException capture 的更小 bug 模式的檢測器(目前,FindBugs 發行版中已包含此 bug 檢測器。)

RuntimeException 捕獲

用 Java™ 語言進行異常處理的一個優點是:異常是一些對象,try-catch 機 制了解異常類型的分層結構,並在客戶機如何處理錯誤處理方面提供實際靈活性 。例如,如果不能找到文件,則 FileInputStream 構造函數會拋出 FileNotFoundException,該異常是 IOException 的一個子類。此傳統用法允許 客戶機處理未發現文件的條件,這些條件是從其他與文件相關的條件中分離出來 的(如果他們喜歡單獨捕獲 FileNotFoundException)。但是,他們還可以使用 捕獲 IOException 的方法處理所有與文件相關的錯誤條件。

另一方面,異常處理的主要缺陷是:在正確使用異常時,易於建立帶有三行 或四行業務邏輯以及 20 或 30 行異常處理的方法。因為錯誤恢復代碼在測試時 容易出現錯誤並且執行困難,使一部分專門用於異常處理的代碼無所適從並容易 出錯。這種情況的典型示例如清單所示,其中帶有兩行 “真的” 代碼的方法需 要三個獨立的捕獲塊,每個捕獲塊都執行完全相同的操作 —— 記錄該異常:

清單 1. 多個相同的捕獲塊

public void addInstance(String className) {
   try {
     Class clazz = Class.forName(className);
     objectSet.add(clazz.newInstance());
   }
   catch (IllegalAccessException e) {
     logger.log("Exception in addInstance", e);
   }
   catch (InstantiationException e) {
     logger.log("Exception in addInstance", e);
   }
   catch (ClassNotFoundException e) {
     logger.log("Exception in addInstance", e);
   }
}

請參見清單 1,您可能嘗試將三個捕獲塊合並成捕獲 Exception 的單獨捕獲 塊,因為每個捕獲塊的捕獲恢復操作是相同的。乍一看,該策略似乎是一個好方 法 —— 但代碼副本有錯誤,所以整合這些復制路徑應該是一種改進。不過,此 “改進” 常常會帶來意想不到的結果。因為 RuntimeException 擴展了 Exception,將三個捕獲塊合並成一個捕獲塊(如清單 2 所示),所以這會更改 語義,現在,未經檢查的異常將被記錄(而不傳播)。此 bug 模式(其中 RuntimeException 容易被超大捕獲塊捕獲)也稱為 RuntimeException 捕獲。

清單 2. RuntimeException 捕獲 bug 模式 —— 不要執行此模式

public void addInstance(String className) {
   try {
     Class clazz = Class.forName(className);
     objectSet.add(clazz.newInstance());
   }
   catch (Exception e) {
     logger.log("Exception in newInstance", e);
   }
}

bug 模式通常源自語言的模糊功能或類庫;出現此 bug 模式是因為 RuntimeException 擴展了 Exception,這稍微有點違反常理。對 RuntimeException 捕獲的修復非常容易 —— 您需要了解以下問題:首先捕獲 RuntimeException,並在捕獲 Exception 之前重新將其拋出,如清單 3 所示。不過,即使知道 bug 模式及其修復方法,在代碼審查過程中也很容易忘記執行 它或忽略它,並且編譯器也不會通知您。這是引入 bug 模式的原因,幫助您避 免違犯 “您已較好地了解” 之類的錯誤。

清單 3. 通過顯式處理 RuntimeException 修復 RuntimeException 捕獲

public void addInstance(String className) {
   try {
     Class clazz = Class.forName(className);
     objectSet.add(clazz.newInstance());
   }
   catch (RuntimeException e) {
     throw e;
   }
   catch (Exception e) {
     logger.log("Exception in newInstance", e);
   }
}

編寫 RuntimeException 捕獲檢測器

正如您在 上一期 中所學的,編寫 bug 模式的第一個步驟是清楚地標識 bug 模式。在這裡,bug 模式是捕獲 Exception 的 catch 塊,這時不存在用於 RuntimeException 的相應捕獲塊,並且嘗試塊中的任何方法調用或 throw 語句 都不會拋出 Exception。要檢測此 bug 模式,則需要知道 try-catch 塊的位置 、try 塊可能拋出的內容以及在 catch 塊中將捕獲的內容。

標識捕獲的異常

像上個月的操作一樣,您可以通過創建 BytecodeScanningDetector 基礎類 (可實現 Visitor 模式)的子類啟動 bug 檢測器。在 BytecodeScanningDetector 中有一個 visit(Code) 方法,並且在每次發現 catch 塊時,該實現都會調用 visit(CodeException)。如果重寫 visit(Code) ,並從那裡調用 super.visit(Code),則當超類 visit(Code) 返回時,它將調 用用於該方法中所有 catch 塊的 visit(CodeException)。清單 4 了顯示實現 visit(Code) 和 visit(CodeException) 的第一步,它將積累方法中所有 catch 塊的信息。每個 CodeException 都包含相應 try 塊的起始和終止的字節碼偏移 量,這樣您可以方便地確定哪一個 CodeException 對象與 try-catch 塊對應。

清單 4. 第一版 RuntimeException 捕獲檢測器可以收集某一方法中拋出的 異常信息

public class RuntimeExceptionCapture extends  BytecodeScanningDetector {
  private BugReporter bugReporter;
  private Method method;
  private OpcodeStack stack = new OpcodeStack();
  private List<ExceptionCaught> catchList;
  private List<ExceptionThrown> throwList;

  public void visitMethod(Method method) {
   this.method = method;
   super.visitMethod(method)  }

  public void visitCode(Code obj) {
   catchList = new ArrayList<ExceptionCaught>();
   throwList = new ArrayList<ExceptionThrown>();
   stack.resetForMethodEntry(this);

   super.visitCode(obj);
   // At this point, we've identified all the catch  blocks
   // More to come...
  }

  public void visit(CodeException obj) {
   super.visit(obj);
   int type = obj.getCatchType();
   if (type == 0) return;
   String name =
    getConstantPool().constantToString(getConstantPool ().getConstant(type));

   ExceptionCaught caughtException =
    new ExceptionCaught(name, obj.getStartPC(), obj.getEndPC (), obj.getHandlerPC());
   catchList.add(caughtException);
  }
}

標識拋出的異常

此時,您已獲得了您需要的一半信息:在何處捕獲哪些異常。現在必須找出 哪些異常被拋出。為此,您需要重寫 BytecodeScanningDetector 的 sawOpcode() 方法,並處理與方法調用和異常拋出相對應的字節碼。可以根據 athrow JVM 指令拋出異常。三個 JVM 指令分別用於調用以下方法: invokestatic、invokevirtual 和 invokespecial。就像使用 visit (CodeException) 一樣,在調用超類 visit(Code) 時可以調用 sawOpcode,這 樣,如果在 sawOpcode() 中收集信息,那麼在 super.visit(Code) 返回時,您 將獲得您需要的、有關捕獲和拋出異常的所有信息。

清單 5 顯示了 sawOpcode() 的實現,它將處理上述 JVM 指令。對於 athrow 指令,可以使用 FindBugs 的 OpcodeStack 幫助器類來了解 athrow 操 作數的類型。對於方法調用指令,可以使用 Bytecode Engineering Library (BCEL) 類來提取方法聲明拋出的已檢查異常的類型。在任何一種情況下,都可 以積累關於哪些異常在方法中的哪個字節碼偏移量被拋出的信息,這樣,在完成 整個方法的處理後,可以將它們進行匹配。

清單 5. 標識受訪問代碼中拋出異常的位置

public void  sawOpcode(int seen) {
  stack.mergeJumps(this);
  try {
    switch (seen) {
    case ATHROW: 
      if (stack.getStackDepth() > 0) {
        OpcodeStack.Item item = stack.getStackItem(0);
        String signature = item.getSignature();
        if (signature != null &&  signature.length() > 0) {
          if (signature.startsWith("L"))
            signature = SignatureConverter.convert (signature);
          else 
            signature = signature.replace('/',  '.');
          throwList.add(new ExceptionThrown(signature,  getPC()));
        }
      }
      break;

    case INVOKEVIRTUAL: 
    case INVOKESPECIAL: 
    case INVOKESTATIC: 
      String className = getDottedClassConstantOperand ();
      try {
        if (!className.startsWith("[")) {
          JavaClass clazz = Repository.lookupClass (className);
          Method[] methods = clazz.getMethods();
          for (Method method : methods) {
            if (method.getName().equals (getNameConstantOperand())
                && method.getSignature().equals(getSigConstantOperand()))  {
              ExceptionTable et =  method.getExceptionTable();
              if (et != null) {
                String[] names =  et.getExceptionNames();
                for (String name : names)
                  throwList.add(new  ExceptionThrown(name, getPC()));
              }
              break;
            }
          }
        }
      } catch (ClassNotFoundException e) {
        bugReporter.reportMissingClass(e);
      }
      break;
    default: 
      break;
    }
  } finally {
    stack.sawOpcode(this, seen);
  }
}

匯總結果

在獲得所需的關於捕獲和拋出異常的信息後,最後一步是匯總這些信息。在 超類 visit(Code) 的調用返回後,將完全填充 throwList 和 caughtList 集合 。它們包含關於方法中所有 try-catch 塊的信息,所以您必須將拋出信息和捕 獲信息關聯,以標識 bug 模式。

清單 6 顯示了用於標識 RuntimeException 捕獲的邏輯。它將迭代捕獲塊的 列表,如果發現捕獲 Exception 的塊,它會再次查找捕獲塊,該捕獲塊將捕獲 字節碼同一范圍的 RuntimeException。它還可以查找在字節碼的相應范圍中拋 出 Exception 的實例。如果沒有捕獲 RuntimeException,也沒有拋出 Exception,則存在一個潛在的 bug。

清單 6. 合並捕獲和拋出數據,以標識 RuntimeException 捕獲

for (ExceptionCaught caughtException : catchList) {
   Set<String> thrownSet = new HashSet<String> ();
   for (ExceptionThrown thrownException : throwList) {
     if (thrownException.offset >=  caughtException.startOffset
         && thrownException.offset < caughtException.endOffset)  {
       thrownSet.add(thrownException.exceptionClass);
       if (thrownException.exceptionClass.equals (caughtException.exceptionClass))
         caughtException.seen = true;
     }
   }
   int catchClauses = 0;
   if (caughtException.exceptionClass.equals ("java.lang.Exception")
    && !caughtException.seen) {
     // Now we have a case where Exception is caught,  but not thrown
     boolean rteCaught = false;
     for (ExceptionCaught otherException : catchList) {
       if (otherException.startOffset ==  caughtException.startOffset
           && otherException.endOffset == caughtException.endOffset)  {
         catchClauses++;
         if (otherException.exceptionClass.equals ("java.lang.RuntimeException"))
           rteCaught = true;
       }
     }
     int range = caughtException.endOffset -  caughtException.startOffset;
     if (!rteCaught) {
       bugReporter.reportBug(new BugInstance(this,  "REC_CATCH_EXCEPTION",
           NORM_PRIORITY)
           .addClassAndMethod(this)
           .addSourceLine(this,  caughtException.sourcePC));
     }
   }
}

要編寫 bug 檢測器,則需要了解 JVM 字節碼和類文件的一些結構。BCEL 和 FindBugs 庫將為您處理此任務,並從字節碼中提取信息,在稍高級別上呈現它 。遺憾的是,關於 BCEL 和 FindBugs 如何支持類分離的文檔並不能滿足您的需 要。像使用許多開放源碼項目一樣,關於如何編寫檢測器的最佳信息源是參照執 行類似任務的其他檢測器。

優化檢測器

使用靜態分析的最大消耗是處理假警報。靜態分析不一定精確,其目標不是 發現 bug,而是只發現那些可能是 bug 的構造,這意味著有時會標記正確的代 碼出錯。如果代碼審核工具產生 95% 的假警報,那麼任何人都不太可能想再次 使用它;第一次發現報告新 bug 的假警報真的很痛苦。所以對於一個有效的 bug 模式檢測器,它必須最小化假警報數量,最好使假情報不超過 50%。

優化檢測器的最佳方法是在 JDK 類庫 (rt.jar)、Eclipse 或 JBoss 之類的 大型代碼基址上運行它。所以在編寫 bug 檢測器後,應該試著在新的項目和示 例上運行它,以查看它們是真實的 bug,還是假警報。對於非凡的檢測器(比如 這裡開發的檢測器),第一次體驗常常有點讓人失望—— 假警報比預期的多。

優化檢測器的過程包括查找假警報和細化 bug 模式,以消除某些假警報,同 時不要將太多的真實 bug 排除在外。為細化模式,可以執行的操作之一是消除 以下情況:存在零或 try 塊中拋出已經過檢查的異常;在這些情況下,捕獲 Exception 不可能導致嘗試合並多個捕獲塊,而會導致反映對捕獲未經檢查的異 常的真實願望。此修改對假警報率有很大的影響。

優化檢測器通常包括 “得分” 算法的使用,以確定是否將匹配報告為 bug 。通過使用幾個因素可執行其他調優,以增加或減少對給定實例的 “信心得分 ”。某些方面(如不存在任何已檢查的異常)可以減少候選匹配的得分;其他方 面,比如捕獲異常失效方面(在捕獲塊中不使用),可以增加候選匹配的得分。清單 7 顯示了對此檢測器進行優化後形成的得分算法;它將優先級用作得分, 因為在某一阈值上具有優先權的 bug 被 bug 報告的框架忽略(較高優先級的值 指示的實際 bug 的嚴重性較低)。

清單 7. 優化後 RuntimeException 捕獲檢測器使用的得分算法

if (!rteCaught) {
   int priority = LOW_PRIORITY + 1;
   if (range > 300) priority--;
   else if (range < 30) priority++;
   if (catchClauses > 1) priority++;
   if (thrownSet.size() > 1) priority--;
   if (caughtException.dead) priority--;
   bugReporter.reportBug(new BugInstance(this,  "REC_CATCH_EXCEPTION",
       priority)
       .addClassAndMethod(this)
       .addSourceLine(this, caughtException.sourcePC));
}

結束語

為靜態代碼分析工具(如 FindBugs)編寫自定義 bug 檢測器可以顯著提高 代碼質量,並且有許多樂趣。盡管編寫和優化 bug 檢測器非常困難(優化它們 對確保其能夠使用非常重要),用檢測器捕獲 bug 模式的信息要付出高昂的代 價,但是,使用這些信息能夠花費少量工作來掃描任何項目中的 bug 模式,從 而使您對最愚蠢的 bug 查找方式感到驚訝。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved