2016年09月22日
一、程式設計規約
命名規約:
【強制】類名使用UpperCamelCase。 不能以下劃線或美元符號打頭,必須遵從駝峰形式,但以下情形例外:(領域模型的相關命名)DO / DTO / VO / DAO等。
正例:MarcoPolo / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion【強制】方法名、引數名、成員變數、區域性變數都統一使用lowerCamelCase。儘量是英文語法表達,避免使用漢語拼音來命名,嚴禁使用拼音與英文混合的命名方式。
說明:
正例: localValue / getHttpMessage() / inputUserId
反例: LocalValue / getPingfenByName() [評分的拼音]【強制】常量命名應該全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
正例: MAX_STOCK_COUNT
反例: MAX_COUNT【參考】列舉類名建議帶上Enum字尾,列舉成員名稱需要全大寫,單詞間用下劃線隔開。
說明:列舉其實就是特殊的常量類,且構造方法被預設強制是私有。
正例:列舉名字:DealStatusEnum;成員名稱:SUCCESS / UNKOWN_REASON【強制】POJO類中的任何布林型別的變數,都不要加is,否則部分框架解析會引起序列化錯誤。
反例:定義為基本資料型別boolean isSuccess;的屬性,它的方法也是isSuccess(),HSF框架在反向解析的時候,“以為”對應的屬性名稱是success,導致屬性獲取不到,進而丟擲異常。【強制】包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有複數含義,類名可以使用複數形式。
正例: 應用工具類包名為com.alibaba.mpp.util、類名為MessageUtils(此規則參考spring的框架結構)【強制】儘量少用縮寫,特別是完全不規範的縮寫。集團認可的縮寫請參考附2。
反例:【推薦】如果使用到了設計模式,建議在類名中體現出具體模式。
說明:將設計模式體現在名字中,有利於閱讀者快速理解架構設計思想。
正例:public class OrderFactory;
????public class LoginProxy;
????public class ResourceObserver;【推薦】介面類中的方法和屬性不要加任何修飾符號(public 也不要加),保持程式碼的簡潔性,並加上有效的javadoc註釋。儘量不要在接口裡定義變數,如果一定要定義變數,肯定是與介面方法相關,並且是整個應用的基礎常量。
正例:介面方法簽名:void f();
??? 介面基礎常量表示:String COMPANY = "alibaba";
反例:介面方法定義:public abstract void f();
說明:JDK8中介面允許有預設實現,那麼這個default方法,是對所有實現類都有價值的預設實現。介面和實現類的命名有三套規則:
?1)【強制】對於Service和DAO類,基於SOA的理念,暴露出來的服務一定是介面,內部的實現類用Impl的字尾與介面區別。
正例: CacheServiceImpl實現CacheService介面。
?2)【推薦】如果是形容能力的介面名稱,取對應的形容詞做介面名(通常是–able的形式)。
正例:AbstractTranslator實現 Translatable。
?3)【推薦】其它名詞型別介面前加I來區別,實現類則去掉“I”即可。
正例: language 實現ILanguage介面。【參考】各層命名規約:
A) Service/DAO層方法命名規約
1) 獲取單個物件的方法用get做字首。
2) 獲取多個物件的方法用list做字首。
3) 獲取統計值的方法用count做字首。
4) 插入的方法總是用save(推薦)或insert做字首。
5) 刪除的方法總是用remove(推薦)或delete做字首。
6) 修改的方法總是用update做字首。
B) 領域模型命名規約
1) 資料物件:xxxDO,xxx即為資料表名。
2) 資料傳輸物件:xxxDTO,xxx為業務領域相關的名稱。
3) 展示物件:xxxVO,xxx一般為網頁名稱。
4) POJO是DO/DTO/BO/VO的統稱,禁止命名成xxxPOJO。
常量定義:
【強制】不允許出現任何魔法值(即未經定義的常量)直接出現在程式碼中。
反例: String key="Id#taobao_"+tradeId;
?????cache.put(key, value);【強制】long或者Long初始賦值時,必須使用大寫的L,不能是小寫的l,小寫容易跟數字1混淆,造成誤解。
說明:Long a = 2l; 寫的是數字的21,還是Long型的2?【推薦】不要使用一個常量類維護所有常量,應該按常量功能進行歸類,分開維護。如:快取相關的常量放在類:CacheConsts下;系統配置相關的常量放在類:ConfigConsts類下。
說明:大而全的常量類,非得ctrl+f才定位到修改的常量,不利於理解,也不利於維護。【推薦】常量的複用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
?1) 跨應用共享常量:放置在二方庫中,通常是client.jar中的const目錄下。
?2) 應用內共享常量:放置在一方庫的modules中的const目錄下。
????反例:易懂變數也要統一定義成應用內共享常量,兩位攻城師在兩個類中分別定義了表示“是”的變數:
????類A中:public static final String YES = "yes";
????類B中:public static final String YES = "y";
????A.YES.equals(B.YES),預期是true,但實際返回為false,導致產生線上問題。
?3) 子工程內部共享常量:即在當前子工程的const目錄下。
?4) 包內共享常量:即在當前包下單獨建const目錄下。
?5) 類內共享常量:直接在類內部private static final定義。【推薦】如果變數值僅在一個範圍內變化用Enum類。如果還帶有名稱之外的延伸屬性,必須使用Enum類,下面正例中的數字就是延伸資訊,表示星期幾。
正例:public Enum{ MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7);}
OOP規約:
【強制】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
【強制】所有的覆寫方法,必須加@Override註解。
反例:getObject()與get0bject()的問題。一個是字母的O,一個是數字的0,加@Override可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。【強制】相同引數型別,相同業務含義,才可以使用Java的可變引數,避免使用Object。
說明:比如列印方法: void print(String... str); 不要寫成:void print(Object... str);【強制】對外暴露的介面簽名,原則上不允許修改方法簽名,避免對介面呼叫方產生影響。介面過時必須加@deprecated註釋,並清晰地說明採用的新介面或者新服務是什麼。
【強制】不能使用過時的類或方法。
說明:java.net.URLDecoder 中的方法decode(String encodeStr) 這個方法已經過時,應該使用雙引數decode(String source, String encode)。介面提供方既然明確是過時介面,那麼有義務同時提供新的介面;作為呼叫方來說,有義務去考證過時方法的新實現是什麼。【強制】Object的equals方法容易拋空指標異常,應使用常量或確定有值的物件來呼叫equals。
正例: "test".equals(object);
反例: object.equals("test");
說明:推薦使用java.util.Objects#equals (JDK1.7引入的工具類)【強制】所有的包裝類物件值的比較,全部使用equals方法比較。
說明:對於Integer var=?在-128至127之間的賦值,Integer物件是在IntegerCache.cache產生,會複用已有物件,這個區間內的Integer值可以直接使用==進行判斷,但是這個區間之外的所有資料,都會在堆上產生,並不會複用已有物件,這是一個大坑,推薦作用equals方法進行判斷。【強制】除區域性變數使用基本資料型別外,其它場景都使用包裝資料型別,如:方法返回值、形參、POJO類屬性。
?說明:
?1)資料庫的查詢結果可能是null,因為自動拆箱,用基本資料型別接收有NPE風險。
?2)包裝資料型別的null值,能夠表示賦值不成功,如:遠端呼叫失敗,異常退出。
?反例:交易總額漲跌HSF服務,返回值為基本資料型別,呼叫不成功時,返回的是預設值,頁面顯示:0%,這是不合理的。
?3)屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE問題,或者入庫檢查,都由使用者來保證。
?反例:基本資料型別做為形參,傳入是Integer,會有自動拆箱操作,容易NPE。【強制】定義DO/DTO/VO等POJO類時,不要加任何屬性預設值。
反例:某業務的DO的gmtCreate預設值為new Date();但是這個屬性在資料提取時並沒有置入具體值,在更新其它欄位時又附帶更新了此欄位,導致建立時間被修改成當前時間。【強制】序列化類修改時,請不要修改serialVersionUID欄位,避免反序列失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改serialVersionUID值。
說明:注意serialVersionUID不一致會丟擲序列化執行時異常。【強制】構造方法裡面禁止加入任何業務邏輯,如果有初始化邏輯,請放在init方法中。
【推薦】當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀。
【強制】POJO類必須寫toString方法。使用工具類source> generate toString時,如果繼承了另一個POJO類,注意在前面加一下super.toString。
說明:在方法執行丟擲異常時,可以直接呼叫POJO的toString()方法列印其屬性值,便於排查問題。【推薦】類內方法定義順序依次是:公有方法或保護方法 > 私有方法 > getter/setter方法。
說明:公有方法是類的呼叫者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式”下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因為方法資訊價值較低,所有Service和DAO的getter/setter方法放在類體最後。【推薦】本類呼叫加this,父類呼叫加super,不管是變數還是方法。這樣有利於閱讀者清晰知道這個方法是本類定義,還是從父類繼承。
【推薦】setter方法中,引數名稱與類成員變數名稱一致,this.成員名=引數名。在get/set方法中,儘量不要增加業務邏輯,增加排查問題難度。
反例:public Integer getData(){ if(true) { return data + 100; } else { return data - 100; } }
【推薦】字串的聯接方式,推薦使用StringBuffer和StringBuilder的方法進行擴充套件:
正例:StringBuffer s = new StringBuffer(); s.append("guan").append("bao");
??如果確定沒有執行緒安全問題,或者是方法內區域性變數,推薦使用StringBuilder,在能夠提前預估目標字串的長度時,可以指定StringBuilder初始容量值,避免自動擴容帶來的資料拷貝開銷與頻繁記憶體分配產生的記憶體碎片。
【推薦】final可提高程式響應效率,宣告成final的情況:
?1) 不需要重新賦值的變數,包括類屬性、區域性變數。
?2) 物件引數前加final,表示不允許修改引用的指向。
?3) 類方法確定不會被重寫。
說明:final修飾的方法會被編譯器優化,優化意味著編譯器可能將該方法用內聯方式載入。final修飾變量表示此變數是不可變的,在多執行緒讀寫場景中是執行緒安全的。【推薦】慎用Object的clone方法來拷貝物件。
說明:物件的clone方法預設是淺拷貝,若想實現深拷貝需要重寫clone方法實現屬性物件的拷貝。【推薦】類成員與方法訪問控制從嚴:
?1) 如果不允許外部直接通過new來生成物件,那麼構造方法必須private。
?2) 工具類不允許有public或default構造方法。
?3) 類的非static成員變數,必須private。
?4) 類的static成員變數如果只是類內部使用,也必須是private。
?5) 若是static成員變數,必須考慮是否為final。
?6) 類成員方法只供類內部呼叫,必須private。
?7) 如果成員方法只對繼承類公開,那麼限制為protected。
說明:任何類、方法、引數、變數,嚴控訪問範圍。過寬泛的訪問範圍,不利於模組解耦。思考:如果是一個private的方法,想刪除就刪除,可是一個public的Service方法,或者一個public的成員變數,刪除一下,不得手心冒點汗嗎?變數像自己的小孩,儘量在自己的視線內,變數作用域太大,如果無限制的到處跑,那麼你會擔心的。【推薦】方法的多個引數順序為:輸入引數在前,輸出引數在後;程式碼裡先出現的引數在前,後出現的引數在後。
反例1:public Object f(Object obj) {… ; return obj;} //obj既是輸入引數,又是返回物件,儘量避免。
反例2:public String f(Long id, String str){ return str + id;} //str和id的使用先後儘量與引數順序保持一致。
集合處理:
【強制】Map/Set的key為自定義物件時,必須重寫hashCode和equals。
說明:為什麼要重寫hashCode() 。
正例:String重寫了hashCode和equals方法,所以我們可以非常愉快地使用String物件作為key來使用。【強制】ArrayList的subList結果不可強轉成ArrayList,否則會丟擲ClassCastException異常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList ;
說明: subList 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList ,而是 ArrayList 的一個檢視,對於SubList子列表的所有操作最終會反映到原列表上。【強制】在subList場景中,高度注意對原列表的修改,會導致子列表的遍歷、增加、刪除均產生ConcurrentModificationException 異常。
說明: 抽查表明,九成的開發同學對此知識點都有錯誤的認知。【強制】使用集合轉陣列的方法,必須使用集合的toArray(T[] a),傳入的是型別完全一樣的陣列,大小就是list.size()。
說明:直接使用toArray無參方法存在問題,此方法返回值只能是Object[]類,若強轉其它型別陣列將出現ClassCastException錯誤。使用toArray帶參方法,入參分配的陣列空間不夠大時,toArray方法內部將重新分配記憶體空間,並返回新陣列地址;如果陣列元素大於實際所需,多餘的陣列元素將被置為null,因此最好將方法入引數組大小定義與集合元素個數一致。【強制】使用工具類Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear方法會丟擲UnsupportedOperationException異常。
說明:asList的返回物件是一個Arrays內部類,並沒有實現集合的修改方法。Arrays.asList體現的是介面卡模式,只是轉換介面,後臺的資料仍是陣列。
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
第一種情況:list.add("c"); 執行時異常。
第二種情況:str[0]= "gujin"; 那麼list.get(0)也會隨之修改。【強制】泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用add方法。
說明:蘋果裝箱後返回一個<? extends Fruits>物件,此物件就不能往裡加任何水果,包括蘋果。【強制】不要在foreach迴圈裡進行元素的remove/add操作。remove元素請使用Iterator方式,如果併發操作,需要對Iterator物件加鎖。
反例:List<String> a = new ArrayList<String>(); a.add("1"); a.add("2"); for (String temp : a) { if("1".equals(temp)){ a.remove(temp); } }
說明:這個例子的執行結果會出乎大家的意料,那麼試一下把“1”換成“2”,會是同樣的結果嗎?
參考:ConcurrentModificationException異常分析
正例:Iterator<String> it = a.iterator(); while(it.hasNext()){ String temp = it.next(); if(刪除元素的條件){ it.remove(); } }
【推薦】集合初始化時,儘量指定集合初始值大小。
說明: ArrayList儘量使用ArrayList(int initialCapacity) 初始化。【推薦】使用entrySet遍歷Map類集合KV,而不是keySet方式進行遍歷。
說明:keySet其實是遍歷了2次,一次是轉為Iterator物件,另一次是從hashMap中取出key所對應的value。而entrySet只是遍歷了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。
正例:values()返回的是V值集合,是一個list集合物件;keySet()返回的是K值集合,是一個Set集合物件;entrySet()返回的是K-V值組合集合。【推薦】高度注意Map類集合K/V能不能儲存null值的情況,如下表格:
集合類 Key Value Super 說明 Hashtable 不允許為null 不允許為null Dictionary 執行緒安全 ConcurrentHashMap 不允許為null 不允許為null AbstractMap 執行緒區域性安全 TreeMap 不允許為null 允許為null AbstractMap 執行緒不安全 HashMap 允許為null 允許為null AbstractMap 執行緒不安全 反例:前期抽樣表明近八成的同學認為ConcurrentHashMap是可以置入null值。在“美杜莎”的批量翻譯場景中,子執行緒分發時,出現置入null值的情況,但主執行緒沒有捕獲到此異常,導致排查困難。
【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。
說明:穩定性指集合每次遍歷的元素次序是一定的。有序性是指遍歷的結果是按某種比較規則依次排列的。如:ArrayList是order/unsort;HashMap是unorder/unsort;TreeSet是order/sort。【參考】利用Set元素唯一的特性,可以快速對另一個集合進行去重操作,避免使用List的contains方法進行遍歷去重操作。
併發處理:
【強制】靜態Utils或單例必須是執行緒安全的。
【強制】執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒。
說明:使用執行緒池的好處是減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。【強制】SimpleDateFormat 是執行緒不安全的類,一般不要定義為static變數,如果定義為static,必須加鎖,或者使用DateUtils工具類。
正例:注意執行緒安全,使用DateUtils。亦推薦如下處理:private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
【強制】高併發時,同步呼叫應該去考量鎖的效能損耗。能鎖區塊,就不要鎖整個方法體;能用物件鎖,就不要用類鎖。
【強制】對多個資源、資料庫表、物件同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:執行緒一需要對錶A、B、C依次全部加鎖後才可以進行更新操作,那麼執行緒二的加鎖順序也必須是A、B、C,否則可能出現死鎖。【參考】注意HashMap的擴容死鏈,導致CPU飆升的問題。
反例:“採購直達”業務 P2故障,原因是依賴的開源軟體中對HashMap有併發寫,導致get時死迴圈,耗盡了CPU資源,參考開源框架誤用HashMap導致的線上問題分析【強制】併發修改同一記錄時,避免更新丟失,要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新依據。
說明:如果每次訪問衝突概率小於20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3次。
正例:集團很多業務使用TairManager方法:incr(namespace, lockKey, 1, 0, expireTime); 判斷返回步長是否為1,實現分散式鎖。【強制】多執行緒並行處理定時任務時,Timer執行多個TimeTask時,只要其中之一沒有捕獲丟擲的異常,其它任務便會自動終止執行,使用ScheduledExecutorService則沒有這個問題。
反例:阿里雲平臺產品技術部,域名更新具體產品資訊儲存到tair,Timer產生了RunTimeExcetion異常後,定時任務不再執行,通過檢查日誌發現原因,改為ScheduledExecutorService方式。【推薦】使用CountDownLatch進行同步轉非同步操作,每個執行緒退出前必須呼叫countDown方法,執行緒執行程式碼注意catch異常,確保countDown方法可以執行,避免主執行緒無法執行至countDown方法,直到超時才返回結果。
說明:注意,子執行緒丟擲異常堆疊,不能在主執行緒try-catch到。
反例:在“馬可波羅平臺”的翻譯同步轉非同步多執行緒時,由於翻譯過程丟擲異常,導致countDown方法失敗,經常超時才返回。【推薦】建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。
正例:public class TimerTaskThread extends Thread { public TimerTaskThread(){ super.setName("TimerTaskThread"); … }
【參考】Executors.newFixedThreadPool(int x) 注意它的佇列是無限大的[Integer.MAX_VALUE],如果消費執行緒的速度小於加入執行緒物件的速度,那麼就有可能產生OOM,推薦使用顯示指定佇列長度來生成多執行緒,並且捕獲佇列滿時,執行緒物件加入的異常。
【參考】volatile解決多執行緒記憶體不可見問題。對於一寫多讀,是可以解決變數同步問題,但是如果多寫,同樣無法解決執行緒安全問題。如果想取回count++資料,使用如下類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 參考記憶體模型:深入理解java記憶體模型
說明:count++操作如果是JDK8,推薦使用LongAdder物件,比AtomicLong效能更好(減少樂觀鎖的重試次數)。【參考】ThreadLocal無法解決共享物件的更新問題,ThreadLocal物件必須使用static修飾。這個變數是針對一個執行緒內所有操作共有的,所以設定為靜態變數,所有此類例項共享此靜態變數 ,也就是說在類第一次被使用時裝載,只分配一塊儲存空間,所有此類的物件(只要是這個執行緒內定義的)都可以操控這個變數。
控制語句:
【強制】switch語句中,每個case後,都必須要break一次;沒有任何匹配項,需要顯式宣告default標籤。
【強制】在if/for/while/switch語句中儘量使用大括號,即使只有一行程式碼,避免使用下面的形式:if (condition) statements;
【推薦】推薦儘量少用else,如下寫法:
if(condition){ ... return obj; } // 接著寫else的業務邏輯程式碼;
說明:如果使用要if-else if-else方式表達邏輯,【強制】請勿超過3層。
【推薦】不要在條件語句中執行方法,以提高可讀性。
正例:InputStream stream = File.open(fileName, "w");
if (stream != null) {…}
反例: if (File.open(fileName, "w") != null)) {…}【推薦】迴圈體中的語句要考量效能,以下操作儘量移至迴圈體外處理,如定義物件、變數、獲取資料庫連線,進行不必要的try-catch操作(這個try-catch是否可以移至迴圈體外)。
【推薦】方法中需要進行引數校驗的場景:
?1) 呼叫頻次低的方法。
?2) 執行時間開銷很大的方法,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,那得不償失。
?3) 需要極高穩定性和可用性的方法。
?4) 對外提供的開放介面,不管是HSF/API/HTTP介面。【推薦】方法中不需要引數校驗的場景:
?1) 極有可能被迴圈呼叫的方法,不建議對引數進行校驗。但在方法說明裡必須註明外部引數檢查。
?2) 底層的方法呼叫頻度都比較高,一般不校驗。畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才會暴露問題。一般DAO層與Service層都在同一個應用中,部署在同一臺伺服器中,所以DAO的引數校驗,可以省略。
?3) 被宣告成private只會被自己程式碼所呼叫的方法,如果能夠確定呼叫方法的程式碼傳入引數已經做過檢查或者肯定不會有問題,此時可以不校驗引數。
註釋規約:
【強制】類、類屬性、類方法的註釋必須使用javadoc規範,使用/**內容*/格式,不得使用//xxx方式。
說明:在IDE編輯視窗中,javadoc方式會提示相關注釋,生成javadoc可以正確輸出相應註釋;在IDE中,工程呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。【強制】所有的類都必須新增建立者資訊。
說明:未來集團會統一IDE開發模板。【強制】比較短的註釋可以放在一行中,各行的註釋儘量採用相同的縮排。如果註釋不放在同一行,那麼必須按照塊註釋的格式來寫。
【推薦】與其“半吊子”英文來註釋,不如用中文註釋把問題說清楚。專有名詞、關鍵字,保持英文原文即可。
反例:“TCP連線超時”解釋成“傳輸控制協議連線超時”,理解反而費腦筋。【參考】對於註釋的要求:第一、能夠準確反應設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。
【參考】好的命名、程式碼結構是自解釋的,註釋力求精簡準確、表達到位。避免出現註釋的一個極端:過多過濫的註釋,程式碼的邏輯一旦修改,修改註釋是相當大的負擔。
正例:// put elephant into fridge
??put(elephant, fridge);
??方法名put,加上兩個有意義的變數名elephant和fridge,已經說明了這是在幹什麼,語義清晰的程式碼不需要額外的註釋。【參考】特殊註釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源於這些標記處的程式碼。
?1) 待辦事宜(TODO):( [標記人,標記時間,[預計處理時間])
???表示需要實現,但目前還未實現的功能。這實際上是一個javadoc的標籤,目前的javadoc還沒有實現,但已經被廣泛使用。只能應用於類,介面和方法(因為它是一個javadoc標籤)。
?2) 錯誤,不能工作(FIXME):([標記人,標記時間,[預計處理時間])
???在註釋中用FIXME標記某程式碼是錯誤的,而且不能工作,需要及時糾正的情況。
其它:
【強制】在使用正則表示式時,利用好其預編譯功能,可以有效加快正則匹配速度。
說明:不要在方法體內定義:Pattern pattern = Pattern.compile(規則);【強制】避免用Apache Beanutils進行屬性的copy。
說明:Apache BeanUtils效能較差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier。【強制】velocity呼叫POJO類的屬性時,建議直接使用屬性名取值即可,模板引擎會自動按規範呼叫POJO的getXxx(),如果是boolean基本資料型別變數(注意,boolean命名不需要加is字首),會自動呼叫isXxx()方法。
說明:注意如果是Boolean包裝類物件,優先呼叫getXxx()的方法。【強制】儘量不要在vm中加入變數宣告、邏輯運算子,更不要在vm模板中加入任何複雜的邏輯。
【強制】後臺輸送給頁面的變數必須加$!{var}——中間的感嘆號。
說明:如果var=null或者不存在,那麼${var}會直接顯示在頁面上。【強制】注意 Math.random() 這個方法返回是double型別,注意取值範圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數型別的隨機數,不要將x放大10的若干倍然後取整,直接使用Random物件的nextInt或者nextLong方法。
【強制】獲取當前毫秒數:System.currentTimeMillis(); 而不是new Date().getTime();
說明:如果想獲取更加精確的納秒級時間值,用System.nanoTime。在JDK1.8中,針對統計時間等場景,推薦使用Instant類【推薦】對於“明確停止使用的程式碼和配置”,如方法、變數、類、配置檔案、動態配置屬性等要堅決從程式中清理出去,避免造成過多垃圾。清理這類垃圾程式碼是技術氣場,不要有這樣的觀念:“不做不錯,多做多錯”。
二、異常日誌
異常處理:
【強制】不要捕獲Java類庫中定義的繼承自RuntimeException的執行時異常類,如:IndexOutOfBoundsException / NullPointerException,這類異常由程式設計師預檢查來規避,保證程式健壯性。
正例:if(obj != null) {...}
反例:try { obj.method() } catch(NullPointerException e){…}【強制】異常不要用來做流程控制,條件控制,因為異常的處理效率比條件分支低。
【強制】一大段程式碼進行try-catch,這是不負責任的表現。catch時請分清穩定程式碼和非穩定程式碼,穩定程式碼指的是無論如何不會出錯的程式碼。對於非穩定程式碼的catch儘可能進行區分異常型別,再做對應的異常處理。
【強制】捕獲異常是為了處理它,不要捕獲了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的呼叫者。最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容。
【強制】有try塊放到了事務程式碼中,catch異常後,如果需要回滾事務,一定要注意手動回滾事務。
【強制】finally塊必須對資源物件、流物件進行關閉,有異常也要做try-catch。
說明:如果JDK7,可以使用try-with-resources方法。【強制】不能在finally塊中使用return,finally塊中的return返回後方法結束執行,不會再執行try塊中的return語句。
【強制】捕獲異常與拋異常,必須是完全匹配。捕獲異常必須是拋異常的父類。
說明:如果預期拋的是繡球,實際接到的是鉛球,就會產生意外情況。【推薦】方法的返回值可以為null,不強制返回空集合,或者空物件等,必須添加註釋充分說明什麼情況下會返回null值。呼叫方需要進行null判斷防止NPE問題。
說明:本規約明確防止NPE是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗,執行時異常等場景返回null的情況。【推薦】防止NPE,是程式設計師的基本修養,注意NPE產生的場景:
?1) 返回型別為包裝資料型別,有可能是null,返回int值時注意判空。
??反例:public int f(){ return Integer物件},如果為null,自動解箱拋NPE。
?2) 資料庫的查詢結果可能為null。
?3) 集合裡的元素即使isNotEmpty,取出的資料元素也可能為null。
?4) 遠端呼叫返回物件,一律要求進行NPE判斷。
?5) 對於Session中獲取的資料,建議NPE檢查,避免空指標。
?6) 級聯呼叫obj.getA().getB().getC();一連串呼叫,易產生NPE。
??反例:“一拍檔客戶”的返回值從空物件變成了null,導致線上故障,NPE無小事。【推薦】在程式碼中使用“拋異常”還是“返回錯誤碼”,對於公司外的http/api開放介面必須使用“錯誤碼”;而應用內部推薦異常丟擲;跨應用間HSF呼叫優先考慮使用Result方式,封裝isSuccess、“錯誤碼”、“錯誤簡簡訊息”。
說明:關於HSF方法返回方式使用Result方式的理由:
?1)中介軟體平臺基本上使用ResultDO來封裝,由於中介軟體的普及,本身就有標準的引導含義。
?2)使用拋異常返回方式,呼叫方如果沒有捕獲到就會產生執行時錯誤。
?3)如果不加棧資訊,只是new自定義異常,加入自己的理解的error message,對於呼叫端解決問題的幫助不會太多。如果加了棧資訊,在頻繁調用出錯的情況下,資料序列化和傳輸的效能損耗也是問題。【推薦】定義時區分unckecked / checked 異常,避免直接使用RuntimeException丟擲,更不允許丟擲Exception或者Throwable,應使用有業務含義的自定義異常。推薦業界或者集團已定義過的自定義異常,如:DaoException / ServiceException等。
日誌規約:
【強制】應用中不可直接使用日誌系統(Log4j、Logback)中的API,而應依賴使用日誌框架(SLF4J、JCL--Jakarta Commons Logging)中的API。什麼是日誌框架和日誌系統,請參考webx作者寶寶的文章,文章裡也詳細說明了為什麼不能直接依賴使用日誌系統而是日誌框架,以及應用的pom中如何做dependencyManagement。
說明:日誌框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推薦使用SLF4J):
使用SLF4J:import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Abc.class);
使用JCL:
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; private static final Log log = LogFactory.getLog(Abc.class);
參考連結:為什麼使用slf4j
【強制】日誌檔案推薦至少儲存15天,因為有些異常具備以“周”為頻次發生的特點。對於當天日誌,以“應用名.log”來儲存,儲存在/home/admin/應用名/logs/目錄下,過往日誌格式為: {logname}.log.{儲存日期},日期格式:yyyy-MM-dd
說明:以mppserver應用為例,日誌儲存在/home/admin/mppserver/logs/mppserver.log,歷史日誌名稱為mppserver.log.2016-08-01【強制】應用中的擴充套件日誌(如打點、臨時監控、訪問日誌等)命名方式:appName_logType_logName.log。logType:日誌型別,推薦分類有stats/desc/monitor/visit等;logName:日誌描述。這種命名的好處:通過檔名就可知道日誌檔案屬於什麼應用,什麼型別,什麼目的,也有利於歸類查詢。
正例:mppserver應用中單獨監控時區轉換異常,如:mppserver_monitor_timeZoneConvert.log
說明:推薦對日誌進行分類,錯誤日誌和普通提示日誌儘量分開存放,便於開發人員檢視,也便於通過日誌對系統進行及時監控。【強制】對trace/debug/info級別的日誌輸出,必須使用條件輸出形式或者使用佔位符的方式,否則大量的物件toString和字串拼接會帶來嚴重的效能問題。
正例:(條件)if (logger.isDebugEnabled()) { logger.debug("Processing trade with id: " + id + " symbol: " + symbol); }
正例:(佔位符)
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
【強制】避免重複列印日誌,浪費磁碟空間,務必在log4j.xml中設定additivity=false。
正例:<logger name="com.taobao.ecrm.member.config" additivity="false"\>
【強制】生產環境禁止直接使用System.out 或System.err 輸出日誌或使用e.printStackTrace()列印異常堆疊。由於標準日誌輸出與標準錯誤輸出檔案每次Jboss重啟時才滾動,如果大量輸出送往這兩個檔案,容易造成檔案大小超過作業系統大小限制。
【強制】異常資訊應該包括兩類資訊:案發現場資訊和異常堆疊資訊。如果不處理,那麼往上拋。
正例:logger.error(各類引數或者物件toString + "_" + e.getMessage(), e);
輸出的POJO類必須重寫toString方法,否則只輸出此物件的hashCode值(地址值),沒啥參考意義。【推薦】可以使用warn日誌級別來記錄使用者輸入引數錯誤的情況,避免使用者投訴時,無所適從。注意日誌輸出的級別,error級別只記錄系統邏輯出錯、異常、或者重要的錯誤資訊。如非必要,請不要在此場景打出error級別,避免頻繁報警。
【推薦】如果使用log.warn記錄跟蹤除錯資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並記得及時刪除這些觀察日誌。
【參考】如果日誌用英文描述不清楚,推薦使用中文註釋。對於中文UTF-8的日誌,在secureCRT中,set encoding=utf-8;如果中文字元還亂碼,請設定:全域性>預設的會話設定>外觀>字型>選擇字符集gb2312;如果還不行,執行命令:set termencoding=gbk,並且直接使用中文來進行檢索。
三、資料庫規約
建表規約:
【強制】表達是與否概念的欄位,必須使用is_xxx的方式命名,資料型別是unsigned tinyint( 1表示是,0表示否),此規則適用於odps建表方式。
說明:任何欄位如果為非負數,必須是unsigned。【強制】小數型別為decimal,禁止使用float和double。
說明:float和double在儲存的時候,存在精度損失的問題,很可能使用插入的值與資料庫中的值進行等於比較的時候,得出不正確的結果。如果儲存的資料範圍超過decimal的範圍,建議將資料拆成整數和小數分開儲存。【強制】如果儲存的字串長度幾乎相等,使用CHAR定長字串型別。
【強制】表名、欄位名必須使用小寫字母或數字,欄位命名可參考附2;禁止出現數字開頭,禁止兩個下劃線中間只出現數字。資料庫欄位名的修改代價很大,因為無法進行預釋出,所以欄位名稱需要慎重考慮。
正例:getter_admin,task_config,level3_name
反例:GetterAdmin,taskConfig,level_3_name【強制】主鍵索引命名,為pk_表名(一般情況下id為主鍵索引指定為unsigned bigint、自增、步長為1);唯一索引,則為uk_欄位名;普通索引,標記成idx_欄位名。
說明:pk_ 即primary key;uk_ 即 unique key;idx_ 即index的簡稱。【強制】表必備三欄位:id, gmt_create, gmt_modified ,其中id必為主鍵,gmt_modified最好建為索引。
說明:歷史資料歸檔與雲梯同步中心很多情形是以gmt_modified進行增量資料拉取。【強制】表名不使用複數名詞。
說明:表名應該僅僅表示表裡面的實體內容,不應該表示實體數量,對應於DO類名也是單數形式,符合表達習慣。【強制】禁用保留字,如desc、range、match、delayed等,參考官方保留字。
【推薦】欄位允許適當冗餘,以提高效能,但是必須考慮資料同步的情況。冗餘欄位應遵循:
?1)不是頻繁修改的欄位。
?2)不是varchar超長欄位,更不能是text欄位。
正例:各業務線經常冗餘儲存商品名稱,避免查詢時需要呼叫IC服務獲取。【推薦】表的命名最好是加上“業務名稱_表的作用”,避免上雲梯後,再與其它業務表關聯時有混淆。
正例:tiger_task / tiger_reader / mpp_config【推薦】庫名與應用名稱儘量一致。
索引規約:
【強制】對於varchar欄位建立索引,必須指定索引長度,沒必要對全欄位建立索引,根據實際文字區分度決定索引長度。
說明:索引的長度與區分度是一對矛盾體,一般對字串型別資料,長度為20的索引,區分度會高達90%以上,可以使用count(distinct left(列名, 索引長度))/count(*)的區分度來確定。【強制】資料庫的like儘量限制使用,頁面搜尋嚴禁左模糊或者全模糊,如果需要請走搜尋引擎來解決。
說明:索引檔案具有B-Tree的最左字首匹配特性,如果左邊的值未確定,那麼無法使用此索引。【強制】業務上具有唯一特性的欄位,即使是組合欄位,也必須建成唯一索引。
說明:不要以為唯一索引影響了insert速度,這個速度損耗可以忽略,但提高查詢速度是明顯的;另外,即使在應用層做了非常完善的校驗和控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生。【強制】儘量不要join,尤其是超過三個表的join是禁止的。需要join的欄位,資料型別保持絕對一致。多表關聯查詢時,保證被關聯的欄位需要有索引。
【強制】在表查詢中,一律不要使用 * 作為查詢的欄位列表,需要哪些欄位必須明確寫明。
說明:1)增加查詢分析器解析成本。 2)增減欄位容易與resultMap配置不一致。【推薦】利用覆蓋索引來進行查詢操作,來避免回表操作。
說明:如果一本書需要知道第11章是什麼標題,會翻開第11章對應的那一頁嗎?目錄瀏覽一下就好,這個目錄就是起到覆蓋索引的作用。
正例:IDB能夠建立索引的種類:主鍵索引、唯一索引、普通索引,而覆蓋索引是一種查詢的一種效果,用explain的結果,extra列會出現:using index.【推薦】如果有order by的場景,請注意利用索引的有序性。order by 最後的欄位是組合索引的一部分,並且放在索引組合順序的最後,避免出現file_sort的情況,影響查詢效能。
正例:where a=? and b=? order by c; 索引:a_b_c
反例:索引中有範圍查詢,那麼索引有序性無法利用,如:WHERE a>10 ORDER BY b; 索引a_b無法排序。【推薦】利用延遲關聯或者子查詢優化超多分頁場景。
說明:MySQL並不是跳過offset行,而是取offset+N行,然後返回放棄前offset行,返回N行,那當offset特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行SQL改寫。
正例:先快速定位需要獲取的id段,然後再關聯:
SELECT a.* FROM 表1 a, (select id from 表1 where 條件 LIMIT 100000,20 ) b where a.id=b.id
反例:“服務市場”某交易分頁超過1000頁,使用者點選最後一頁時,資料庫基本處於半癱瘓狀態。【推薦】SQL效能優化的目標:至少要達到 range 級別,要求是ref級別,如果可以是consts最好。
說明:
?1)consts 單表中最多隻有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到資料。
?2)ref 指的是使用普通的索引。(normal index)
?3)range 對索引進範圍檢索。
反例:explain表的結果,type=index,索引物理檔案全掃描,速度非常慢,這個index級別比較range還低,與全表掃描是小巫見大巫。【推薦】建組合索引的時候,區分度最高的在最左邊。如果where a=? and b=? ,a列的幾乎接近於唯一值,那麼只需要單建idx_a索引即可。
【推薦】in操作能避免則避免,若實在避免不了,需要仔細評估in後邊的集合元素數量,控制在1000個之內。
【推薦】如果修改欄位含義或對欄位表示的狀態追加時,需要及時更新欄位註釋。
【參考】合適的字元儲存長度,不但節約資料庫表空間、節約索引儲存,更重要的是提升檢索速度。
正例:人的年齡用unsigned tinyint(表示範圍0-255,人的壽命不會超過255歲);海龜就必須是smallint,但如果是太陽的年齡,就必須是int;如果是所有恆星的年齡都加起來,那麼就必須使用bigint。【參考】建立索引時避免有如下極端誤解:
?1)索引寧濫勿缺
誤認為一個查詢就需要建一個索引。
?2)吝嗇索引的建立
誤認為索引會消耗空間、嚴重拖慢更新和新增速度。
?3)抵制唯