藉助SpotBugs將程式錯誤扼殺在搖籃中

2022-06-12 18:03:09

背景

最近一年多在一家toB業務的公司,部門主要做的是建築行業的招投標業務資訊化,希望藉助軟體來達到「陽光、降本、提效」的目的,我剛入職時大概30多家客戶,截止現在已經超過100家,發展勢頭迅猛,隨之而來也暴露出來一個很嚴重的問題:「質量差,線上反饋多。」

大夥每天疲於處理線上反饋,需求響應緩慢,交付速度跟不上市場節奏,長此下去對團隊士氣是一種打擊也會激化研發和業務的矛盾,鑑於此我們對線上反饋進行了逐個分析、打標,最終的結論是:「大部分反饋是由於客戶一些個性化需求導致邏輯衝突,太多的開關充斥在程式碼中有時會顧此失彼,還有一部分完全是編碼層面的低階問題,空指標、跨型別的equals等」,對於需求問題由產品經理和研發經理把控,不接稀裡糊塗的需求,對於低階別的編碼問題由我牽頭出具一些優化方案。

 

研發流程現狀

「開發-》測試-》上線-》線上環境複測」,很簡潔的一個流程吧,在當初業務量小、團隊小的時候確實沒什麼問題,響應也快,但是現在人員規模30+的團隊還是用這種簡單的流程,已然是不合適了,必須增加一些手段來保證開發質量,讓一些低階錯誤扼殺在搖籃中,首當其要的是改善現有的研發流程,增加一些必要的自檢和code review,初步改善以後為:「拉取私有分支-》開發-》自測-》提交 Pull Request合流-》組內同事code review-》測試-》上線-》線上環境複測」,在初期確實是收到了一些收益,一些低階別的問題通過「人肉」確實能發現不少,但是到了大家都趕進度的時候就完全靠不住,原因你懂得。

 

上工具

參照我之前公司CTO的一句話:「靠人終歸是靠不住的,最好靠遵循規則的機器。

人都是有惰性的,靠管理制度來提高研發質量這種精神值得鼓勵但是不提倡,找到順手的工具才是我們的解決之道,將工具和管理制度結合起來,接下來就是我們本篇的重點SpotBugs,一個靜態程式碼掃描工具。

 

靜態程式碼掃描概念

靜態原始碼掃描是近年被人提及較多的軟體應用安全解決方案之一。它是指在軟體工程中,程式設計師在寫好原始碼後,無需經過編譯器編譯,而直接使用一些掃描工具對其進行掃描,找出程式碼當中存在的一些語意缺陷、安全漏洞的解決方案。靜態掃描技術已經從90年代時候的,編碼規則匹配這種由編譯技術拓展過來的分析技術向程式模擬全路徑執行的方向發展,由此,這種模擬執行相對的執行路徑比動態執行更多,能夠發現很多動態測試難以發現的缺陷。

靜態程式碼掃描優點

這個方案的優點在於,無需進行編譯、也無需去搭建執行環境,就可以對程式設計師所寫的原始碼進行掃描。可以節省大量的人力和時間成本,提高開發效率,並且能夠發現很多靠人力無法發現的安全漏洞,站在駭客的角度上去審查程式設計師的程式碼,大大降低專案中的安全風險,提高軟體質量。

 

靜態程式碼掃描缺點

傳統的靜態分析,傳統的靜態分析都是基於語法解析或者編譯器,這些方式分析程式碼的缺陷是以程式碼所匹配的規則模式(patterns)去評估程式碼,只要模式匹配或者相似就報出來。需要人工去分辨出其中的真假,主要存在的問題:

– False positive(誤報)

– False negative(漏報)

以上內容來源於百度百科

 

案例

案例一 空指標

這真的是一個很低階的問題了,不見得是能力問題,稍不留神很容易出,||寫成&&,少加個!之類的。

List<Map<String, Object>> resultMap = getBasedao().queryList(sql, paramMap);
if (resultMap == null && resultMap.isEmpty()) {

}

  

 

修復方法1:

