【Java】阿里巴巴開發規範手冊
Java 開發手冊
一、 程式設計規約
(一) 命名風格
【強制】程式碼中的命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。
反例:_name / name / \(name / name_ / name\) / name
【強制】程式碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
說明:正確的英文拼寫和語法可以讓閱讀者易於理解,避免歧義。注意,純拼音命名方式更要避免採用。
正例:renminbi / alibaba / taobao / youku / hangzhou 等國際通用的名稱,可視同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [評分] / int 某變數 = 3【強制】類名使用 UpperCamelCase 風格,但以下情形例外:DO / BO / DTO / VO / AO/ PO / UID 等。
正例:JavaServerlessPlatform / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:javaserverlessplatform / UserDo / XMLService / TCPUDPDeal / TAPromotion【強制】方法名、引數名、成員變數、區域性變數都統一使用 lowerCamelCase 風格,必須遵從駝峰形式。
正例:localValue / getHttpMessage() / inputUserId
【強制】常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME【強制】抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾;測試類命名以它要測試的類的名稱開始,以 Test 結尾。
【強制】型別與中括號緊挨相連來表示陣列。
正例:定義整形陣列 int[] arrayDemo;
反例:在 main 引數中,使用 String args[]來定義。【強制】POJO 類中布林型別變數都不要加 is 字首,否則部分框架解析會引起序列化錯誤。
說明:在本文 MySQL 規約中的建表約定第一條,表達是與否的值採用 is_xxx 的命名方式,所以,需要在<resultMap>設定從 is_xxx 到 xxx 的對映關係。
反例:定義為基本資料型別 Boolean isDeleted 的屬性,它的方法也是 isDeleted(),RPC 框架在反向解析的時候,“誤以為”對應的屬性名稱是 deleted,導致屬性獲取不到,進而丟擲異常。【強制】包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有複數含義,類名可以使用複數形式。
正例:應用工具類包名為 com.alibaba.ai.util、類名為 MessageUtils(此規則參考 spring 的框架結構)
【強制】避免在子父類的成員變數之間、或者不同程式碼塊的區域性變數之間採用完全相同的命名,使可讀性降低。
說明:子類、父類成員變數名相同,即使是 public 型別的變數也是能夠通過編譯,而區域性變數在同一方法內的不同程式碼塊中同名也是合法的,但是要避免使用。對於非 setter/getter 的引數名稱也要避免與成員變數名稱相同。
反例:public class ConfusingName { public int age; // 非 setter/getter 的引數名稱,不允許與本類成員變數同名 public void getData(String alibaba) { if(condition) { final int money = 531; // ... } for (int i = 0; i < 10; i++) { // 在同一方法體中,不允許與其它程式碼塊中的 money 命名相同 final int money = 615; // ... } } } class Son extends ConfusingName { // 不允許與父類的成員變數名稱相同 public int age; }
【強制】杜絕完全不規範的縮寫,避免望文不知義。
反例:AbstractClass“縮寫”命名成 AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了程式碼的可閱讀性。
【推薦】為了達到程式碼自解釋的目標,任何自定義程式設計元素在命名時,使用盡量完整的單詞組合來表達其意。
正例:在 JDK 中,表達原子更新的類名為:AtomicReferenceFieldUpdater。
反例:int a 的隨意命名方式。【推薦】在常量與變數的命名時,表示型別的名詞放在詞尾,以提升辨識度。
正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD【推薦】如果模組、介面、類、方法使用了設計模式,在命名時需體現出具體模式。
說明:將設計模式體現在名字中,有利於閱讀者快速理解架構設計理念。正例:
public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;【推薦】介面類中的方法和屬性不要加任何修飾符號(public 也不要加),保持程式碼的簡潔性,並加上有效的 Javadoc 註釋。儘量不要在接口裡定義變數,如果一定要定義變數,肯定是與介面方法相關,並且是整個應用的基礎常量。
正例:
介面方法簽名 void commit();
介面基礎常量 String COMPANY = "alibaba";
反例:介面方法定義 public abstract void f();
說明:JDK8 中介面允許有預設實現,那麼這個 default 方法,是對所有實現類都有價值的預設實現。- 介面和實現類的命名有兩套規則:
【強制】對於 Service 和 DAO 類,基於 SOA 的理念,暴露出來的服務一定是介面,內部的實現類用 Impl 的字尾與介面區別。
正例:CacheServiceImpl 實現 CacheService 介面。
【推薦】如果是形容能力的介面名稱,取對應的形容詞為介面名(通常是–able 的形容詞)。
正例:AbstractTranslator 實現 Translatable 介面。
【參考】列舉類名帶上 Enum 字尾,列舉成員名稱需要全大寫,單詞間用下劃線隔開。
說明:列舉其實就是特殊的類,域成員均為常量,且構造方法被預設強制是私有。正例:列舉名字為 ProcessStatusEnum 的成員名稱:SUCCESS / UNKNOWN_REASON。
- 【參考】各層命名規約:
- Service/DAO 層方法命名規約
- 獲取單個物件的方法用 get 做字首。
- 獲取多個物件的方法用 list 做字首,複數形式結尾如:listObjects。
- 獲取統計值的方法用 count 做字首。
- 插入的方法用 save/insert 做字首。
- 刪除的方法用 remove/delete 做字首。
- 修改的方法用 update 做字首。
- 領域模型命名規約
- 資料物件:xxxDO,xxx 即為資料表名。
- 資料傳輸物件:xxxDTO,xxx 為業務領域相關的名稱。
- 展示物件:xxxVO,xxx 一般為網頁名稱。
- POJO 是 DO/DTO/BO/VO 的統稱,禁止命名成 xxxPOJO。
- Service/DAO 層方法命名規約
(二) 常量定義
【強制】不允許任何魔法值(即未經預先定義的常量)直接出現在程式碼中。
反例:
String key = "Id#taobao_" + tradeId; cache.put(key, value); // 快取 get 時,由於在程式碼複製時,漏掉下劃線,導致快取擊穿而出現問題
【強制】在 long 或者 Long 賦值時,數值後使用大寫的 L,不能是小寫的 l,小寫容易跟數字 1 混淆,造成誤解。
說明:Long a = 2l; 寫的是數字的 21,還是 Long 型的 2。【推薦】不要使用一個常量類維護所有常量,要按常量功能進行歸類,分開維護。
說明:大而全的常量類,雜亂無章,使用查詢功能才能定位到修改的常量,不利於理解和維護。正例:快取相關常量放在類 CacheConsts 下;系統配置相關常量放在類 ConfigConsts 下。
- 【推薦】常量的複用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
- 跨應用共享常量:放置在二方庫中,通常是 client.jar 中的 constant 目錄下。
應用內共享常量:放置在一方庫中,通常是子模組中的 constant 目錄下。
反例:易懂變數也要統一定義成應用內共享常量,兩位工程師在兩個類中分別定義了“YES”的變數:
類 A 中:public static final String YES = "yes";
類 B 中:public static final String YES = "y";
A.YES.equals(B.YES),預期是 true,但實際返回為 false,導致線上問題。- 子工程內部共享常量:即在當前子工程的 constant 目錄下。
- 包內共享常量:即在當前包下單獨的 constant 目錄下。
類內共享常量:直接在類內部 private static final 定義。
【推薦】如果變數值僅在一個固定範圍內變化用 enum 型別來定義。
說明:如果存在名稱之外的延伸屬性應使用 enum 型別,下面正例中的數字就是延伸資訊,表示一年中的第幾個季節。正例:
public enum SeasonEnum { SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); private int seq; SeasonEnum(int seq) { this.seq = seq; } public int getSeq() { return seq; } }
(三) 程式碼格式
- 【強制】如果是大括號內為空,則簡潔地寫成{}即可,大括號中間無需換行和空格;如果是非空程式碼塊則:
- 左大括號前不換行。
- 左大括號後換行。
- 右大括號前換行。
- 右大括號後還有 else 等程式碼則不換行;表示終止的右大括號後必須換行。
【強制】左小括號和字元之間不出現空格;同樣,右小括號和字元之間也不出現空格;而左大括號前需要空格。詳見第 5 條下方正例提示。
反例:if (空格 a == b 空格)
【強制】if/for/while/switch/do 等保留字與括號之間都必須加空格。
【強制】任何二目、三目運算子的左右兩邊都需要加一個空格。
說明:運算子包括賦值運算子=、邏輯運算子&&、加減乘除符號等。【強制】採用 4 個空格縮排,禁止使用 tab 字元。
說明:如果使用 tab 縮排,必須設定 1 個 tab 為 4 個空格。IDEA 設定 tab 為 4 個空格時,請勿勾選 Use tab character;而在 eclipse 中,必須勾選 insert spaces for tabs。正例: (涉及 1-5 點)
public static void main(String[] args) { // 縮排 4 個空格 String say = "hello"; // 運算子的左右必須有一個空格 int flag = 0; // 關鍵詞 if 與括號之間必須有一個空格,括號內的 f 與左括號,0 與右括號不需要空格 if (flag == 0) { System.out.println(say); } // 左大括號前加空格且不換行;左大括號後換行 if (flag == 1) { System.out.println("world"); // 右大括號前換行,右大括號後有 else,不用換行 } else { System.out.println("ok"); // 在右大括號後直接結束,則必須換行 } }
【強制】註釋的雙斜線與註釋內容之間有且僅有一個空格。
正例:
// 這是示例註釋,請注意在雙斜線之後有一個空格 String param = new String();
【強制】在進行型別強制轉換時,右括號與強制轉換值之間不需要任何空格隔開。
正例:
long first = 1000000000000L; int second = (int)first + 2;
- 【強制】單行字元數限制不超過 120 個,超出需要換行,換行時遵循如下原則:
- 第二行相對第一行縮排 4 個空格,從第三行開始,不再繼續縮排,參考示例。
- 運算子與下文一起換行。
- 方法呼叫的點符號與下文一起換行。
- 方法呼叫中的多個引數需要換行時,在逗號後進行。
- 在括號前不要換行,見反例。
正例:
StringBuilder sb = new StringBuilder(); // 超過 120 個字元的情況下,換行縮排 4 個空格,點號和方法名稱一起換行 sb.append("Jack").append("Ma")... .append("alibaba")... .append("alibaba")... .append("alibaba");
反例:
StringBuilder sb = new StringBuilder(); // 超過 120 個字元的情況下,不要在括號前換行 sb.append("Jack").append("Ma")...append ("alibaba"); // 引數很多的方法呼叫可能超過 120 個字元,不要在逗號前換行 method(args1, args2, args3, ... , argsX);
【強制】方法引數在定義和傳入時,多個引數逗號後邊必須加空格。
正例:下例中實參的 args1,後邊必須要有一個空格。
method(args1, args2, args3);
【強制】IDE 的 text file encoding 設定為 UTF-8; IDE 中檔案的換行符使用 Unix 格式,不要使用 Windows 格式。
【推薦】單個方法的總行數不超過 80 行。
說明:除註釋之外的方法簽名、左右大括號、方法內程式碼、空行、回車及任何不可見字元的總行數不超過 80 行。
正例:程式碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成為額外方法,使主幹程式碼更加清晰;共性邏輯抽取成為共性方法,便於複用和維護。【推薦】沒有必要增加若干空格來使變數的賦值等號與上一行對應位置的等號對齊。
正例:
int one = 1; long two = 2L; float three = 3F; StringBuilder sb = new StringBuilder();
說明:增加 sb 這個變數,如果需要對齊,則給 one、two、three 都要增加幾個空格,在變數比較多的情況下,是非常累贅的事情。
【推薦】不同邏輯、不同語義、不同業務的程式碼之間插入一個空行分隔開來以提升可讀性。
說明:任何情形,沒有必要插入多個空行進行隔開。
(四) OOP 規約
【強制】避免通過一個類的物件引用訪問此類的靜態變數或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
【強制】所有的覆寫方法,必須加@Override 註解。
說明:getObject()與 get0bject()的問題。一個是字母的 O,一個是數字的 0,加@Override 可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
【強制】相同引數型別,相同業務含義,才可以使用 Java 的可變引數,避免使用 Object。
說明:可變引數必須放置在引數列表的最後。(提倡同學們儘量不用可變引數程式設計)
正例:public List<User> listUsers(String type, Long... ids) {...}
【強制】外部正在呼叫或者二方庫依賴的介面,不允許修改方法簽名,避免對介面呼叫方產生影響。介面過時必須加@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(JDK7 引入的工具類)。【強制】所有整型包裝類物件之間值的比較,全部使用 equals 方法比較。
說明:對於 Integer var = ? 在-128 至 127 範圍內的賦值,Integer 物件是在 IntegerCache.cache 產生,會複用已有物件,這個區間內的 Integer 值可以直接使用==進行判斷,但是這個區間之外的所有資料,都會在堆上產生,並不會複用已有物件,這是一個大坑,推薦使用 equals 方法進行判斷。
【強制】浮點數之間的等值判斷,基本資料型別不能用==來比較,包裝資料型別不能用 equals 來判斷。
說明:浮點數採用“尾數+階碼”的編碼方式,類似於科學計數法的“有效數字+指數”的表示方式。二進位制無法精確表示大部分的十進位制小數,具體原理參考《碼出高效》。
反例:float a = 1.0f - 0.9f; float b = 0.9f - 0.8f; if (a == b) { // 預期進入此程式碼快,執行其它業務邏輯 // 但事實上 a==b 的結果為 false } Float x = Float.valueOf(a); Float y = Float.valueOf(b); if (x.equals(y)) { // 預期進入此程式碼快,執行其它業務邏輯 // 但事實上 equals 的結果為 false }
正例:
(1) 指定一個誤差範圍,兩個浮點數的差值在此範圍之內,則認為是相等的。
float a = 1.0f - 0.9f; float b = 0.9f - 0.8f; float diff = 1e-6f; if (Math.abs(a - b) < diff) { System.out.println("true"); }
(2) 使用 BigDecimal 來定義值,再進行浮點數的運算操作。
BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); BigDecimal x = a.subtract(b); BigDecimal y = b.subtract(c); if (x.equals(y)) { System.out.println("true"); }
【強制】定義資料物件 DO 類時,屬性型別要與資料庫欄位型別相匹配。
正例:資料庫欄位的 bigint 必須與類屬性的 Long 型別相對應。
反例:某個案例的資料庫表 id 欄位定義型別 bigint unsigned,實際類物件屬性為 Integer,隨著 id 越來越大,超過 Integer 的表示範圍而溢位成為負數。【強制】為了防止精度損失,禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化為 BigDecimal 物件。
說明:BigDecimal(double)存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。
如:BigDecimal g = new BigDecimal(0.1f); 實際的儲存值為:0.10000000149
正例:優先推薦入參為 String 的構造方法,或使用 BigDecimal 的 valueOf 方法,此方法內部其實執行了 Double 的 toString,而 Double 的 toString 按 double 的實際能表達的精度對尾數進行了截斷。BigDecimal recommend1 = new BigDecimal("0.1"); BigDecimal recommend2 = BigDecimal.valueOf(0.1);
- 關於基本資料型別與包裝資料型別的使用標準如下:
- 【強制】所有的 POJO 類屬性必須使用包裝資料型別。
- 【強制】RPC 方法的返回值和引數必須使用包裝資料型別。
【推薦】所有的區域性變數使用基本資料型別。
說明:POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何 NPE 問題,或者入庫檢查,都由使用者來保證。
正例:資料庫的查詢結果可能是 null,因為自動拆箱,用基本資料型別接收有 NPE 風險。
反例:比如顯示成交總額漲跌情況,即正負 x%,x 為基本資料型別,呼叫的 RPC 服務,呼叫不成功時,返回的是預設值,頁面顯示為 0%,這是不合理的,應該顯示成中劃線。所以包裝資料型別的 null 值,能夠表示額外的資訊,如:遠端呼叫失敗,異常退出。
【強制】定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性預設值。
反例:POJO 類的 createTime 預設值為 new Date(),但是這個屬性在資料提取時並沒有置入具體值,在更新其它欄位時又附帶更新了此欄位,導致建立時間被修改成當前時間。
【強制】序列化類新增屬性時,請不要修改 serialVersionUID 欄位,避免反序列失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。
說明:注意 serialVersionUID 不一致會丟擲序列化執行時異常。
【強制】構造方法裡面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
【強制】POJO 類必須寫 toString 方法。使用 IDE 中的工具:source> generate toString 時,如果繼承了另一個 POJO 類,注意在前面加一下 super.toString。
說明:在方法執行丟擲異常時,可以直接呼叫 POJO 的 toString()方法列印其屬性值,便於排查問題。
【強制】禁止在 POJO 類中,同時存在對應屬性 xxx 的 isXxx()和 getXxx()方法。
說明:框架在呼叫屬性 xxx 的提取方法時,並不能確定哪個方法一定是被優先呼叫到。
【推薦】使用索引訪問用 String 的 split 方法得到的陣列時,需做最後一個分隔符後有無內容的檢查,否則會有拋 IndexOutOfBoundsException 的風險。
說明:
String str = "a,b,c,,"; String[] ary = str.split(","); // 預期大於 3,結果是 3 System.out.println(ary.length);
【推薦】當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀,此條規則優先於下一條。
【推薦】類內方法定義的順序依次是:公有方法或保護方法 > 私有方法 > getter / setter 方法。
說明:公有方法是類的呼叫者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是“模板設計模式”下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因為承載的資訊價值較低,所有 Service 和 DAO 的 getter/setter 方法放在類體最後。
【推薦】setter 方法中,引數名稱與類成員變數名稱一致,this.成員名 = 引數名。在 getter/setter 方法中,不要增加業務邏輯,增加排查問題的難度。
反例:
public Integer getData() { if (condition) { return this.data + 100; } else { return this.data - 100; } }
【推薦】迴圈體內,字串的連線方式,使用 StringBuilder 的 append 方法進行擴充套件。
說明:下例中,反編譯出的位元組碼檔案顯示每次迴圈都會 new 出一個 StringBuilder 物件,然後進行 append 操作,最後通過 toString 方法返回 String 物件,造成記憶體資源浪費。
反例:String str = "start"; for (int i = 0; i < 100; i++) { str = str + "hello"; }
- 【推薦】final 可以宣告類、成員變數、方法、以及本地變數,下列情況使用 final 關鍵字:
- 不允許被繼承的類,如:String 類。
- 不允許修改引用的域物件。
- 不允許被覆寫的方法,如:POJO 類的 setter 方法。
- 不允許執行過程中重新賦值的區域性變數。
- 避免上下文重複使用一個變數,使用 final 可以強制重新定義一個變數,方便更好地進行重構。
【推薦】慎用 Object 的 clone 方法來拷貝物件。
說明:物件 clone 方法預設是淺拷貝,若想實現深拷貝需覆寫 clone 方法實現域物件的深度遍歷式拷貝。
- 【推薦】類成員與方法訪問控制從嚴:
- 如果不允許外部直接通過 new 來建立物件,那麼構造方法必須是 private。
- 工具類不允許有 public 或 default 構造方法。
- 類非 static 成員變數並且與子類共享,必須是 protected。
- 類非 static 成員變數並且僅在本類使用,必須是 private。
- 類 static 成員變數如果僅在本類使用,必須是 private。
- 若是 static 成員變數,考慮是否為 final。
- 類成員方法只供類內部呼叫,必須是 private。
類成員方法只對繼承類公開,那麼限制為 protected。
說明:任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解耦。思考:如果是一個 private 的方法,想刪除就刪除,可是一個 public 的 service 成員方法或成員變數,刪除一下,不得手心冒點汗嗎?變數像自己的小孩,儘量在自己的視線內,變數作用域太大,無限制的到處跑,那麼你會擔心的。
(五) 集合處理
- 【強制】關於 hashCode 和 equals 的處理,遵循如下規則:
- 只要覆寫 equals,就必須覆寫 hashCode。
- 因為 Set 儲存的是不重複的物件,依據 hashCode 和 equals 進行判斷,所以 Set 儲存的物件必須覆 寫這兩個方法。
如果自定義物件作為 Map 的鍵,那麼必須覆寫 hashCode 和 equals。
說明: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 子列表的所有操作最終會反映到原列表上。
【強制】使用 Map 的方法 keySet()/values()/entrySet()返回集合物件時,不可以對其進行新增元素操作,否則會丟擲 UnsupportedOperationException 異常。
【強制】Collections 類返回的物件,如:emptyList()/singletonList()等都是 immutable list,不可對其進行新增或者刪除元素的操作。
反例:如果查詢無結果,返回 Collections.emptyList()空集合物件,呼叫方一旦進行了新增元素的操作,就會觸發 UnsupportedOperationException 異常。
【強制】在 subList 場景中,高度注意對原集合元素的增加或刪除,均會導致子列表的遍歷、增加、刪除產生 ConcurrentModificationException 異常。
【強制】使用集合轉陣列的方法,必須使用集合的 toArray(T[] array),傳入的是型別完全一致、長度為 0 的空陣列。
反例:直接使用 toArray 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它型別陣列將出現 ClassCastException 錯誤。
正例:List<String> list = new ArrayList<>(2); list.add("guan"); list.add("bao"); String[] array = list.toArray(new String[0]);
說明:使用 toArray 帶參方法,陣列空間大小的 length:
- 等於 0,動態建立與 size 相同的陣列,效能最好。
- 大於 0 但小於 size,重新建立大小等於 size 的陣列,增加 GC 負擔。
- 等於 size,在高併發情況下,陣列建立完成之後,size 正在變大的情況下,負面影響與上相同。
- 大於 size,空間浪費,且在 size 處插入 null 值,存在 NPE 隱患。
【強制】在使用 Collection 介面任何實現類的 addAll()方法時,都要對輸入的集合引數進行NPE 判斷。
說明:在 ArrayList#addAll 方法的第一行程式碼即 Object[] a = c.toArray(); 其中 c 為輸入集合引數,如果為 null,則直接丟擲異常。
【強制】使用工具類 Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法會丟擲 UnsupportedOperationException 異常。
說明:asList 的返回物件是一個 Arrays 內部類,並沒有實現集合的修改方法。Arrays.asList 體現的是介面卡模式,只是轉換介面,後臺的資料仍是陣列。
String[] str = new String[] { "yang", "hao" }; List list = Arrays.asList(str);
第一種情況:list.add("yangguanbao"); 執行時異常。
第二種情況:str[0] = "changed"; 也會隨之修改,反之亦然。【強制】泛型萬用字元<? extends T>來接收返回的資料,此寫法的泛型集合不能使用 add 方 法,而<? super T>不能使用 get 方法,作為介面呼叫賦值時易出錯。
說明:擴充套件說一下 PECS(Producer Extends Consumer Super)原則:
第一、頻繁往外讀取內容的,適合用<? extends T>。
第二、經常往裡插入的,適合用<? super T>【強制】在無泛型限制定義的集合賦值給泛型限制的集合時,在使用集合元素時,需要進行 instanceof 判斷,避免丟擲 ClassCastException 異常。
說明:畢竟泛型是在 JDK5 後才出現,考慮到向前相容,編譯器是允許非泛型集合與泛型集合互相賦值。
反例:List<String> generics = null; List notGenerics = new ArrayList(10); notGenerics.add(new Object()); notGenerics.add(new Integer(1)); generics = notGenerics; // 此處丟擲 ClassCastException 異常 String string = generics.get(0);
【強制】不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 物件加鎖。
正例:
List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (刪除元素的條件) { iterator.remove(); } }
反例:
for (String item : list) { if ("1".equals(item)) { list.remove(item); } }
說明:以上程式碼的執行結果肯定會出乎大家的意料,那麼試一下把“1”換成“2”,會是同樣的結果嗎?
【強制】在 JDK7 版本及以上,Comparator 實現類要滿足如下三個條件,不然 Arrays.sort,Collections.sort 會拋 IllegalArgumentException 異常。
說明:三個條件如下
- x,y 的比較結果和 y,x 的比較結果相反。
- x>y,y>z,則 x>z。 3) x=y,則 x,z 比較結果和 y,z 比較結果相同。
反例:下例中沒有處理相等的情況,交換兩個物件判斷結果並不互反,不符合第一個條件,在實際使用中可能會出現異常。
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; } };
【推薦】集合泛型定義時,在 JDK7 及以上,使用 diamond 語法或全省略。
說明:菱形泛型,即 diamond,直接使用<>來指代前邊已經指定的型別。
正例:// diamond 方式,即<> HashMap<String, String> userCache = new HashMap<>(16); // 全省略方式 ArrayList<User> users = new ArrayList(10);
【推薦】集合初始化時,指定集合初始值大小。
說明:HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity = (需要儲存的元素個數 / 負載因子) + 1。注意負載因子(即 loader factor)預設為 0.75,如果暫時無法確定初始值大小,請設定為 16(即預設值)。
反例:HashMap 需要放置 1024 個元素,由於沒有設定容量初始大小,隨著元素不斷增加,容量 7 次被迫擴大,resize 需要重建 hash 表,嚴重影響效能。【推薦】使用 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 鎖分段技術(JDK8:CAS) TreeMap 不允許為 null 允許為 null AbstractMap 執行緒不安全 HashMap 允許為 null 允許為 null AbstractMap 執行緒不安全 反例:由於 HashMap 的干擾,很多人認為 ConcurrentHashMap 是可以置入 null 值,而事實上,儲存 null 值時會丟擲 NPE 異常。
【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。
說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。
【參考】利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的 contains 方法進行遍歷、對比、去重操作。
(六) 併發處理
【強制】獲取單例物件需要保證執行緒安全,其中的方法也要保證執行緒安全。
說明:資源驅動類、工具類、單例工廠類都需要注意。
【強制】建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。
正例:自定義執行緒工廠,並且根據外部特徵進行分組,比如機房資訊。
public class UserThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1); // 定義執行緒組名稱,在 jstack 問題排查時,非常有幫助 UserThreadFactory(String whatFeaturOfGroup) { namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-"; } @Override public Thread newThread(Runnable task) { String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null, task, name, 0, false); System.out.println(thread.getName()); return thread; } }
【強制】執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒。
說明:執行緒池的好處是減少在建立和銷燬執行緒上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。
【強制】執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
說明:Executors 返回的執行緒池物件的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool:
允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。【強制】SimpleDateFormat 是執行緒不安全的類,一般不要定義為 static 變數,如果定義為 static,必須加鎖,或者使用 DateUtils 工具類。
正例:注意執行緒安全,使用 DateUtils。亦推薦如下處理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。
【強制】必須回收自定義的 ThreadLocal 變數,尤其線上程池場景下,執行緒經常會被複用,如果不清理自定義的 ThreadLocal 變數,可能會影響後續業務邏輯和造成記憶體洩露等問題。儘量在代理中使用 try-finally 塊進行回收。
正例:
objectThreadLocal.set(userInfo); try { // ... } finally { objectThreadLocal.remove(); }
【強制】高併發時,同步呼叫應該去考量鎖的效能損耗。能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用物件鎖,就不要用類鎖。
說明:儘可能使加鎖的程式碼塊工作量儘可能的小,避免在鎖程式碼塊中呼叫 RPC 方法。
【強制】對多個資源、資料庫表、物件同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:執行緒一需要對錶 A、B、C 依次全部加鎖後才可以進行更新操作,那麼執行緒二的加鎖順序也必須是 A、B、C,否則可能出現死鎖。
【強制】在使用阻塞等待獲取鎖的方式中,必須在 try 程式碼塊之外,並且在加鎖方法與 try 程式碼塊之間沒有任何可能丟擲異常的方法呼叫,避免加鎖成功後,在 finally 中無法解鎖。
說明一:如果在 lock 方法與 try 程式碼塊之間的方法呼叫丟擲異常,那麼無法解鎖,造成其它執行緒無法成功獲取鎖。
說明二:如果 lock 方法在 try 程式碼塊之內,可能由於其它方法丟擲異常,導致在 finally 程式碼塊中,unlock 對未加鎖的物件解鎖,它會呼叫 AQS 的 tryRelease 方法(取決於具體實現類),丟擲 IllegalMonitorStateException 異常。
說明三:在 Lock 物件的 lock 方法實現中可能丟擲 unchecked 異常,產生的後果與說明二相同。
正例:Lock lock = new XxxLock(); // ... lock.lock(); try { doSomething(); doOthers(); } finally { lock.unlock(); }
反例:
Lock lock = new XxxLock(); // ... try { // 如果此處丟擲異常,則直接執行 finally 程式碼塊 doSomething(); // 無論加鎖是否成功,finally 程式碼塊都會執行 lock.lock(); doOthers(); } finally { lock.unlock(); }
【強制】在使用嘗試機制來獲取鎖的方式中,進入業務程式碼塊之前,必須先判斷當前執行緒是否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同。
說明:Lock 物件的 unlock 方法在執行時,它會呼叫 AQS 的 tryRelease 方法(取決於具體實現類),如果當前執行緒不持有鎖,則丟擲 IllegalMonitorStateException 異常。
正例:Lock lock = new XxxLock(); // ... boolean isLocked = lock.tryLock(); if (isLocked) { try { doSomething(); doOthers(); } finally { lock.unlock(); } }
【強制】併發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用 version 作為更新依據。
說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。
【強制】多執行緒並行處理定時任務時,Timer 執行多個 TimeTask 時,只要其中之一沒有捕獲丟擲的異常,其它任務便會自動終止執行,如果在處理定時任務時使用 ScheduledExecutorService 則沒有這個問題。
【推薦】資金相關的金融敏感資訊,使用悲觀鎖策略。
說明:樂觀鎖在獲得鎖的同時已經完成了更新操作,校驗邏輯容易出現漏洞,另外,樂觀鎖對衝突的解決策略有較複雜的要求,處理不當容易造成系統壓力或資料異常,所以資金相關的金融敏感資訊不建議使用樂觀鎖更新。
【推薦】使用 CountDownLatch 進行非同步轉同步操作,每個執行緒退出前必須呼叫 countDown 方法,執行緒執行程式碼注意 catch 異常,確保 countDown 方法被執行到,避免主執行緒無法執行至 await 方法,直到超時才返回結果。
說明:注意,子執行緒丟擲異常堆疊,不能在主執行緒 try-catch 到。
【推薦】避免 Random 例項被多執行緒使用,雖然共享該例項是執行緒安全的,但會因競爭同一 seed 導致的效能下降。
說明:Random 例項包括 java.util.Random 的例項或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個執行緒持有一個例項。【推薦】在併發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解決方案中較為簡單一種(適用於 JDK5 及以上版本),將目標屬性宣告為 volatile 型。
反例:
public class LazyInitDemo { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } // other methods and fields... }
【參考】volatile 解決多執行緒記憶體不可見問題。對於一寫多讀,是可以解決變數同步問題,但
是如果多寫,同樣無法解決執行緒安全問題。說明:如果是 count++操作,使用如下類實現:AtomicInteger count = new AtomicInteger();
count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 物件,比 AtomicLong 效能更好(減少樂觀
鎖的重試次數)。【參考】HashMap 在容量不夠進行 resize 時由於高併發可能出現死鏈,導致 CPU 飆升,在
開發過程中可以使用其它資料結構或加鎖來規避此風險。【參考】ThreadLocal 物件使用 static 修飾,ThreadLocal 無法解決共享物件的更新問題。
說明:這個變數是針對一個執行緒內所有操作共享的,所以設定為靜態變數,所有此類例項共享此靜態變
量,也就是說在類第一次被使用時裝載,只分配一塊儲存空間,所有此類的物件(只要是這個執行緒內定義
的)都可以操控這個變數。
(七) 控制語句
【強制】在一個 switch 塊內,每個 case 要麼通過 continue/break/return 等來終止,要麼註釋說明程式將繼續執行到哪一個 case 為止;在一個 switch 塊內,都必須包含一個 default 語句並且放在最後,即使它什麼程式碼也沒有。
說明:注意 break 是退出 switch 語句塊,而 return 是退出方法體。
【強制】當 switch 括號內的變數型別為 String 並且此變數為外部引數時,必須先進行 null 判斷。
反例:猜猜下面的程式碼輸出是什麼?
public class SwitchString { public static void main(String[] args) { method(null); } public static void method(String param) { switch (param) { // 肯定不是進入這裡 case "sth": System.out.println("it's sth"); break; // 也不是進入這裡 case "null": System.out.println("it's null"); break; // 也不是進入這裡 default: System.out.println("default"); } } }
【強制】在 if/else/for/while/do 語句中必須使用大括號。
說明:即使只有一行程式碼,避免採用單行的編碼方式:if (condition) statements;
【強制】在高併發場景中,避免使用”等於”判斷作為中斷或退出的條件。
說明:如果併發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,使用大於或小於的區間判斷條件來代替。
反例:判斷剩餘獎品數量等於 0 時,終止發放獎品,但因為併發處理錯誤導致獎品數量瞬間變成了負數,這樣的話,活動無法終止。【推薦】表達異常的分支時,少用 if-else 方式,這種方式可以改寫成:
if (condition) { ... return obj; } // 接著寫 else 的業務邏輯程式碼;
說明:如果非使用 if()...else if()...else...方式表達邏輯,避免後續程式碼維護困難,【強制】請勿超過 3 層。
正例:超過 3 層的 if-else 的邏輯判斷程式碼可以使用衛語句、策略模式、狀態模式等來實現,其中衛語句即程式碼邏輯先考慮失敗、異常、中斷、退出等直接返回的情況,以方法多個出口的方式,解決程式碼中判斷分支巢狀的問題,這是逆向思維的體現。
示例如下:public void findBoyfriend(Man man) { if (man.isUgly()) { System.out.println("本姑娘是外貌協會的資深會員"); return; } if (man.isPoor()) { System.out.println("貧賤夫妻百事哀"); return; } if (man.isBadTemper()) { System.out.println("銀河有多遠,你就給我滾多遠"); return; } System.out.println("可以先交往一段時間看看"); }
【推薦】除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行其它複雜的語句,將複雜邏輯判斷的結果賦值給一個有意義的布林變數名,以提高可讀性。
說明:很多 if 語句內的邏輯表示式相當複雜,與、或、取反混合運算,甚至各種方法縱深呼叫,理解成本非常高。如果賦值一個非常好理解的布林變數名字,則是件令人爽心悅目的事情。
正例:// 虛擬碼如下 final boolean existed = (file.open(fileName, "w") != null) && (...) || (...); if (existed) { ... }
反例:
public final void acquire(long arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } }
【推薦】不要在其它表示式(尤其是條件表示式)中,插入賦值語句。
說明:賦值點類似於人體的穴位,對於程式碼的理解至關重要,所以賦值語句需要清晰地單獨成為一行。
反例:public Lock getLock(boolean fair) { // 算術表示式中出現賦值操作,容易忽略 count 值已經被改變 threshold = (count = Integer.MAX_VALUE) - 1; // 條件表示式中出現賦值操作,容易誤認為是 sync==fair return (sync = fair) ? new FairSync() : new NonfairSync(); }
【推薦】迴圈體中的語句要考量效能,以下操作儘量移至迴圈體外處理,如定義物件、變數、獲取資料庫連線,進行不必要的 try-catch 操作(這個 try-catch 是否可以移至迴圈體外)。
【推薦】避免採用取反邏輯運算子。
說明:取反邏輯不利於快速理解,並且取反邏輯寫法必然存在對應的正向邏輯寫法。
正例:使用 if (x < 628) 來表達 x 小於 628。
反例:使用 if (!(x >= 628)) 來表達 x 小於 628。【推薦】介面入參保護,這種場景常見的是用作批量操作的介面。
- 【參考】下列情形,需要進行引數校驗:
- 呼叫頻次低的方法。
- 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,那得不償失。
- 需要極高穩定性和可用性的方法。
- 對外提供的開放介面,不管是 RPC/API/HTTP 介面。
- 敏感許可權入口。
- 【參考】下列情形,不需要進行引數校驗:
- 極有可能被迴圈呼叫的方法。但在方法說明裡必須註明外部引數檢查要求。
- 底層呼叫頻度比較高的方法。畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才會暴露問題。一般 DAO 層與 Service 層都在同一個應用中,部署在同一臺伺服器中,所以 DAO 的引數校驗,可以省略。
- 被宣告成 private 只會被自己程式碼所呼叫的方法,如果能夠確定呼叫方法的程式碼傳入引數已經做過檢查或者肯定不會有問題,此時可以不校驗引數。
(八) 註釋規約
【強制】類、類屬性、類方法的註釋必須使用 Javadoc 規範,使用/**內容*/格式,不得使用// xxx 方式。
說明:在 IDE 編輯視窗中,Javadoc 方式會提示相關注釋,生成 Javadoc 可以正確輸出相應註釋;在 IDE 中,工程呼叫方法時,不進入方法即可懸浮提示方法、引數、返回值的意義,提高閱讀效率。
【強制】所有的抽象方法(包括介面中的方法)必須要用 Javadoc 註釋、除了返回值、引數、異常說明外,還必須指出該方法做什麼事情,實現什麼功能。
說明:對子類的實現要求,或者呼叫注意事項,請一併說明。
【強制】所有的類都必須新增建立者和建立日期。
【強制】方法內部單行註釋,在被註釋語句上方另起一行,使用//註釋。方法內部多行註釋使用/* */註釋,注意與程式碼對齊。
【強制】所有的列舉型別欄位必須要有註釋,說明每個資料項的用途。
【推薦】與其“半吊子”英文來註釋,不如用中文註釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。
反例:“TCP 連線超時”解釋成“傳輸控制協議連線超時”,理解反而費腦筋。
【推薦】程式碼修改的同時,註釋也要進行相應的修改,尤其是引數、返回值、異常、核心邏輯等的修改。
說明:程式碼與註釋更新不同步,就像路網與導航軟體更新不同步一樣,如果導航軟體嚴重滯後,就失去了導航的意義。
【參考】謹慎註釋掉程式碼。在上方詳細說明,而不是簡單地註釋掉。如果無用,則刪除。
說明:程式碼被註釋掉有兩種可能性:1)後續會恢復此段程式碼邏輯。2)永久不用。前者如果沒有備註資訊,難以知曉註釋動機。後者建議直接刪掉(程式碼倉庫已然儲存了歷史程式碼)。
【參考】對於註釋的要求:第一、能夠準確反映設計思想和程式碼邏輯;第二、能夠描述業務含義,使別的程式設計師能夠迅速瞭解到程式碼背後的資訊。完全沒有註釋的大段程式碼對於閱讀者形同天書,註釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;註釋也是給繼任者看的,使其能夠快速接替自己的工作。
【參考】好的命名、程式碼結構是自解釋的,註釋力求精簡準確、表達到位。避免出現註釋的一個極端:過多過濫的註釋,程式碼的邏輯一旦修改,修改註釋是相當大的負擔。
反例:
// put elephant into fridge put(elephant, fridge);
方法名 put,加上兩個有意義的變數名 elephant 和 fridge,已經說明了這是在幹什麼,語義清晰的程式碼不需要額外的註釋。
- 【參考】特殊註釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描,經常清理此類標記。線上故障有時候就是來源於這些標記處的程式碼。
- 待辦事宜(TODO):(標記人,標記時間,[預計處理時間])
表示需要實現,但目前還未實現的功能。這實際上是一個 Javadoc 的標籤,目前的 Javadoc 還沒有實現,但已經被廣泛使用。只能應用於類,介面和方法(因為它是一個 Javadoc 標籤)。 - 錯誤,不能工作(FIXME):(標記人,標記時間,[預計處理時間])
在註釋中用 FIXME 標記某程式碼是錯誤的,而且不能工作,需要及時糾正的情況。
- 待辦事宜(TODO):(標記人,標記時間,[預計處理時間])
(九) 其它
【強制】在使用正則表示式時,利用好其預編譯功能,可以有效加快正則匹配速度。
說明:不要在方法體內定義:Pattern pattern = Pattern.compile(“規則”);
【強制】velocity 呼叫 POJO 類的屬性時,直接使用屬性名取值即可,模板引擎會自動按規範呼叫 POJO 的 getXxx(),如果是 boolean 基本資料型別變數(boolean 命名不需要加 is 前 綴),會自動呼叫 isXxx()方法。
說明:注意如果是 Boolean 包裝類物件,優先呼叫 getXxx()的方法。
【強制】後臺輸送給頁面的變數必須加\(!{var}——中間的感嘆號。 > **說明:**如果 var 等於 null 或者不存在,那麼\){var}會直接顯示在頁面上。
【強制】注意 Math.random() 這個方法返回是 double 型別,注意取值的範圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數型別的隨機數,不要將 x 放大 10 的若干倍然後取整,直接使用 Random 物件的 nextInt 或者 nextLong 方法。
【強制】獲取當前毫秒數 System.currentTimeMillis(); 而不是 new Date().getTime();
說明:如果想獲取更加精確的納秒級時間值,使用 System.nanoTime()的方式。在 JDK8 中,針對統計時間等場景,推薦使用 Instant 類。
【強制】日期格式化時,傳入 pattern 中表示年份統一使用小寫的 y。
說明:日期格式化時,yyyy 表示當天所在的年,而大寫的 YYYY 代表是 week in which year(JDK7 之後引入的概念),意思是當天所在的周屬於的年份,一週從週日開始,週六結束,只要本週跨年,返回的 YYYY 就是下一年。另外需要注意:
- 表示月份是大寫的 M
- 表示分鐘則是小寫的 m
- 24 小時制的是大寫的 H
- 12 小時制的則是小寫的 h
正例:表示日期和時間的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
【推薦】不要在檢視模板中加入任何複雜的邏輯。
說明:根據 MVC 理論,檢視的職責是展示,不要搶模型和控制器的活。
【推薦】任何資料結構的構造或初始化,都應指定大小,避免資料結構無限增長吃光記憶體。
【推薦】及時清理不再使用的程式碼段或配置資訊。
說明:對於垃圾程式碼或過時配置,堅決清理乾淨,避免程式過度臃腫,程式碼冗餘。
正例:對於暫時被註釋掉,後續可能恢復使用的程式碼片斷,在註釋程式碼上方,統一規定使用三個斜槓(///)來說明註釋掉程式碼的理由。
二、異常日誌
(一) 異常處理
【強制】Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過 catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException 等等。
說明:無法通過預檢查的異常除外,比如,在解析字串形式的數字時,可能存在數字格式錯誤,不得不通過 catch NumberFormatException 來實現。
正例:if (obj != null) {...}
反例:try { obj.method(); } catch (NullPointerException e) {…}【強制】異常不要用來做流程控制,條件控制。
說明:異常設計的初衷是解決程式執行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
【強制】catch 時請分清穩定程式碼和非穩定程式碼,穩定程式碼指的是無論如何不會出錯的程式碼。
對於非穩定程式碼的 catch 儘可能進行區分異常型別,再做對應的異常處理。說明:對大段程式碼進行 try-catch,使程式無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。
正例:使用者註冊的場景中,如果使用者輸入非法字元,或使用者名稱稱已存在,或使用者輸入密碼過於簡單,在程式上作出分門別類的判斷,並提示給使用者。【強制】捕獲異常是為了處理它,不要捕獲了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的呼叫者。最外層的業務使用者,必須處理異常,將其轉化為使用者可以理解的內容。
【強制】有 try 塊放到了事務程式碼中,catch 異常後,如果需要回滾事務,一定要注意手動回滾事務。
【強制】finally 塊必須對資源物件、流物件進行關閉,有異常也要做 try-catch。
說明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
【強制】不要在 finally 塊中使用 return。
說明:try 塊中的 return 語句執行成功後,並不馬上返回,而是繼續執行 finally 塊中的語句,如果此處存在 return 語句,則在此直接返回,無情丟棄掉 try 塊中的返回點。
反例:private int x = 0; public int checkReturn() { try { // x 等於 1,此處不返回 return ++x; } finally { // 返回的結果是 2 return ++x; } }
【強制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。
說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。
【強制】在呼叫 RPC、二方包、或動態生成類的相關方法時,捕捉異常必須使用 Throwable 類來進行攔截。
說明:通過反射機制來呼叫方法,如果找不到方法,丟擲 NoSuchMethodException。什麼情況會丟擲 NoSuchMethodError 呢?二方包在類衝突時,仲裁機制可能導致引入非預期的版本使類的方法簽名不匹配,或者在位元組碼修改框架(比如:ASM)動態建立或修改類時,修改了相應的方法簽名。這些情況,即使程式碼編譯期是正確的,但在程式碼執行期時,會丟擲 NoSuchMethodError。
【推薦】方法的返回值可以為 null,不強制返回空集合,或者空物件等,必須添加註釋充分說明什麼情況下會返回 null 值。
說明:本手冊明確防止 NPE 是呼叫者的責任。即使被呼叫方法返回空集合或者空物件,對呼叫者來說,也並非高枕無憂,必須考慮到遠端呼叫失敗、序列化失敗、執行時異常等場景返回 null 的情況。
- 【推薦】防止 NPE,是程式設計師的基本修養,注意 NPE 產生的場景:
返回型別為基本資料型別,return 包裝資料型別的物件時,自動拆箱有可能產生 NPE。
反例:public int f() { return Integer 物件}, 如果為 null,自動解箱拋 NPE。
- 資料庫的查詢結果可能為 null。
- 集合裡的元素即使 isNotEmpty,取出的資料元素也可能為 null。
- 遠端呼叫返回物件時,一律要求進行空指標判斷,防止 NPE。
- 對於 Session 中獲取的資料,建議進行 NPE 檢查,避免空指標。
級聯呼叫 obj.getA().getB().getC();一連串呼叫,易產生 NPE。
正例:使用 JDK8 的 Optional 類來防止 NPE 問題。
【推薦】定義時區分 unchecked / checked 異常,避免直接丟擲 new RuntimeException(),更不允許丟擲 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException 等。
【參考】對於公司外的 http/api 開放介面必須使用“錯誤碼”;而應用內部推薦異常丟擲;跨應用間 RPC 呼叫優先考慮使用 Result 方式,封裝 isSuccess()方法、“錯誤碼”、“錯誤簡簡訊息”。
說明:關於 RPC 方法返回方式使用 Result 方式的理由:
- 使用拋異常返回方式,呼叫方如果沒有捕獲到就會產生執行時錯誤。
- 如果不加棧資訊,只是 new 自定義異常,加入自己的理解的 error message,對於呼叫端解決問題的幫助不會太多。如果加了棧資訊,在頻繁調用出錯的情況下,資料序列化和傳輸的效能損耗也是問題。
【參考】避免出現重複的程式碼(Don't Repeat Yourself),即 DRY 原則。
說明:隨意複製和貼上程式碼,必然會導致程式碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是元件化。
正例:一個類中有多個 public 方法,都需要進行數行相同的引數校驗操作,這個時候請抽取:private boolean checkParam(DTO dto) {...}
(二) 日誌規約
【強制】應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架 SLF4J 中的 API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(Test.class);
【強制】所有日誌檔案至少儲存 15 天,因為有些異常具備以“周”為頻次發生的特點。網路執行狀態、安全相關資訊、系統監測、管理後臺操作、使用者敏感操作需要留存相關的網路日誌不少於 6 個月。
【強制】應用中的擴充套件日誌(如打點、臨時監控、訪問日誌等)命名方式:
appName_logType_logName.log。logType:日誌型別,如 stats/monitor/access 等;logName:日誌描述。這種命名的好處:通過檔名就可知道日誌檔案屬於什麼應用,什麼型別,什麼目的,也有利於歸類查詢。說明:推薦對日誌進行分類,如將錯誤日誌和業務日誌分開存放,便於開發人員檢視,也便於通過日誌對系統進行及時監控。
正例:force-web 應用中單獨監控時區轉換異常,如:force_web_timeZoneConvert.log【強制】在日誌輸出時,字串變數之間的拼接使用佔位符的方式。
說明:因為 String 字串的拼接會使用 StringBuilder 的 append()方式,有一定的效能損耗。使用佔位符僅是替換動作,可以有效提升效能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);【強制】對於 trace/debug/info 級別的日誌輸出,必須進行日誌級別的開關判斷。
說明:雖然在 debug(引數)的方法體內第一行程式碼 isDisabled(Level.DEBUG_INT)為真時(Slf4j 的常見實現 Log4j 和 Logback),就直接 return,但是引數可能會進行字串拼接運算。此外,如果 debug(getName())這種引數內有 getName()方法呼叫,無謂浪費方法呼叫的開銷。正例:
// 如果判斷為真,那麼可以輸出 trace 和 debug 級別的日誌 if (logger.isDebugEnabled()) { logger.debug("Current ID is: {} and name is: {}", id, getName()); }
【強制】避免重複列印日誌,浪費磁碟空間,務必在 log4j.xml 中設定 additivity=false。
正例:<logger name="com.taobao.dubbo.config" additivity="false">
【強制】異常資訊應該包括兩類資訊:案發現場資訊和異常堆疊資訊。如果不處理,那麼通過關鍵字 throws 往上丟擲。
正例:logger.error(各類引數或者物件 toString() + "_" + e.getMessage(), e);
【推薦】謹慎地記錄日誌。生產環境禁止輸出 debug 日誌;有選擇地輸出 info 日誌;如果使用 warn 來記錄剛上線時的業務行為資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並記得及時刪除這些觀察日誌。
說明:大量地輸出無效日誌,不利於系統性能提升,也不利於快速定位錯誤點。記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?
【推薦】可以使用 warn 日誌級別來記錄使用者輸入引數錯誤的情況,避免使用者投訴時,無所適從。如非必要,請不要在此場景打出 error 級別,避免頻繁報警。
說明:注意日誌輸出的級別,error 級別只記錄系統邏輯出錯、異常或者重要的錯誤資訊。
【推薦】儘量用英文來描述日誌錯誤資訊,如果日誌中的錯誤資訊用英文描述不清楚的話使用中文描述即可,否則容易產生歧義。【強制】國際化團隊或海外部署的伺服器由於字符集問題,使用全英文來註釋和描述日誌錯誤資訊。
三、單元測試
【強制】好的單元測試必須遵守 AIR 原則。
說明:單元測試在線上執行時,感覺像空氣(AIR)一樣並不存在,但在測試質量的保障上,卻是非常關鍵的。好的單元測試巨集觀上來說,具有自動化、獨立性、可重複執行的特點。
- A:Automatic(自動化)
- I:Independent(獨立性)
- R:Repeatable(可重複)
【強制】單元測試應該是全自動執行的,並且非互動式的。測試用例通常是被定期執行的,執行過程必須完全自動化才有意義。輸出結果需要人工檢查的測試不是一個好的單元測試。單元測試中不準使用 System.out 來進行人肉驗證,必須使用 assert 來驗證。
【強制】保持單元測試的獨立性。為了保證單元測試穩定可靠且便於維護,單元測試用例之間決不能互相呼叫,也不能依賴執行的先後次序。
反例:method2 需要依賴 method1 的執行,將執行結果作為 method2 的輸入。
【強制】單元測試是可以重複執行的,不能受到外界環境的影響。
說明:單元測試通常會被放到持續整合中,每次有程式碼 check in 時單元測試都會被執行。如果單測對外部環境(網路、服務、中介軟體等)有依賴,容易導致持續整合機制的不可用。
正例:為了不受外界環境影響,要求設計程式碼時就把 SUT 的依賴改成注入,在測試時用 spring 這樣的 DI 框架注入一個本地(記憶體)實現或者 Mock 實現。【強制】對於單元測試,要保證測試粒度足夠小,有助於精確定位問題。單測粒度至多是類級別,一般是方法級別。
說明:只有測試粒度小才能在出錯時儘快定位到出錯位置。單測不負責檢查跨類或者跨系統的互動邏輯,那是整合測試的領域。
【強制】核心業務、核心應用、核心模組的增量程式碼確保單元測試通過。
說明:新增程式碼及時補充���元測試,如果新增程式碼影響了原有單元測試,請及時修正。
【強制】單元測試程式碼必須寫在如下工程目錄:src/test/java,不允許寫在業務程式碼目錄下。
說明:原始碼編譯時會跳過此目錄,而單元測試框架預設是掃描此目錄。
【推薦】單元測試的基本目標:語句覆蓋率達到 70%;核心模組的語句覆蓋率和分支覆蓋率都要達到 100%
說明:在工程規約的應用分層中提到的 DAO 層,Manager 層,可重用度高的 Service,都應該進行單元測試。
- 【推薦】編寫單元測試程式碼遵守 BCDE 原則,以保證被測試模組的交付質量。
- B:Border,邊界值測試,包括迴圈邊界、特殊取值、特殊時間點、資料順序等。
- C:Correct,正確的輸入,並得到預期的結果。
- D:Design,與設計文件相結合,來編寫單元測試。
- E:Error,強制錯誤資訊輸入(如:非法資料、異常流程、業務允許外等),並得到預期的結果。
【推薦】對於資料庫相關的查詢,更新,刪除等操作,不能假設資料庫裡的資料是存在的,或者直接操作資料庫把資料插入進去,請使用程式插入或者匯入資料的方式來準備資料。
反例:刪除某一行資料的單元測試,在資料庫中,先直接手動增加一行作為刪除目標,但是這一行新增資料並不符合業務插入規則,導致測試結果異常。
【推薦】和資料庫相關的單元測試,可以設定自動回滾機制,不給資料庫造成髒資料。或者對單元測試產生的資料有明確的前後綴標識。
正例:在企業智慧事業部的內部單元測試中,使用 ENTERPRISE_INTELLIGENCE _UNIT_TEST_的字首來標識單元測試相關程式碼。
【推薦】對於不可測的程式碼在適當的時機做必要的重構,使程式碼變得可測,避免為了達到測試要求而書寫不規範測試程式碼。
【推薦】在設計評審階段,開發人員需要和測試人員一起確定單元測試範圍,單元測試最好覆蓋所有測試用例。
【推薦】單元測試作為一種質量保障手段,在專案提測前完成單元測試,不建議專案釋出後補充單元測試用例。
- 【參考】為了更方便地進行單元測試,業務程式碼應避免以下情況:
- 構造方法中做的事情過多。
- 存在過多的全域性變數和靜態方法。
- 存在過多的外部依賴。
存在過多的條件語句。
說明:多層條件語句建議使用衛語句、策略模式、狀態模式等方式重構。
- 【參考】不要對單元測試存在如下誤解:
- 那是測試同學乾的事情。本文是開發手冊,凡是本文內容都是與開發同學強相關的。
- 單元測試程式碼是多餘的。系統的整體功能與各單元部件的測試正常與否是強相關的。
- 單元測試程式碼不需要維護。一年半載後,那麼單元測試幾乎處於廢棄狀態。
- 單元測試與線上故障沒有辯證關係。好的單元測試能夠最大限度地規避線上故障。
四、安全規約
【強制】隸屬於使用者個人的頁面或者功能必須進行許可權控制校驗。
說明:防止沒有做水平許可權校驗就可隨意訪問、修改、刪除別人的資料,比如檢視他人的私信內容、修改他人的訂單。
【強制】使用者敏感資料禁止直接展示,必須對展示資料進行脫敏。
說明:中國大陸個人手機號碼顯示為:137****0969,隱藏中間 4 位,防止隱私洩露。
【強制】使用者輸入的 SQL 引數嚴格使用引數繫結或者 METADATA 欄位值限定,防止 SQL 注入,禁止字串拼接 SQL 訪問資料庫。
【強制】使用者請求傳入的任何引數必須做有效性驗證。
說明:忽略引數校驗可能導致:
- page size 過大導致記憶體溢位
- 惡意 order by 導致資料庫慢查詢
- 任意重定向
- SQL 注入
- 反序列化注入
- 正則輸入源串拒絕服務 ReDoS
說明:Java 程式碼用正則來驗證客戶端的輸入,有些正則寫法驗證普通使用者輸入沒有問題,但是如果攻擊人員使用的是特殊構造的字串來驗證,有可能導致死迴圈的結果。
【強制】禁止向 HTML 頁面輸出未經安全過濾或未正確轉義的使用者資料。
【強制】表單、AJAX 提交必須執行 CSRF 安全驗證。
說明:CSRF(Cross-site request forgery)跨站請求偽造是一類常見程式設計漏洞。對於存在 CSRF 漏洞的應用/網站,攻擊者可以事先構造好 URL,只要受害者使用者一訪問,後臺便在使用者不知情的情況下對資料庫中使用者引數進行相應修改。
【強制】在使用平臺資源,譬如簡訊、郵件、電話、下單、支付,必須實現正確的防重放的 機制,如數量限制、疲勞度控制、驗證碼校驗,避免被濫刷而導致資損。
說明:如註冊時傳送驗證碼到手機,如果沒有限制次數和頻率,那麼可以利用此功能騷擾到其它使用者,並造成簡訊平臺資源浪費。
【推薦】發貼、評論、傳送即時訊息等使用者生成內容的場景必須實現防刷、文字內容違禁詞過濾等風控策略。
五、MySQL 資料庫 (一) 建表規約
【強制】表達是與否概念的欄位,必須使用 is_xxx 的方式命名,資料型別是 unsigned tinyint(1 表示是,0 表示否)。
說明:任何欄位如果為非負數,必須是 unsigned。
注意:POJO 類中的任何布林型別的變數,都不要加 is 字首,所以,需要在設定從 is_xxx 到 Xxx 的對映關係。資料庫表示是與否的值,使用 tinyint 型別,堅持 is_xxx 的命名方式是為了明確其取值含義與取值範圍。
正例:表達邏輯刪除的欄位名 is_deleted,1 表示刪除,0 表示未刪除。【強制】表名、欄位名必須使用小寫字母或數字,禁止出現數字開頭,禁止兩個下劃線中間只出現數字。資料庫欄位名的修改代價很大,因為無法進行預釋出,所以欄位名稱需要慎重考慮。
說明:MySQL 在 Windows 下不區分大小寫,但在 Linux 下預設是區分大小寫。因此,資料庫名、表名、欄位名,都不允許出現任何大寫字母,避免節外生枝。
正例:aliyun_admin,rdc_config,level3_name
反例:AliyunAdmin,rdcConfig,level_3_name【強制】表名不使用複數名詞。
說明:表名應該僅僅表示表裡面的實體內容,不應該表示實體數量,對應於 DO 類名也是單數形式,符合表達習慣。
【強制】禁用保留字,如 desc、range、match、delayed 等,請參考 MySQL 官方保留字。
【強制】主鍵索引名為 pk_欄位名;唯一索引名為 uk_欄位名;普通索引名則為 idx_欄位名。
說明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的簡稱。
【強制】小數型別為 decimal,禁止使用 float 和 double。
說明:在儲存的時候,float 和 double 都存在精度損失的問題,很可能在比較值的時候,得到不正確的結果。如果儲存的資料範圍超過 decimal 的範圍,建議將資料拆成整數和小數並分開儲存。
【強制】如果儲存的字串長度幾乎相等,使用 char 定長字串型別。
【強制】varchar 是可變長字串,不預先分配儲存空間,長度不要超過 5000,如果儲存長度大於此值,定義欄位型別為 text,獨立出來一張表,用主鍵來對應,避免影響其它欄位索引效率。
【強制】表必備三欄位:id, create_time, update_time。
說明:其中 id 必為主鍵,型別為 bigint unsigned、單表時自增、步長為 1。create_time, update_time 的型別均為 datetime 型別。
【推薦】表的命名最好是遵循“業務名稱_表的作用”。
正例:alipay_task / force_project / trade_config
【推薦】庫名與應用名稱儘量一致。
【推薦】如果修改欄位含義或對欄位表示的狀態追加時,需要及時更新欄位註釋。
- 【推薦】欄位允許適當冗餘,以提高查詢效能,但必須考慮資料一致。冗餘欄位應遵循:
- 不是頻繁修改的欄位。
- 不是 varchar 超長欄位,更不能是 text 欄位。
不是唯一索引的欄位。
正例:商品類目名稱使用頻率高,欄位長度短,名稱基本一不變,可在相關聯的表中冗餘儲存類目名稱,避免關聯查詢。
【推薦】單錶行數超過 500 萬行或者單表容量超過 2GB,才推薦進行分庫分表。
說明:如果預計三年後的資料量根本達不到這個級別,請不要在建立表時就分庫分表。【參考】合適的字元儲存長度,不但節約資料庫表空間、節約索引儲存,更重要的是提升檢索速度。
正例:如下表,其中無符號值可以避免誤存負數,且擴大了表示範圍。
物件 年齡區間 型別 位元組 表示範圍 人 150 歲之內 tinyint unsigned 1 無符號值:0 到 255 龜 數百歲 smallint unsigned 2 無符號值:0 到 65535 恐龍化石 數千萬年 int unsigned 4 無符號值:0 到約 42.9 億 太陽 約 50 億年 bigint unsigned 8 無符號值:0 到約 10 的 19 次方
(二) 索引規約
【強制】業務上具有唯一特性的欄位,即使是多個欄位的組合,也必須建成唯一索引。
說明:不要以為唯一索引影響了 insert 速度,這個速度損耗可以忽略,但提高查詢速度是明顯的;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生。
【強制】超過三個表禁止 join。需要 join 的欄位,資料型別必須絕對一致;多表關聯查詢時,保證被關聯的欄位需要有索引。
說明:即使雙表 join 也要注意表索引、SQL 效能。
【強制】在 varchar 欄位上建立索引時,必須指定索引長度,沒必要對全欄位建立索引,根據實際文字區分度決定索引長度即可。
說明:索引的長度與區分度是一對矛盾體,一般對字串型別資料,長度為 20 的索引,區分度會高達 90% 以上,可以使用 count(distinct left(列名, 索引長度))/count(*)的區分度來確定。
【強制】頁面搜尋嚴禁左模糊或者全模糊,如果需要請走搜尋引擎來解決。
說明:索引檔案具有 B-Tree 的最左字首匹配特性,如果左邊的值未確定,那麼無法使用此索引。
【推薦】如果有 order by 的場景,請注意利用索引的有序性。order by 最後的欄位是組合索引的一部分,並且放在索引組合順序的最後,避免出現 file_sort 的情況,影響查詢效能。
正例:where a=? and b=? order by c; 索引:a_b_c
反例:索引如果存在範圍查詢,那麼索引有序性無法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 無法排序。【推薦】利用覆蓋索引來進行查詢操作,避免回表。
說明:如果一本書需要知道第 11 章是什麼標題,會翻開第 11 章對應的那一頁嗎?目錄瀏覽一下就好,這個目錄就是起到覆蓋索引的作用。
正例:能夠建立索引的種類分為主鍵索引、唯一索引、普通索引三種,而覆蓋索引只是一種查詢的一種效果,用 explain 的結果,extra 列會出現:using index。【推薦】利用延遲關聯或者子查詢優化超多分頁場景。
說明: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
【推薦】SQL 效能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好。
說明:
- consts 單表中最多隻有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到資料。
- ref 指的是使用普通的索引(normal index)。 3) range 對索引進行範圍檢索。
反例:explain 表的結果,type=index,索引物理檔案全掃描,速度非常慢,這個 index 級別比較 range 還低,與全表掃描是小巫見大巫。
【推薦】建組合索引的時候,區分度最高的在最左邊。
正例:如果 where a=? and b=? ,如果 a 列的幾乎接近於唯一值,那麼只需要單建 idx_a 索引即可。說明:存在非等號和等號混合時,在建索引時,請把等號條件的列前置。如:where c>? and d=? 那麼即使 c 的區分度更高,也必須把 d 放在索引的最前列,即索引 idx_d_c。
【推薦】防止因欄位型別不同造成的隱式轉換,導致索引失效。
- 【參考】建立索引時避免有如下極端誤解:
- 寧濫勿缺。認為一個查詢就需要建一個索引。
- 寧缺勿濫。認為索引會消耗空間、嚴重拖慢記錄的更新以及行的新增速度。
- 抵制惟一索引。認為業務的惟一性一律需要在應用層通過“先查後插”方式解決。
(三) SQL 語句
【強制】不要使用 count(列名)或 count(常量)來替代 count(),count()是 SQL92 定義的標準統計行數的語法,跟資料庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值為 NULL 的行,而 count(列名)不會統計此列為 NULL 值的行。
【強制】count(distinct col) 計算該列除 NULL 之外的不重複行數,注意 count(distinct col1, col2) 如果其中一列全為 NULL,那麼即使另一列有不同的值,也返回為 0。
【強制】當某一列的值全是 NULL 時,count(col)的返回結果為 0,但 sum(col)的返回結果為 NULL,因此使用 sum()時需注意 NPE 問題。
正例:使用如下方式來避免 sum 的 NPE 問題:SELECT IFNULL(SUM(column), 0) FROM table;【強制】使用 ISNULL()來判斷是否為 NULL 值。
說明:NULL 與任何值的直接比較都為 NULL。
- NULL<>NULL 的返回結果是 NULL,而不是 false。
- NULL=NULL 的返回結果是 NULL,而不是 true。
- NULL<>1 的返回結果是 NULL,而不是 true。
【強制】程式碼中寫分頁查詢邏輯時,若 count 為 0 應直接返回,避免執行後面的分頁語句。
【強制】不得使用外來鍵與級聯,一切外來鍵概念必須在應用層解決。
說明:以學生和成績的關係為例,學生表中的 student_id 是主鍵,那麼成績表中的 student_id 則為外來鍵。如果更新學生表中的 student_id,同時觸發成績表中的 student_id 更新,即為級聯更新。外來鍵與級聯更新適用於單機低併發,不適合分散式、高併發叢集;級聯更新是強阻塞,存在資料庫更新風暴的風險;外來鍵影響資料庫的插入速度。
【強制】禁止使用儲存過程,儲存過程難以除錯和擴充套件,更沒有移植性。
【強制】資料訂正(特別是刪除、修改記錄操作)時,要先 select,避免出現誤刪除,確認無誤才能執行更新語句。
【推薦】in 操作能避免則避免,若實在避免不了,需要仔細評估 in 後邊的集合元素數量,控制在 1000 個之內。
【參考】如果有國際化需要,所有的字元儲存與表示,均以 utf-8 編碼,注意字元統計函式的區別。
說明:
SELECT LENGTH("輕鬆工作"); 返回為 12
SELECT CHARACTER_LENGTH("輕鬆工作"); 返回為 4
如果需要儲存表情,那麼選擇 utf8mb4 來進行儲存,注意它與 utf-8 編碼的區別。【參考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系統和事務日誌資源少,但 TRUNCATE 無事務且不觸發 trigger,有可能造成事故,故不建議在開發程式碼中使用此語句。
說明:TRUNCATE TABLE 在功能上與不帶 WHERE 子句的 DELETE 語句相同。
(四) ORM 對映
【強制】在表查詢中,一律不要使用 * 作為查詢的欄位列表,需要哪些欄位必須明確寫明。
說明:
- 增加查詢分析器解析成本。
- 增減欄位容易與 resultMap 配置不一致。
- 無用欄位增加網路消耗,尤其是 text 型別的欄位。
【強制】POJO 類的布林屬性不能加 is,而資料庫欄位必須加 is_,要求在 resultMap 中進行欄位與屬性之間的對映。
說明:參見定義 POJO 類以及資料庫欄位定義規定,在
中增加對映,是必須的。在 MyBatis Generator 生成的程式碼中,需要進行對應的修改。 【強制】不要用 resultClass 當返回引數,即使所有類屬性名與資料庫欄位一一對應,也需要定義;反過來,每一個表也必然有一個 POJO 類與之對應。
說明:配置對映關係,使欄位與 DO 類解耦,方便維護。
【強制】sql.xml 配置引數使用:#{},#param# 不要使用${} 此種方式容易出現 SQL 注入。
【強制】iBATIS 自帶的 queryForList(String statementName,int start,int size)不推薦使用。
說明:其實現方式是在資料庫取到 statementName 對應的 SQL 語句的所有記錄,再通過 subList 取 start,size 的子集合。
正例:Map<String, Object> map = new HashMap<>(); map.put("start", start); map.put("size", size);
【強制】不允許直接拿 HashMap 與 Hashtable 作為查詢結果集的輸出。
說明:resultClass=”Hashtable”,會置入欄位名和屬性值,但是值的型別不可控。
【強制】更新資料表記錄時,必須同時更新記錄對應的 gmt_modified 欄位值為當前時間。
【推薦】不要寫一個大而全的資料更新介面。傳入為 POJO 類,不管是不是自己的目標更新欄位,都進行 update table set c1=value1,c2=value2,c3=value3; 這是不對的。執行 SQL 時,不要更新無改動的欄位,一是易出錯;二是效率低;三是增加 binlog 儲存。
【參考】@Transactional 事務不要濫用。事務會影響資料庫的 QPS,另外使用事務的地方需要考慮各方面的回滾方案,包括快取回滾、搜尋引擎回滾、訊息補償、統計修正等。
【參考】<isEqual>中的 compareValue 是與屬性值對比的常量,一般是數字,表示相等時帶上此條件;<isNotEmpty>表示不為空且不為 null 時執行;<isNotNull>表示不為 null 值時執行。
六、工程結構
(一) 應用分層
- 【推薦】圖中預設上層依賴於下層,箭頭關係表示可直接依賴,如:開放介面層可以依賴於 Web 層,也可以直接依賴於 Service 層,依此類推:
- 開放介面層:可直接封裝 Service 方法暴露成 RPC 介面;通過 Web 封裝成 http 介面;進行閘道器安 全控制、流量控制等。
- 終端顯示層:各個端的模板渲染並執行顯示的層。當前主要是 velocity 渲染,JS 渲染,JSP 渲染,移動端展示等。
- Web 層:主要是對訪問控制進行轉發,各類基本引數校驗,或者不復用的業務簡單處理等。
- Service 層:相對具體的業務邏輯服務層。
- Manager 層:通用業務處理層,它有如下特徵:
- 對第三方平臺封裝的層,預處理返回結果及轉化異常資訊。
- 對 Service 層通用能力的下沉,如快取方案、中介軟體通用處理。
- 與 DAO 層互動,對多個 DAO 的組合複用。
- DAO 層:資料訪問層,與底層 MySQL、Oracle、Hbase 等進行資料互動。
- 外部介面或第三方平臺:包括其它部門 RPC 開放介面,基礎平臺,其它公司的 HTTP 介面。
【參考】(分層異常處理規約)在 DAO 層,產生的異常型別有很多,無法用細粒度的異常進行 catch,使用 catch(Exception e)方式,並 throw new DAOException(e),不需要列印日誌,因為日誌在 Manager/Service 層一定需要捕獲並列印到日誌檔案中去,如果同臺伺服器再打日誌,浪費效能和儲存。在 Service 層出現異常時,必須記錄出錯日誌到磁碟,儘可能帶上引數資訊,相當於保護案發現場。如果 Manager 層與 Service 同機部署,日誌方式與 DAO 層處理一致,如果是單獨部署,則採用與 Service 一致的處理方式。Web 層絕不應該繼續往上拋異常,因為已經處於頂層,如果意識到這個異常將導致頁面無法正常渲染,那麼就應該直接跳轉到友好錯誤頁面,加上使用者容易理解的錯誤提示資訊。開放介面層要將異常處理成錯誤碼
和錯誤資訊方式返回。- 【參考】分層領域模型規約:
- DO(Data Object):此物件與資料庫表結構一一對應,通過 DAO 層向上傳輸資料來源物件。
- DTO(Data Transfer Object):資料傳輸物件,Service 或 Manager 向外傳輸的物件。
- BO(Business Object):業務物件,由 Service 層輸出的封裝業務邏輯的物件。
- AO(Application Object):應用物件,在 Web 層與 Service 層之間抽象的複用物件模型,極為貼近展示層,複用度不高。
- VO(View Object):顯示層物件,通常是 Web 向模板渲染引擎層傳輸的物件。
- Query:資料查詢物件,各層接收上層的查詢請求。注意超過 2 個引數的查詢封裝,禁止使用 Map 類來傳輸。
(二) 二方庫依賴
- 【強制】定義 GAV 遵從以下規則:
GroupID 格式:com.{公司/BU }.業務線 [.子業務線],最多 4 級。
說明:{公司/BU} 例如:alibaba/taobao/tmall/aliexpress 等 BU 一級;子業務線可選。
正例:com.taobao.jstorm 或 com.alibaba.dubbo.registerArtifactID 格式:產品線名-模組名。語義不重複不遺漏,先到中央倉庫去查證一下。
正例:dubbo-client / fastjson-api / jstorm-tool
Version:詳細規定參考下方。
- 【強制】二方庫版本號命名方式:主版本號.次版本號.修訂號
- 主版本號:產品方向改變,或者大規模 API 不相容,或者架構不相容升級。
- 次版本號:保持相對相容性,增加主要功能特性,影響範圍極小的 API 不相容修改。
修訂號:保持完全相容性,修復 BUG、新增次要功能特性等。
說明:注意起始版本號必須為:1.0.0,而不是 0.0.1,正式釋出的類庫必須先去中央倉庫進行查證,使版本號有延續性,正式版本號不允許覆蓋升級。如當前版本:1.3.3,那麼下一個合理的版本號:1.3.4 或 1.4.0 或 2.0.0
【強制】線上應用不要依賴 SNAPSHOT 版本(安全包除外)。
說明:不依賴 SNAPSHOT 版本是保證應用釋出的冪等性。另外,也可以加快編譯時的打包構建。
【強制】二方庫的新增或升級,保持除功能點之外的其它 jar 包仲裁結果不變。如果有改變,必須明確評估和驗證。
說明:在升級時,進行 dependency:resolve 前後資訊比對,如果仲裁結果完全不一致,那麼通過 dependency:tree 命令,找出差異點,進行
排除 jar 包。 【強制】二方庫裡可以定義列舉型別,引數可以使用列舉型別,但是介面返回值不允許使用列舉型別或者包含列舉型別的 POJO 物件。
【強制】依賴於一個二方庫群時,必須定義一個統一的版本變數,避免版本號不一致。
說明:依賴 springframework-core,-context,-beans,它們都是同一個版本,可以定義一個變數來儲存版本:${spring.version},定義依賴的時候,引用該版本。
【強制】禁止在子專案的 pom 依賴中出現相同的 GroupId,相同的 ArtifactId,但是不同的 Version。
說明:在本地除錯時會使用各子專案指定的版本號,但是合併成一個 war,只能有一個版本號出現在最後的 lib 目錄中。可能出現線下除錯是正確的,釋出到線上卻出故障的問題。
【推薦】底層基礎技術框架、核心資料管理平臺、或近硬體端系統謹慎引入第三方實現。
【推薦】所有 pom 檔案中的依賴宣告放在<dependencies>語句塊中,所有版本仲裁放在<dependencyManagement>語句塊中。
說明:<dependencyManagement>裡只是宣告版本,並不實現引入,因此子專案需要顯式的宣告依賴,version 和 scope 都讀取自父 pom。而<dependencies>所有宣告在主 pom 的<dependencies>裡的依賴都會自動引入,並預設被所有的子專案繼承。
【推薦】二方庫不要有配置項,最低限度不要再增加配置項。
- 【參考】為避免應用二方庫的依賴衝突問題,二方庫釋出者應當遵循以下原則:
- 精簡可控原則。移除一切不必要的 API 和依賴,只包含 Service API、必要的領域模型物件、Utils 類、常量、列舉等。如果依賴其它二方庫,儘量是 provided 引入,讓二方庫使用者去依賴具體版本號; 無 log 具體實現,只依賴日誌框架。
- 穩定可追溯原則。每個版本的變化應該被記錄,二方庫由誰維護,原始碼在哪裡,都需要能方便查到。除非使用者主動升級版本,否則公共二方庫的行為不應該發生變化。
(三) 伺服器
【推薦】高併發伺服器建議調小 TCP 協議的 time_wait 超時時間。
說明:作業系統預設 240 秒後,才會關閉處於 time_wait 狀態的連線,在高併發訪問下,伺服器端會因為處於 time_wait 的連線數太多,可能無法建立新的連線,所以需要在伺服器上調小此等待值。
正例:在 linux 伺服器上請通過變更/etc/sysctl.conf 檔案去修改該預設值(秒):
net.ipv4.tcp_fin_timeout = 30【推薦】調大伺服器所支援的最大檔案控制代碼數(File Descriptor,簡寫為 fd)。
說明:主流作業系統的設計是將 TCP/UDP 連線採用與檔案一樣的方式去管理,即一個連線對應於一個 fd。主流的 linux 伺服器預設所支援最大 fd 數量為 1024,當併發連線數很大時很容易因為 fd 不足而出現“open too many files”錯誤,導致新的連線無法建立。建議將 linux 伺服器所支援的最大控制代碼數調高數倍(與伺服器的記憶體數量相關)。
【推薦】給 JVM 環境引數設定-XX:+HeapDumpOnOutOfMemoryError 引數,讓 JVM 碰到 OOM 場景時輸出 dump 資訊。
說明:OOM 的發生是有概率的,甚至相隔數月才出現一例,出錯時的堆內資訊對解決問題非常有幫助。
【推薦】在線上生產環境,JVM 的 Xms 和 Xmx 設定一樣大小的記憶體容量,避免在 GC 後調整堆大小帶來的壓力。
【參考】伺服器內部重定向使用 forward;外部重定向地址使用 URL 拼裝工具類來生成,否則會帶來 URL 維護不一致的問題和潛在的安全風險。
七、設計規約
【強制】儲存方案和底層資料結構的設計獲得評審一致通過,並沉澱成為文件。
說明:有缺陷的底層資料結構容易導致系統風險上升,可擴充套件性下降,重構成本也會因歷史資料遷移和系統平滑過渡而陡然增加,所以,儲存方案和資料結構需要認真地進行設計和評審,生產環境提交執行後,需要進行 double check。
正例:評審內容包括儲存介質選型、表結構設計能否滿足技術方案、存取效能和儲存空間能否滿足業務發展、表或欄位之間的辯證關係、欄位名稱、欄位型別、索引等;資料結構變更(如在原有表中新增欄位)也需要進行評審通過後上線。【強制】在需求分析階段,如果與系統互動的 User 超過一類並且相關的 User Case 超過 5 個,使用用例圖來表達更加清晰的結構化需求。
【強制】如果某個業務物件的狀態超過 3 個,使用狀態圖來表達並且明確狀態變化的各個觸發條件。
說明:狀態圖的核心是物件狀態,首先明確物件有多少種狀態,然後明確兩兩狀態之間是否存在直接轉換關係,再明確觸發狀態轉換的條件是什麼。
正例:淘寶訂單狀態有已下單、待付款、已付款、待發貨、已發貨、已收貨等。比如已下單與已收貨這兩種狀態之間是不可能有直接轉換關係的。【強制】如果系統中某個功能的呼叫鏈路上的涉及物件超過 3 個,使用時序圖來表達並且明確各呼叫環節的輸入與輸出。
說明:時序圖反映了一系列物件間的互動與協作關係,清晰立體地反映系統的呼叫縱深鏈路。
【強制】如果系統中模型類超過 5 個,並且存在複雜的依賴關係,使用類圖來表達並且明確類之間的關係。
說明:類影象建築領域的施工圖,如果搭平房,可能不需要,但如果建造螞蟻 Z 空間大樓,肯定需要詳細的施工圖。
【強制】如果系統中超過 2 個物件之間存在協作關係,並且需要表示複雜的處理流程,使用活動圖來表示。
說明:活動圖是流程圖的擴充套件,增加了能夠體現協作關係的物件泳道,支援表示併發等。
【推薦】需求分析與系統設計在考慮主幹功能的同時,需要充分評估異常流程與業務邊界。
反例:使用者在淘寶付款過程中,銀行扣款成功,傳送給使用者扣款成功簡訊,但是支付寶入款時由於斷網演練產生異常,淘寶訂單頁面依然顯示未付款,導致使用者投訴。
【推薦】類在設計與實現時要符合單一原則。
說明:單一原則最易理解卻是最難實現的一條規則,隨著系統演進,很多時候,忘記了類設計的初衷。
【推薦】謹慎使用繼承的方式來進行擴充套件,優先使用聚合/組合的方式來實現。
說明:不得已使用繼承的話,必須符合里氏代換原則,此原則說父類能夠出現的地方子類一定能夠出現,比如,“把錢交出來”,錢的子類美元、歐元、人民幣等都可以出現。
【推薦】系統設計時,根據依賴倒置原則,儘量依賴抽象類與介面,有利於擴充套件與維護。
說明:低層次模組依賴於高層次模組的抽象,方便系統間的解耦。
【推薦】系統設計時,注意對擴充套件開放,對修改閉合。
說明:極端情況下,交付線上生產環境的程式碼都是不可修改的,同一業務域內的需求變化,通過模組或類的擴充套件來實現。
【推薦】系統設計階段,共性業務或公共行為抽取出來公共模組、公共配置、公共類、公共方法等,避免出現重複程式碼或重複配置的情況。
說明:隨著程式碼的重複次數不斷增加,維護成本指數級上升。
【推薦】避免如下誤解:敏捷開發 = 講故事 + 編碼 + 釋出。
說明:敏捷開發是快速交付迭代可用的系統,省略多餘的設計方案,摒棄傳統的審批流程,但核心關鍵點上的必要設計和文件沉澱是需要的。
反例:某團隊為了業務快速發展,敏捷成了產品經理催進度的藉口,系統中均是勉強能執行但像麵條一樣的程式碼,可維護性和可擴充套件性極差,一年之後,不得不進行大規模重構,得不償失。【參考】系統設計主要目的是明確需求、理順邏輯、後期維護,次要目的用於指導編碼。
說明:避免為了設計而設計,系統設計文件有助於後期的系統維護和重構,所以設計結果需要進行分類歸檔儲存。
【參考】設計的本質就是識別和表達系統難點,找到系統的變化點,並隔離變化點。
說明:世間眾多設計模式目的是相同的,即隔離系統變化點。
- 【參考】系統架構設計的目的:
- 確定系統邊界。確定系統在技術層面上的做與不做。
- 確定系統內模組之間的關係。確定模組之間的依賴關係及模組的巨集觀輸入與輸出。
- 確定指導後續設計與演化的原則。使後續的子系統或模組設計在規定的框架內繼續演化。
- 確定非功能性需求。非功能性需求是指安全性、可用性、可擴充套件性等。
- 【參考】在做無障礙產品設計時,需要考慮到:
- 所有可互動的控制元件元素必須能被 tab 鍵聚焦,並且焦點順序需符合自然操作邏輯。
- 用於登陸校驗和請求攔截的驗證碼均需提供圖形驗證以外的其它方式。
- 自定義的控制元件型別需明確互動方式。
專有名詞解釋
- POJO(Plain Ordinary Java Object): 在本手冊中,POJO 專指只有 setter / getter / toString 的簡單類,包括 DO/DTO/BO/VO 等。
- GAV(GroupId、ArtifactctId、Version): Maven 座標,是用來唯一標識 jar 包。
- OOP(Object Oriented Programming): 本手冊泛指類、物件的程式設計處理方式。
- ORM(Object Relation Mapping): 物件關係對映,物件領域模型與底層資料之間的轉換,本文泛指 iBATIS, mybatis 等框架。
- NPE(java.lang.NullPointerException): 空指標異常。
- SOA(Service-Oriented Architecture): 面向服務架構,它可以根據需求通過網路對鬆散耦合的粗粒度應用元件進行分散式部署、組合和使用,有利於提升元件可重用性,可維護性。
- IDE(Integrated Development Environment): 用於提供程式開發環境的應用程式,一般包括程式碼編輯器、編譯器、偵錯程式和圖形使用者介面等工具,本《手冊》泛指 IntelliJ IDEA 和 eclipse。
- OOM(Out Of Memory): 源於 java.lang.OutOfMemoryError,當 JVM 沒有足夠的記憶體來為物件分配空間並且垃圾回收器也無法回收空間時,系統出現的嚴重狀況。
- 一方庫:本工程內部子專案模組依賴的庫(jar 包)。
- 二方庫:公司內部發布到中央倉庫,可供公司內部其它應用依賴的庫(jar 包)。
- 三方庫:公司之外的開源庫(jar 包)。