if (resultMap == null || resultMap.isEmpty())

修復方法2:

if (CollectionUtils.isEmpty(resultMap ))

推薦使用工具類的判斷方法,將重複性的事情交給工具類來幹,減少出錯的可能性,而且程式碼的描述性更強,isEmpty,isNotEmpty等等。

 

案例二 返回值被忽略

String querySql = "select * from biz_table where id=xxx";

Map<String, Object> query = getBasedao().query(querySql);
if (query == null) {
   querySql.replace("biz_table ","biz_table _history");
   query = getBasedao().query(querySql);
}

 

修復方法:

接收返回值,querySql = querySql.replace

 

案例三 equals兩個不相關的物件

這個絕對是能力問題了,「鋼筋」和「混凝土」怎麼能equals呢,Class都不一樣。

List<Map<String, Object>> maps = getBasedao().queryList(sql, map);
if(CollectionUtils.isEmpty(maps)) {
      return null;
}

if(maps.get(0).equals("")) {
     return null;
}

 

案例四  keySet方式遍歷Map比entrySet方式低效

public static String getXmlDataV3(Map<String,Object> params) {
StringBuffer buf = new StringBuffer();
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
buf.append("<root>");
buf.append("<request>");
for (String key:params.keySet()) {
    buf.append("<"+key+">"+params.get(key)+"</"+key+">");
}
buf.append("</request>");
buf.append("</root>");
return buf.toString();
}

  

修復方法

直接使用entrySet遍歷,減少不必要的讀取

for(Map.Entry<String,Object> me : params.entrySet()){
     buf.append("<"+me.getKey()+">"+me.getValue()+"</"+me.getKey()+">");
}

 

這些問題洩漏出去不知道會引起多少反饋呢?

案例回顧

通過這七個案例可以初步體驗靜態程式碼掃描的強大之處,在釋出之前幫助發現一些簡單但是出錯頻率很高的問題,將錯誤扼殺在搖籃中,錯誤帶到生產環境的代價是巨大的,本來是一個2分鐘解決的空指標問題,最後可能會發酵成客戶的投訴函,整個鏈條上的同事都要一起填坑。

 

 

 

SpotBugs安裝

直接在idea外掛市場安裝,安裝完以後需要重啟。

 

   1.3 等待

   1.4 檢視掃描結果

  

 

 1.5 檢視明細

 

 

 

 

 重點關注Correctness(正確性),邏輯錯誤,空指標之類的。

 

2.掃描某個類

對於新寫的程式碼,強烈建議大家掃描,可以發現不少問題

    2.1 選中具體的類

    2.2 右鍵SpotBugs→Analyze Selected File

  

 

 

 

接下來的步驟和之前沒有區別,就不囉嗦了。

3.按等級檢視bug

SpotBugs將bug按不同維度進行了劃分,比如「正確性、效能、壞味道」這是bug型別的維度,還有一個維度是bug等級,SpotBugs將bug分成了四個等級,分別是:

  1. scariest  最可怕

  2. scary   可怕

  3. troubling  令人不安

  4. of concern 令人擔憂

點選掃描結果左下角的Group by Bug Rank,就會按等級呈現,真的是一個很酷的功能,工具已經幫我們分組,而且以顏色區分,看到的scariest和scary的你就立馬點進去修改一波吧

 

 

 

SpotBugs和Pull Request結合

前面我們對最初的研發流程做了一些優化,增加了自測、Pull Request、Code Review等步驟,短期內確實收到了一些收益,但是時間一長人的惰性再加上時間緊的時候這些流程就開始應付了,鑑於此我們引入工具幫我們發現一些低階別的問題,嵌入到Create Pull Request流程中,合併程式碼必須提供SpotBugs的掃描結果圖,不允許包含scariest和scary級別的問題,這樣我們就把工具和管理手段結合起來保障研發質量。

 

 

推薦閱讀

FindBugs - Wikipedia

https://github.com/spotbugs/spotbugs

https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html

 

寫在最後

工具用的好,下班沒煩惱。