一、為啥要有代碼規範?
1.代碼規範可以加快團隊間的協作
對於每個項目的開發,大多數是由一個團隊來完成的,團隊內部的人來自四面八方,每個人的代碼風格也大不相同,如果沒有統一的代碼規範,那麼代碼的可讀性會大大降低,會嚴重影響團隊的開發效率。這就好比幾個人在一起交流,小王用地道的四川話,小李用地道的湖南話、小王用濃厚的廣東話,試想一下,他們相互之間能暢快的交談嗎?顯然不能,他們可能各自都聽不懂對方的口音,假如他們都改用普通話,那麼問題不就解決了?如此可見,代碼規範就好比我們的普通話,統一標準,才能加快團隊間的協作。
2.代碼規範可以降低維護成本
作為程序員,我想90%的人都應該維護過別人的代碼,當你第一眼看到別人的代碼時,你的心情是怎麼樣的?要麼欣喜,要麼沮喪!欣喜的是,你拿到代碼,能很快上手,花費很短的時間就能把一個新加的功能給做好;沮喪的是,你看到代碼頭就大,可讀性差、模塊重用性不高,變量命名不規範等,一下子就喪失了信心,再加上領導隔三差五的催趕進度,是不是很崩潰?回過頭來,再說代碼規範為啥可以降低維護成本,首先,代碼規範增加了代碼的可讀性,提高了代碼維護的效率,縮短了開發周期,不是節約了成本嗎?其次,代碼規範可提高代碼的復用程度,舉個例子,新增一個新功能,本來需要編寫1000行代碼,但是由於之前代碼復用度高,縮減到300行就能實現,節省了700行代碼,不也解約了成本嗎?
3.代碼規範有助於代碼審查
代碼審查是項目開發過程中的一個必要環節,代碼審查的目的在於發現代碼邏輯中潛在的錯誤,代碼審查的方式一般可以通過靜態掃描工具或人工走查的方式進行。靜態掃描工具大多只能掃描代碼的語法規範,常見的語法邏輯問題,對於業務邏輯操作還得靠人工的方式進行。人工進行代碼走查時,一般是相互走查,A同學寫的代碼由B同學走查,代碼走查是一個很好的學習機會,對成員的進步也是很有益的。隨意編寫的代碼,不遵循編碼規範,會加重的代碼審查的工作量及難度,浪費大量的人力物力。 代碼規範讓代碼審查有據可查,大大提高了審查效率和效果。
4.代碼規範有助於提高程序員自身素質
我認為要想成為一個高素質的程序員,最基本的要求就是要有良好的代碼規範,代碼規範好比一個人的穿衣打扮,那些穿衣華麗、衣冠整潔的人走到哪裡都會成為亮點,相反那些整天衣衫不整、渾身散發著特殊氣味的人一定會被嫌棄!有空閱讀一下java JDK的源碼,那些都是大牛們的傑作,閱讀時真是賞心悅目!好的代碼習慣不是一朝一夕都能養成的,是要經過長期的訓練、積累總結才能養成。
二、常見java代碼規範有哪些?
以下是我在工作中總結的代碼規範(部分規範參考第三方平台的內容),有興趣的可以收藏。
(一) 命名風格
1. 【強制】代碼中的命名均不能以下劃線或美元符號開始,也不能以下劃線或美元符號結束。反 例 :_name / name / $name / name_ / name$ / name
2. 【強制】代碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
說明:正確的英文拼寫和語法可以讓閱讀者易於理解,避免歧義。注意,即使純拼音命名方式也要避免採用。
正例:shaighai / zhengzhou / youku / hangzhou 等國際通用的名稱,可視同英文。反例:DaZhePromotion [打折] / getPingfenByName() [評分] / int 某變量 = 3
3. 【強制】類名使用 UpperCamelCase 風格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion
4. 【強制】方法名、參數名、成員變量、局部變量都統一使用 lowerCamelCase 風格,必須遵從駝峰形式。
正例: localValue / getHttpMessage() / inputUserId
5. 【強制】常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。正例:MAX_STOCK_COUNT
反例:MAX_COUNT
6. 【強制】抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾;測試類命名以它要測試的類的名稱開始,以 Test 結尾。
7. 【強制】類型與中括號緊挨相連來表示數組。正例:定義整形數組 int[] arrayDemo;
反例:在 main 參數中,使用 String args[]來定義。
8. 【強制】POJO 類中布爾類型的變量,都不要加 is 前綴,否則部分框架解析會引起序列化錯誤。反例:定義為基本數據類型 Boolean isDeleted 的屬性,它的方法也是 isDeleted(),RPC
框架在反向解析的時候,「誤以為」對應的屬性名稱是 deleted,導致屬性獲取不到,進而拋出異常。
9. 【強制】包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。
10. 【推薦】杜絕完全不規範的縮寫,避免望文不知義。
反例:AbstractClass「縮寫」命名成 AbsClass;condition「縮寫」命名成 condi,此類隨意縮寫嚴重降低了代碼的可閱讀性。
11. 【推薦】為了達到代碼自解釋的目標,任何自定義編程元素在命名時,使用盡量完整的單詞組合來表達其意。
正例:在 JDK 中,表達原子更新的類名為:
AtomicReferenceFieldUpdater。反例:變量 int a 的隨意命名方式。
12. 【推薦】如果模塊、接口、類、方法使用了設計模式,在命名時需體現出具體模式。說明:將設計模式體現在名字中,有利於閱讀者快速理解架構設計理念。
正例:public class OrderFactory; public class LoginProxy;
public class ResourceObserver;
13. 【推薦】接口類中的方法和屬性不要加任何修飾符號(public 也不要加),保持代碼的簡潔性,並加上有效的 Javadoc 注釋。盡量不要在接口裡定義變量,如果一定要定義變量,肯定是與接口方法相關,並且是整個應用的基礎常量。
正例:接口方法簽名 void commit();
接口基礎常量 String COMPANY = “youqu”;
反例:接口方法定義 public abstract void f();
說明:JDK8 中接口允許有默認實現,那麼這個 default 方法,是對所有實現類都有價值的默認實現。
14. 接口和實現類的命名有兩套規則:
1) 【強制】對於 Service 和 DAO 類,基於 SOA 的理念,暴露出來的服務一定是接口,內部的實現類用 Impl 的後綴與接口區別。
正例:CacheServiceImpl 實現 CacheService 接口。
2) 【推薦】如果是形容能力的接口名稱,取對應的形容詞為接口名(通常是–able 的形式) 。正例:AbstractTranslator 實現 Translatable 接口。
15. 【參考】各層命名規約:
A) Service/DAO 層方法命名規約
1) 獲取單個對象的方法用 get 做前綴。
2) 獲取多個對象的方法用 list 做前綴,複數形式結尾如:listObjects。
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。
(二) 常量定義
1. 【強制】不允許任何魔法值(即未經預先定義的常量)直接出現在代碼中。反例:String key = “Id#Super_” + tradeId;
cache.put(key, value);
2. 【強制】在 long 或者 Long 賦值時,數值後使用大寫的 L,不能是小寫的 l,小寫容易跟數字
1 混淆,造成誤解。
說明:Long a = 2l; 寫的是數字的 21,還是 Long 型的 2?
3. 【推薦】不要使用一個常量類維護所有常量,要按常量功能進行歸類,分開維護。
說明:大而全的常量類,雜亂無章,使用查找功能才能定位到修改的常量,不利於理解和維護。正例:緩存相關常量放在類 CacheConsts 下;系統配置相關常量放在類 ConfigConsts 下。
4. 【推薦】常量的復用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量。
1) 跨應用共享常量:放置在二方庫中,通常是 client.jar 中的 constant 目錄下。
2) 應用內共享常量:放置在一方庫中,通常是子模塊中的 constant 目錄下。
反例:易懂變量也要統一定義成應用內共享常量,兩位攻城師在兩個類中分別定義了表示
「是」的變量:
3) 類 A 中:public static final String YES = “yes”;
4) 類 B 中 :public static final String YES = “y”; A.YES.equals(B.YES),預期是 true,但實際返回為 false,導致線上問題。
5) 子工程內部共享常量:即在當前子工程的 constant 目錄下。
6) 包內共享常量:即在當前包下單獨的 constant 目錄下。
7) 類內共享常量:直接在類內部 private static final 定義。
(三) 代碼格式
1. 【強制】大括號的使用約定。如果是大括號內為空,則簡潔地寫成{}即可,不需要換行;如果是非空代碼塊則:
1) 左大括號前不換行。
2) 左大括號後換行。
3) 右大括號前換行。
4) 右大括號後還有 else 等代碼則不換行;表示終止的右大括號後必須換行。
2.
【強制】左小括號和字符之間不出現空格;同樣,右小括號和字符之間也不出現空格;而左大括號前需要空格。詳見第 5 條下方正例提示。
反例:if (空格 a == b 空格)
3. 【強制】if/for/while/switch/do 等保留字與括號之間都必須加空格。
4. 【強制】任何二目、三目運算符的左右兩邊都需要加一個空格。
說明:運算符包括賦值運算符=、邏輯運算符&&、加減乘除符號等。
5. 【強制】採用 4 個空格縮進,禁止使用 tab 字符。
說明:如果使用 tab 縮進,必須設置 1 個 tab 為 4 個空格。IDEA 設置 tab 為 4 個空格時, 請勿勾選 Use tab character;而在 eclipse 中,必須勾選 insert spaces for tabs。
6. 【強制】單行字符數限制不超過 120 個,超出需要換行,換行時遵循如下原則:
1) 第二行相對第一行縮進 4 個空格,從第三行開始,不再繼續縮進,參考示例。
2) 運算符與下文一起換行。
3) 方法調用的點符號與下文一起換行。
4) 方法調用中的多個參數需要換行時,在逗號後進行。
5) 在括號前不要換行,見反例。正例:
StringBuffer sb = new StringBuffer();
// 超過 120 個字符的情況下,換行縮進 4 個空格,點號和方法名稱一起換行
sb.append(“zi”).append(“xin”)…
.append(“huang”)…
.append(“huang”)…
.append(“huang”);
反例:
StringBuffer sb = new StringBuffer();
// 超過 120 個字符的情況下,不要在括號前換行
sb.append(“zi”).append(“xin”)…append (“huang”);
// 參數很多的方法調用可能超過 120 個字符,不要在逗號前換行
method(args1, args2, args3, …
, argsX);
7. 【強制】方法參數在定義和傳入時,多個參數逗號後邊必須加空格。正例:下例中實參的 args1,後邊必須要有一個空格。
method(args1, args2, args3);
8. 【強制】IDE 的 text file encoding 設置為 UTF-8; IDE 中文件的換行符使用 Unix 格式, 不要使用 Windows 格式。
9. 【推薦】單個方法的總行數不超過 80 行。
說明:包括方法簽名、結束右大括號、方法內代碼、注釋、空行、回車及任何不可見字符的總行數不超過 80 行。
正例:代碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成為額外方法,使主幹代碼更加清晰;共性邏輯抽取成為共性方法,便於復用和維護。
10. 【推薦】不同邏輯、不同語義、不同業務的代碼之間插入一個空行分隔開來以提升可讀性。說明:任何情形,沒有必要插入多個空行進行隔開。
(四) OOP 規約
1. 【強制】避免通過一個類的對象引用訪問此類的靜態變量或靜態方法,無謂增加編譯器解析成本,直接用類名來訪問即可。
2. 【強制】所有的覆寫方法,必須加@Override 註解。
說明:getObject()與 get0bject()的問題。一個是字母的 O,一個是數字的 0,加@Override 可以準確判斷是否覆蓋成功。另外,如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
3. 【推薦】相同參數類型,相同業務含義,才可以使用 Java 的可變參數,避免使用 Object。說明:可變參數必須放置在參數列表的最後。(提倡同學們盡量不用可變參數編程)
正例:public List<User> listUsers(String type, Long… ids) {…}
4. 【推薦】不能使用過時的類或方法。
說明:java.net.URLDecoder 中的方法 decode(String encodeStr) 這個方法已經過時,應
該使用雙參數 decode(String source, String encode)。接口提供方既然明確是過時接口, 那麼有義務同時提供新的接口;作為調用方來說,有義務去考證過時方法的新實現是什麼。
5. 【強制】Object 的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用
equals。
正例:”test”.equals(object); 反例:object.equals(“test”);
說明:推薦使用 java.util.Objects#equals(JDK7 引入的工具類)
6. 關於基本數據類型與包裝數據類型的使用標準如下:
1) 【強制】所有的 POJO 類屬性必須使用包裝數據類型。
2) 【強制】RPC 方法的返回值和參數必須使用包裝數據類型。
3) 【推薦】所有的局部變量使用基本數據類型。
說明:POJO 類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何
NPE 問題,或者入庫檢查,都由使用者來保證。
正例:數據庫的查詢結果可能是 null,因為自動拆箱,用基本數據類型接收有 NPE 風險。反例:比如顯示成交總額漲跌情況,即正負 x%,x 為基本數據類型,調用的 RPC 服務,調用不成功時,返回的是默認值,頁面顯示為 0%,這是不合理的,應該顯示成中劃線。所以包裝數據類型的 null 值,能夠表示額外的信息,如:遠程調用失敗,異常退出。
7. 【強制】定義 DO/DTO/VO 等 POJO 類時,不要設定任何屬性默認值。
反例:POJO 類的 gmtCreate 默認值為 new Date(),但是這個屬性在數據提取時並沒有置入具體值,在更新其它字段時又附帶更新了此字段,導致創建時間被修改成當前時間。
8. 【強制】序列化類新增屬性時,請不要修改 serialVersionUID 字段,避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。
說明:注意 serialVersionUID 不一致會拋出序列化運行時異常。
9. 【強制】構造方法裏面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
10. 【強制】POJO 類必須寫 toString 方法。使用 IDE 中的工具:source> generate toString
時,如果繼承了另一個 POJO 類,注意在前面加一下 super.toString。
說明:在方法執行拋出異常時,可以直接調用 POJO 的 toString()方法打印其屬性值,便於排查問題。
11. 【強制】禁止在 POJO 類中,同時存在對應屬性 xxx 的 isXxx()和 getXxx()方法。說明:框架在調用屬性 xxx 的提取方法時,並不能確定哪個方法一定是被優先調用到。
12. 【推薦】使用索引訪問用 String 的 split 方法得到的數組時,需做最後一個分隔符後有無內容的檢查,否則會有拋 IndexOutOfBoundsException 的風險。
說明:
String str = “a,b,c,,”;
String[] ary = str.split(“,”);
// 預 期 大 於 3, 結 果 是 3 System.out.println(ary.length);
13. 【推薦】當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起, 便於閱讀,此條規則優先於第 16 條規則。
14. 【推薦】 類內方法定義的順序依次是:公有方法或保護方法 > 私有方法 > getter/setter
方法。
說明:公有方法是類的調用者和維護者最關心的方法,首屏展示最好;保護方法雖然只是子類關心,也可能是「模板設計模式」下的核心方法;而私有方法外部一般不需要特別關心,是一個黑盒實現;因為承載的信息價值較低,所有 Service 和 DAO 的 getter/setter 方法放在類體最後。
15. 【推薦】setter 方法中,參數名稱與類成員變量名稱一致,this.成員名 = 參數名。在
getter/setter 方法中,不要增加業務邏輯,增加排查問題的難度。反例:
public Integer getData() { if (condition) {
return this.data + 100;
} else {
return this.data – 100;
}
}
16. 【推薦】循環體內,字符串的連接方式,使用 StringBuilder 的 append 方法進行擴展。說明:下例中,反編譯出的位元組碼文件顯示每次循環都會 new 出一個 StringBuilder 對象, 然後進行 append 操作,最後通過 toString 方法返回 String 對象,造成內存資源浪費。 反例:
String str = “start”;
for (int i = 0; i < 100; i++) { str = str + “hello”;
}
17. 【推薦】final 可以聲明類、成員變量、方法、以及本地變量,下列情況使用 final 關鍵字:
1) 不允許被繼承的類,如:String 類。
2) 不允許修改引用的域對象。
3) 不允許被重寫的方法,如:POJO 類的setter 方法。
4) 不允許運行過程中重新賦值的局部變量。
5) 避免上下文重複使用一個變量,使用 final 描述可以強制重新定義一個變量,方便更好地進行重構。
18. 【推薦】慎用 Object 的 clone 方法來拷貝對象。
說明:對象的 clone 方法默認是淺拷貝,若想實現深拷貝需要重寫 clone 方法實現域對象的深度遍歷式拷貝。
19. 【推薦】類成員與方法訪問控制從嚴:
1) 如果不允許外部直接通過 new 來創建對象,那麼構造方法必須是 private。
2) 工具類不允許有 public 或 default 構造方法。
3) 類非 static 成員變量並且與子類共享,必須是 protected。
4) 類非 static 成員變量並且僅在本類使用,必須是 private。
5) 類 static 成員變量如果僅在本類使用,必須是 private。
6) 若是 static 成員變量,考慮是否為 final。
7) 類成員方法只供類內部調用,必須是 private。
8) 類成員方法只對繼承類公開,那麼限制為 protected。
說明:任何類、方法、參數、變量,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模塊解耦。思考:如果是一個 private 的方法,想刪除就刪除,可是一個 public 的 service 成員方法或成員變量,刪除一下,不得手心冒點汗嗎?變量像自己的小孩,盡量在自己的視線內,變量作用域太大,無限制的到處跑,那麼你會擔心的。
(五) 集合處理
1. 【強制】關於 hashCode 和 equals 的處理,遵循如下規則:
1) 只要重寫 equals,就必須重寫 hashCode。
2) 因為 Set 存儲的是不重複的對象,依據 hashCode 和 equals 進行判斷,所以 Set 存儲的對象必須重寫這兩個方法。
3) 如果自定義對象作為 Map 的鍵,那麼必須重寫 hashCode 和 equals。
說明:String 重寫了hashCode 和 equals 方法,所以我們可以非常愉快地使用 String 對象作為 key 來使用。
2. 【強制】使用集合轉數組的方法,必須使用集合的 toArray(T[] array),傳入的是類型完全一樣的數組,大小就是 list.size()。
說明:使用 toArray 帶參方法,入參分配的數組空間不夠大時,toArray 方法內部將重新分配內存空間,並返回新數組地址;如果數組元素個數大於實際所需,下標為[ list.size() ] 的數組元素將被置為 null,其它數組元素保持原值,因此最好將方法入參數組大小定義與集合元素個數一致。
正例:
List<String> list = new ArrayList<String>(2); list.add(“guan”);
list.add(“bao”);
String[] array = new String[list.size()]; array = list.toArray(array);
反例:直接使用 toArray 無參方法存在問題,此方法返回值只能是 Object[]類,若強轉其它類型數組將出現 ClassCastException 錯誤。
3. 【強制】使用工具類 Arrays.asList()把數組轉換成集合時,不能使用其修改集合相關的方法,它的 add/remove/clear 方法會拋出
UnsupportedOperationException 異常。
說明:asList 的返回對象是一個 Arrays 內部類,並沒有實現集合的修改方法。Arrays.asList體現的是適配器模式,只是轉換接口,後台的數據仍是數組。
String[] str = new String[] { “you”, “wu” }; List list = Arrays.asList(str);
第一種情況:list.add(“yangguanbao”); 運行時異常。
第二種情況:str[0] = “gujin”; 那麼 list.get(0)也會隨之修改。
4. 【強制】不要在 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」,會是同樣的結果嗎?
5. 【強制】在 JDK7 版本及以上,Comparator 實現類要滿足如下三個條件,不然 Arrays.sort,
Collections.sort 會報 IllegalArgumentException 異常。說明:三個條件如下
1) x,y 的比較結果和 y,x 的比較結果相反。
2) 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;
}
};
6. 【推薦】集合泛型定義時,在 JDK7 及以上,使用 diamond 語法或全省略。說明:菱形泛型,即 diamond,直接使用<>來指代前邊已經指定的類型。
正例:
// <> diamond 方式
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
7. 【推薦】集合初始化時,指定集合初始值大小。
說明:HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity = (需要存儲的元素個數 / 負載因子) + 1。注意負載因子(即 loader
factor)默認為 0.75,如果暫時無法確定初始值大小,請設置為 16(即默認值)。
反例:HashMap 需要放置 1024 個元素,由於沒有設置容量初始大小,隨着元素不斷增加,容量 7 次被迫擴大,resize 需要重建 hash 表,嚴重影響性能。
8. 【推薦】使用 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 值組合集合。
9. 【推薦】高度注意 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 異常。
10. 【參考】合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。
說明:有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是
order/sort。
11. 【參考】利用 Set 元素唯一的特性,可以快速對一個集合進行去重操作,避免使用 List 的
contains 方法進行遍歷、對比、去重操作。
(六) 並發處理
1. 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。說明:資源驅動類、工具類、單例工廠類都需要注意。
2. 【推薦】創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。正例:
public class TimerTaskThread extends Thread { public TimerTaskThread() {
super.setName(“TimerTaskThread”);
…
}
}
3. 【推薦】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。
說明:使用線程池的好處是減少在創建和銷毀線程上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者「過度切換」的問題。
4. 【推薦】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
說明:Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。
5. 【強制】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。
6. 【推薦】高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
說明:儘可能使加鎖的代碼塊工作量儘可能的小,避免在鎖代碼塊中調用 RPC 方法。
7. 【推薦】並發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在緩存加鎖,要麼在數據庫層使用樂觀鎖,使用 version 作為更新依據。
說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。
8. 【強制】多線程並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
9. 【強制】禁止在循環語句中進行數據庫增刪改查操作,高並發時,會耗盡數據庫連接池資源,如果遇到耗時的操作,會導致連接佔用時間過長而一直得不到釋放,會造成其他請求獲取不到連接的情況。循環插入、更新、刪除數據庫可以使用jdbc batch update進行操作,循環查詢數據庫可以一次查詢滿足條件的所有數據,然後再進行篩選。
10. 【推薦】使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown 方法,線程執行代碼注意 catch 異常,確保 countDown 方法被執行到,避免主線程無法執行至 await 方法,直到超時才返回結果。
說明:注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
11. 【推薦】避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一
seed 導致的性能下降。
說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線程持有一個實例。
12. 【參考】volatile 解決多線程內存不可見問題。對於一寫多讀,是可以解決變量同步問題, 但是如果多寫,同樣無法解決線程安全問題。如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。
13. 【參考】 HashMap 在容量不夠進行 resize 時由於高並發可能出現死鏈,導致 CPU 飆升,在開發過程中可以使用其它數據結構或加鎖來規避此風險。
14. 【參考】ThreadLocal 無法解決共享對象的更新問題,ThreadLocal 對象建議使用 static 修飾。這個變量是針對一個線程內所有操作共享的,所以設置為靜態變量,所有此類實例共享此靜態變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個線程內定義的)都可以操控這個變量。
(七) 控制語句
1. 【強制】在一個 switch 塊內,每個 case 要麼通過 break/return 等來終止,要麼注釋說明程序將繼續執行到哪一個 case 為止;在一個 switch 塊內,都必須包含一個 default 語句並且放在最後,即使空代碼。
2. 【強制】在 if/else/for/while/do 語句中必須使用大括號。即使只有一行代碼,避免採用單行的編碼方式:if (condition) statements;
3. 【推薦】表達異常的分支時,少用 if-else 方式,這種方式可以改寫成:
if (condition) {
…
return obj;
}
// 接着寫 else 的業務邏輯代碼;
說明:如果非得使用 if()…else if()…else…方式表達邏輯,【強制】避免後續代碼維護困難,請勿超過 3 層。
正例:超過 3 層的 if-else 的邏輯判斷代碼可以使用衛語句、策略模式、狀態模式等來實現, 其中衛語句示例如下:
public void today() { if (isBusy()) {
System.out.println(「change time.」); return;
}
if (isFree()) {
System.out.println(「go to travel.」); return;
}
System.out.println(「stay at home to learn Alibaba Java Coding Guidelines.」); return;
}
4. 【推薦】除常用方法(如 getXxx/isXxx)等外,不要在條件判斷中執行其它複雜的語句,將複雜邏輯判斷的結果賦值給一個有意義的布爾變量名,以提高可讀性。
說明:很多 if 語句內的邏輯相當複雜,閱讀者需要分析條件表達式的最終結果,才能明確什麼樣的條件執行什麼樣的語句,那麼,如果閱讀者分析邏輯表達式錯誤呢?
正例:
// 偽代碼如下
final boolean existed = (file.open(fileName, “w”) != null) && (…) || (…); if (existed) {
…
}
反例:
if ((file.open(fileName, “w”) != null) && (…) || (…)) {
…
}
5. 【推薦】循環體中的語句要考量性能,以下操作盡量移至循環體外處理,如定義對象、變量、獲取數據庫連接,進行不必要的 try-catch 操作(這個 try-catch 是否可以移至循環體外)。
6. 【推薦】避免採用取反邏輯運算符。
說明:取反邏輯不利於快速理解,並且取反邏輯寫法必然存在對應的正向邏輯寫法。正例:使用 if (x < 628) 來表達 x 小於 628。
反例:使用 if (!(x >= 628)) 來表達 x 小於 628。
7. 【參考】下列情形,需要進行參數校驗:
1) 調用頻次低的方法。
2) 執行時間開銷很大的方法。此情形中,參數校驗時間幾乎可以忽略不計,但如果因為參數錯誤導致中間執行回退,或者錯誤,那得不償失。
3) 需要極高穩定性和可用性的方法。
4) 對外提供的開放接口,不管是 RPC/API/HTTP 接口。
5) 敏感權限入口。
8. 【參考】下列情形,不需要進行參數校驗:
1) 極有可能被循環調用的方法。但在方法說明裡必須註明外部參數檢查要求。
2) 底層調用頻度比較高的方法。畢竟是像純凈水過濾的最後一道,參數錯誤不太可能到底層才會暴露問題。一般 DAO 層與 Service 層都在同一個應用中,部署在同一台服務器中,所以 DAO 的參數校驗,可以省略。
3) 被聲明成 private 只會被自己代碼所調用的方法,如果能夠確定調用方法的代碼傳入參數已經做過檢查或者肯定不會有問題,此時可以不校驗參數。
(八) 注釋規約
1. 【強制】類、類屬性、類方法的注釋必須使用 Javadoc 規範,使用/**內容*/格式,不得使用
// xxx 方式。
說明:在 IDE 編輯窗口中,Javadoc 方式會提示相關注釋,生成 Javadoc 可以正確輸出相應注釋;在 IDE 中,工程調用方法時,不進入方法即可懸浮提示方法、參數、返回值的意義,提高閱讀效率。
2. 【強制】所有的抽象方法(包括接口中的方法)必須要用 Javadoc 注釋、除了返回值、參數、異常說明外,還必須指出該方法做什麼事情,實現什麼功能。
說明:對子類的實現要求,或者調用注意事項,請一併說明。
3. 【強制】所有的類都必須添加創建者和創建日期。
4. 【強制】方法內部單行注釋,在被注釋語句上方另起一行,使用//注釋。方法內部多行注釋使用/* */注釋,注意與代碼對齊。
5. 【強制】所有的枚舉類型字段必須要有注釋,說明每個數據項的用途。
6. 【推薦】與其「半吊子」英文來注釋,不如用中文注釋把問題說清楚。專有名詞與關鍵字保持英文原文即可。
反例:「TCP 連接超時」解釋成「傳輸控制協議連接超時」,理解反而費腦筋。
7. 【推薦】代碼修改的同時,注釋也要進行相應的修改,尤其是參數、返回值、異常、核心邏輯等的修改。
說明:代碼與注釋更新不同步,就像路網與導航軟件更新不同步一樣,如果導航軟件嚴重滯後, 就失去了導航的意義。
8. 【參考】謹慎注釋掉代碼。在上方詳細說明,而不是簡單地注釋掉。如果無用,則刪除。
說明:代碼被注釋掉有兩種可能性:1)後續會恢復此段代碼邏輯。2)永久不用。前者如果沒有備註信息,難以知曉注釋動機。後者建議直接刪掉(代碼倉庫保存了歷史代碼)。
9. 【參考】對於注釋的要求:第一、能夠準確反應設計思想和代碼邏輯;第二、能夠描述業務含義,使別的程序員能夠迅速了解到代碼背後的信息。完全沒有注釋的大段代碼對於閱讀者形同天書,注釋是給自己看的,即使隔很長時間,也能清晰理解當時的思路;注釋也是給繼任者看的,使其能夠快速接替自己的工作。
10. 【參考】好的命名、代碼結構是自解釋的,注釋力求精簡準確、表達到位。避免出現注釋的一個極端:過多過濫的注釋,代碼的邏輯一旦修改,修改注釋是相當大的負擔。
反例:
// put elephant into fridge put(elephant, fridge);
方法名 put,加上兩個有意義的變量名 elephant 和 fridge,已經說明了這是在幹什麼,語
義清晰的代碼不需要額外的注釋。
11. 【參考】特殊注釋標記,請註明標記人與標記時間。注意及時處理這些標記,通過標記掃描, 經常清理此類標記。線上故障有時候就是來源於這些標記處的代碼。 1) 待辦事宜(TODO):( 標記人,標記時間,[預計處理時間])
表示需要實現,但目前還未實現的功能。這實際上是一個 Javadoc 的標籤,目前的 Javadoc
還沒有實現,但已經被廣泛使用。只能應用於類,接口和方法(因為它是一個 Javadoc 標籤)。
2) 錯誤,不能工作(FIXME):(標記人,標記時間,[預計處理時間])
在注釋中用 FIXME 標記某代碼是錯誤的,而且不能工作,需要及時糾正的情況。
(九) 其它
1. 【強制】velocity 調用 POJO 類的屬性時,建議直接使用屬性名取值即可,模板引擎會自動按規範調用 POJO 的 getXxx(),如果是 boolean 基本數據類型變量(boolean 命名不需要加 is 前綴),會自動調用 isXxx()方法。
說明:注意如果是 Boolean 包裝類對象,優先調用 getXxx()的方法。
2. 【強制】注意 Math.random() 這個方法返回是 double 類型,注意取值的範圍 0≤x<1(能夠取到零值,注意除零異常),如果想獲取整數類型的隨機數,不要將 x 放大 10 的若干倍然後取整,直接使用 Random 對象的 nextInt 或者 nextLong 方法。
3. 【強制】獲取當前毫秒數 System.currentTimeMillis(); 而不是new Date().getTime(); 說明:如果想獲取更加精確的納秒級時間值,使用 System.nanoTime()的方式。在 JDK8 中, 針對統計時間等場景,推薦使用 Instant 類。
4. 【推薦】任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存。
5. 【推薦】及時清理不再使用的代碼段或配置信息。
說明:對於垃圾代碼或過時配置,堅決清理乾淨,避免程序過度臃腫,代碼冗餘。
正例:對於暫時被注釋掉,後續可能恢復使用的代碼片斷,在注釋代碼上方,統一規定使用三個斜杠(///)來說明注釋掉代碼的理由。
一、異常日誌
(一) 異常處理
1. 【強制】Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過
catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException 等等。說明:無法通過預檢查的異常除外,比如,在解析字符串形式的數字時,不得不通過 catch
NumberFormatException 來實現。正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}
2. 【強制】異常不要用來做流程控制,條件控制。
說明:異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
3. 【強制】catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對於非穩定代碼的 catch 儘可能進行區分異常類型,再做對應的異常處理。
說明:對大段代碼進行 try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利於定位問題,這是一種不負責任的表現。
正例:用戶註冊的場景中,如果用戶輸入非法字符,或用戶名稱已存在,或用戶輸入密碼過於簡單,在程序上作出分門別類的判斷,並提示給用戶。
4. 【強制】捕獲異常是為了處理它,不要捕獲了卻什麼都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的內容。
5. 【強制】有 try 塊放到了事務代碼中,catch 異常後,如果需要回滾事務,一定要注意手動回滾事務。
6. 【強制】finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch。說明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
7. 【強制】不要在 finally 塊中使用 return。
說明:finally 塊中的 return 返回後方法結束執行,不會再執行 try 塊中的 return 語句。
8. 【強制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。說明:如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。
9. 【推薦】方法的返回值可以為 null,不強制返回空集合,或者空對象等,必須添加註釋充分說明什麼情況下會返回 null 值。
說明:本手冊明確防止 NPE 是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也並非高枕無憂,必須考慮到遠程調用失敗、序列化失敗、運行時異常等場景返回
null 的情況。
10. 【推薦】防止 NPE,是程序員的基本修養,注意 NPE 產生的場景:
1) 返回類型為基本數據類型,return 包裝數據類型的對象時,自動拆箱有可能產生 NPE。反例:public int f() { return Integer 對象}, 如果為null,自動解箱拋 NPE。
2) 數據庫的查詢結果可能為 null。
3) 集合里的元素即使 isNotEmpty,取出的數據元素也可能為 null。
4) 遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。
5) 對於 Session 中獲取的數據,建議 NPE 檢查,避免空指針。
6) 級聯調用 obj.getA().getB().getC();一連串調用,易產生 NPE。正例:使用 JDK8 的 Optional 類來防止 NPE 問題。
11. 【推薦】定義時區分 unchecked / checked 異常,避免直接拋出 new RuntimeException(), 更不允許拋出 Exception 或者 Throwable,應使用有業務含義的自定義異常。推薦業界已定義過的自定義異常,如:DAOException / ServiceException 等。
12. 【參考】對於公司外的 http/api 開放接口必須使用「錯誤碼」;而應用內部推薦異常拋出; 跨應用間 RPC 調用優先考慮使用 Result 方式,封裝 isSuccess()方法、「錯誤碼」、「錯誤簡短訊息」。
說明:關於 RPC 方法返回方式使用 Result 方式的理由:
1) 使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
2) 如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對於調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。
13. 【參考】避免出現重複的代碼(Don』t Repeat Yourself),即 DRY 原則。
說明:隨意複製和粘貼代碼,必然會導致代碼的重複,在以後需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。
正例:一個類中有多個 public 方法,都需要進行數行相同的參數校驗操作,這個時候請抽取:
private boolean checkParam(DTO dto) {…}
(二) 日誌規約
1. 【強制】應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架
SLF4J 中的 API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2. 【強制】對 trace/debug/info 級別的日誌輸出,必須使用條件輸出形式或者使用佔位符的方式。
說明:logger.debug(“Processing trade with id: ” + id + ” and symbol: ” + symbol); 如果日誌級別是 warn,上述日誌不會打印,但是會執行字符串拼接操作,如果 symbol 是對象,會執行 toString()方法,浪費了系統資源,執行了上述操作,最終日誌卻沒有打印。
正例:(條件)建設採用如下方式
if (logger.isDebugEnabled()) {
logger.debug(“Processing trade with id: ” + id + ” and symbol: ” + symbol);
}
正例:(佔位符)
logger.debug(“Processing trade with id: {} and symbol : {} “, id, symbol);
3. 【強制】避免重複打印日誌,浪費磁盤空間,務必在 log4j.xml 中設置 additivity=false。正例:<logger name=”com.test.dubbo.config” additivity=”false”>
4. 【強制】異常信息應該包括兩類信息:案發現場信息和異常堆棧信息。如果不處理,那麼通過關鍵字 throws 往上拋出。
正例:logger.error(各類參數或者對象 toString() + “_” + e.getMessage(), e);
5. 【推薦】謹慎地記錄日誌。生產環境禁止輸出 debug 日誌;有選擇地輸出 info 日誌;如果使用 warn 來記錄剛上線時的業務行為信息,一定要注意日誌輸出量的問題,避免把服務器磁盤撐爆,並記得及時刪除這些觀察日誌。
說明:大量地輸出無效日誌,不利於系統性能提升,也不利於快速定位錯誤點。記錄日誌時請思考:這些日誌真的有人看嗎?看到這條日誌你能做什麼?能不能給問題排查帶來好處?
6. 【推薦】可以使用 warn 日誌級別來記錄用戶輸入參數錯誤的情況,避免用戶投訴時,無所適從。如非必要,請不要在此場景打出 error 級別,避免頻繁報警。
說明:注意日誌輸出的級別,error 級別只記錄系統邏輯出錯、異常或者重要的錯誤信息。
二、安全規約
(一)數據庫訪問
1. 【強制】用戶輸入的 SQL 參數嚴格使用參數綁定或者 METADATA 字段值限定,防止 SQL 注入, 禁止字符串拼接 SQL 訪問數據庫。
2. 【強制】數據庫連接字符串不應當在應用程序中硬編碼。連接字符串應當存儲在一個可信服務器的獨立配置文件中,必要時,賬戶和密碼字段需要加密。
3. 【推薦】當應用程序訪問數據庫時,應使用儘可能最低的權限。
(二)服務器管理
1. 【強制】關閉服務器中文件上傳目錄的運行權限,防止或限制上傳任意可能被 Web服務器解析的文件。
2. 【推薦】將 Web服務器、進程和服務的賬戶限制為儘可能低的權限。
3. 【推薦】為存儲在服務器中的敏感信息提供恰當的訪問控制。這包括緩存的數據、臨時文件以及只允許特定系統用戶訪問的數據。
(三)訪問控制
1. 【強制】用戶請求傳入的任何參數必須做有效性驗證。說明:忽略參數校驗可能導致:
l page size 過大導致內存溢出
l 惡意 order by 導致數據庫慢查詢
l 任意重定向
l SQL 注入
l 反序列化注入
l 正則輸入源串拒絕服務 ReDoS
說明:Java 代碼用正則來驗證客戶端的輸入,有些正則寫法驗證普通用戶輸入沒有問題, 但是如果攻擊人員使用的是特殊構造的字符串來驗證,有可能導致死循環的結果。
2. 【強制】禁止向 HTML 頁面輸出未經安全過濾或未正確轉義的用戶數據。
3. 【強制】表單、AJAX 提交必須執行 CSRF 安全驗證。
說明:CSRF(Cross-site request forgery)跨站請求偽造是一類常見編程漏洞。對於存在
CSRF 漏洞的應用/網站,攻擊者可以事先構造好 URL,只要受害者用戶一訪問,後台便在用戶不知情的情況下對數據庫中用戶參數進行相應修改。
4. 【推薦】為包含敏感信息或功能、且連接到外部系統的連接使用 TLS。
5. 【強制】不要在 HTTP GET請求參數中包含敏感信息。
6. 【強制】禁止表單中的自動填充功能,因為表單中可能包含敏感信息,包括身份驗證信息。
7. 【強制】禁止客戶端緩存網頁,因為可能包含敏感信息。「Cache-Control: no-store」,可以和 HTTP報頭控制「Pragma: no-cache」一起使用,該控制不是非常有效,但是與 HTTP/1.0向後兼容。
8. 【推薦】為防範對隨機數據的猜測攻擊,應當使用加密模塊中已驗證的隨機數生成器生成所有的隨機數、隨機文件名、隨機 GUID和隨機字符串。
9. 【強制】不要在錯誤響應中泄露敏感信息,包括:系統的詳細信息、會話標識符或者帳號信息。
10. 【推薦】不要在日誌中保存敏感信息,包括:不必要的系統詳細信息、會話標識符或密碼。
11. 【強制】限制只有授權的用戶才能訪問受保護的 URL。
12. 【強制】不要在 URL、錯誤信息或日誌中暴露會話標識符。會話標識符應當只出現在 HTTP cookie頭信息中。比如,不要將會話標識符以 GET 參數進行傳遞。
13. 【推薦】如果任何潛在的 危險字符 必須被作為輸入,請確保您執行了額外的控制,比如:輸出編碼、特定的安全 API、以及在應用程序中使用的原因。部分常見的危險字符包括:< > ” ‘ % ( ) & + \ \’ \” 。
(四)身份認證
1. 【推薦】為所有要求身份驗證的訪問內容和所有其他的敏感信息提供 TLS連接
2. 【強制】在身份驗證的時候,如果連接從 HTTP變為 HTTPS,則生成一個新的會話標識符。在應用程序中,推薦持續使用 HTTPS,而非在 HTTP和 HTTPS之間轉換。
3. 【強制】所有的身份驗證過程必須在可信系統(比如:服務器)上執行,身份驗證的失敗提示信息應當避免過於明確。比如:可以使用「用戶名和/或密碼錯誤」,而不要使用「用戶名錯誤」或者「密碼錯誤」。錯誤提示信息在顯示和源代碼中應保持一致。
三、MySQL 數據庫
(一) 建表規約
1. 【強制】表名必須以t_開頭,如:t_user,函數以f_開頭,存儲過程已p_開頭,視圖以v_開頭。
2. 【強制】字段名按照不同的數據類型進行命名,字符、文本類型以c_開頭,如:c_user_name,整型(包含int,tinyint,bigint)、數字類型都統一以n_開頭、時間類型用長時間格式進行存儲,也以n_開頭,double數據類型以d_開頭,float數據類型以f_開頭。
3. 【強制】表名、字段名必須使用小寫字母或數字,禁止出現數字開頭,禁止兩個下劃線中間只出現數字。數據庫字段名的修改代價很大,因為無法進行預發佈,所以字段名稱需要慎重考慮。說明:MySQL 在 Windows 下不區分大小寫,但在 Linux 下默認是區分大小寫。因此,數據庫名、表名、字段名,都不允許出現任何大寫字母,避免節外生枝。
正例:super_admin,rpc_config,level3_name 反例:superAdmin,rpcConfig,level_3_name
4. 【強制】表名不使用複數名詞。
說明:表名應該僅僅表示表裏面的實體內容,不應該表示實體數量,對應於 DO 類名也是單數形式,符合表達習慣。
5. 【強制】禁用保留字,如 desc、range、match、delayed 等,請參考 MySQL 官方保留字。
6. 【強制】主鍵索引名為 pk_字段名;唯一索引名為 uk_字段名;普通索引名則為 idx_字段名。說明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的簡稱。
7. 【強制】varchar 是可變長字符串,不預先分配存儲空間,長度不要超過 5000,如果存儲長度大於此值,定義字段類型為 text,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率。
8. 【推薦】庫名與應用名稱盡量一致。
9. 【推薦】如果修改字段含義或對字段表示的狀態追加時,需要及時更新字段注釋。
10. 【推薦】字段允許適當冗餘,以提高查詢性能,但必須考慮數據一致。冗餘字段應遵循: 1)不是頻繁修改的字段。
2)不是 varchar 超長字段,更不能是 text 字段。
正例:商品類目名稱使用頻率高,字段長度短,名稱基本一成不變,可在相關聯的表中冗餘存儲類目名稱,避免關聯查詢。
11. 【推薦】單錶行數超過 500 萬行或者單表容量超過 2GB,才推薦進行分庫分表。
說明:如果預計三年後的數據量根本達不到這個級別,請不要在創建表時就分庫分表。
12. 【參考】合適的字符存儲長度,不但節約數據庫表空間、節約索引存儲,更重要的是提升檢索速度。
正例:如下表,其中無符號值可以避免誤存負數,且擴大了表示範圍。
對象 | 年齡區間 | 類型 | 位元組 | 表示範圍 |
人 | 150 歲之內 | tinyint unsigned | 1 | 無符號值:0 到 255 |
龜 | 數百歲 | smallint unsigned | 2 | 無符號值:0 到 65535 |
恐龍化石 | 數千萬年 | int unsigned | 4 | 無符號值:0 到約 42.9 億 |
太陽 | 約 50 億年 | bigint unsigned | 8 | 無符號值:0 到約 10 的 19 次方 |
(二) 索引規約
1. 【強制】業務上具有唯一特性的字段,即使是多個字段的組合,也必須建成唯一索引。
說明:不要以為唯一索引影響了 insert 速度,這個速度損耗可以忽略,但提高查找速度是明顯的;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有臟數據產生。
2. 【強制】超過三個表禁止 join。需要 join 的字段,數據類型必須絕對一致;多表關聯查詢時, 保證被關聯的字段需要有索引。
說明:即使雙表 join 也要注意表索引、SQL 性能。
3. 【推薦】在 varchar 字段上建立索引時,必須指定索引長度,沒必要對全字段建立索引,根據實際文本區分度決定索引長度即可。
說明:索引的長度與區分度是一對矛盾體,一般對字符串類型數據,長度為 20 的索引,區分度會高達 90%以上,可以使用 count(distinct left(列名, 索引長度))/count(*)的區分度來確定。
4. 【推薦】如果有 order by 的場景,請注意利用索引的有序性。order by 最後的字段是組合索引的一部分,並且放在索引組合順序的最後,避免出現 file_sort 的情況,影響查詢性能。正例:where a=? and b=? order by c; 索引:a_b_c
反例:索引中有範圍查找,那麼索引有序性無法利用,如:WHERE a>10 ORDER BY b; 索引
a_b 無法排序。
5. 【推薦】利用覆蓋索引來進行查詢操作,避免回表。
說明:如果一本書需要知道第 11 章是什麼標題,會翻開第 11 章對應的那一頁嗎?目錄瀏覽一下就好,這個目錄就是起到覆蓋索引的作用。
正例:能夠建立索引的種類分為主鍵索引、唯一索引、普通索引三種,而覆蓋索引只是一種查詢的一種效果,用 explain 的結果,extra 列會出現:using index。
6. 【推薦】利用延遲關聯或者子查詢優化超多分頁場景。
說明: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
7. 【推薦】SQL 性能優化的目標:至少要達到 range 級別,要求是 ref 級別,如果可以是 consts
最好。說明:
1) consts 單表中最多只有一個匹配行(主鍵或者唯一索引),在優化階段即可讀取到數據。
2) ref 指的是使用普通的索引(normal index)。
3) range 對索引進行範圍檢索。
反例:explain 表的結果,type=index,索引物理文件全掃描,速度非常慢,這個 index 級別比較 range 還低,與全表掃描是小巫見大巫。
8. 【推薦】建組合索引的時候,區分度最高的在最左邊。
正例:如果 where a=? and b=? ,如果a 列的幾乎接近於唯一值,那麼只需要單建 idx_a
索引即可。
說明:存在非等號和等號混合時,在建索引時,請把等號條件的列前置。如:where c>? and
d=? 那麼即使 c 的區分度更高,也必須把 d 放在索引的最前列,即索引 idx_d_c。
9. 【推薦】防止因字段類型不同造成的隱式轉換,導致索引失效。
10. 【參考】創建索引時避免有如下極端誤解:
1) 寧濫勿缺。認為一個查詢就需要建一個索引。
2) 寧缺勿濫。認為索引會消耗空間、嚴重拖慢更新和新增速度。
3) 抵制惟一索引。認為業務的惟一性一律需要在應用層通過「先查後插」方式解決。
(三) SQL 語句
1. 【強制】不要使用 count(列名)或 count(常量)來替代 count(*),count(*)是 SQL92 定義的標準統計行數的語法,跟數據庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值為 NULL 的行,而 count(列名)不會統計此列為 NULL 值的行。
2. 【強制】count(distinct col) 計算該列除 NULL 之外的不重複行數,注意 count(distinct col1, col2) 如果其中一列全為 NULL,那麼即使另一列有不同的值,也返回為 0。
3. 【強制】當某一列的值全是 NULL 時,count(col)的返回結果為 0,但 sum(col)的返回結果為NULL,因此使用 sum()時需注意 NPE 問題。
正例:可以使用如下方式來避免 sum 的 NPE 問題:SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;
4. 【強制】使用 ISNULL()來判斷是否為 NULL 值。說明:NULL 與任何值的直接比較都為 NULL。
1) NULL<>NULL 的返回結果是 NULL,而不是 false。
2) NULL=NULL 的返回結果是 NULL,而不是 true。
3) NULL<>1 的返回結果是 NULL,而不是 true。
5. 【強制】在代碼中寫分頁查詢邏輯時,若 count 為 0 應直接返回,避免執行後面的分頁語句。
6. 【強制】不得使用外鍵與級聯,一切外鍵概念必須在應用層解決。
說明:以學生和成績的關係為例,學生表中的 student_id 是主鍵,那麼成績表中的 student_id 則為外鍵。如果更新學生表中的 student_id,同時觸發成績表中的 student_id 更新,即為級聯更新。外鍵與級聯更新適用於單機低並發,不適合分佈式、高並發集群;級聯更新是強阻塞,存在數據庫更新風暴的風險;外鍵影響數據庫的插入速度。
7. 【推薦】禁止使用存儲過程,存儲過程難以調試和擴展,更沒有移植性。
8. 【推薦】數據訂正(特別是刪除、修改記錄操作)時,要先 select,避免出現誤刪除,確認無誤才能執行更新語句。
9. 【推薦】in 操作能避免則避免,若實在避免不了,需要仔細評估 in 後邊的集合元素數量,控制在 1000 個之內。
10. 【參考】如果有國際化需要,所有的字符存儲與表示,均以 utf-8 編碼,注意字符統計函數的區別。
說明:
SELECT LENGTH(“輕鬆工作”); 返回為 12
SELECT CHARACTER_LENGTH(“輕鬆工作”); 返回為 4
如果需要存儲表情,那麼選擇 utf8mb4 來進行存儲,注意它與 utf-8 編碼的區別。
11. 【參考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系統和事務日誌資源少,但 TRUNCATE
無事務且不觸發 trigger,有可能造成事故,故不建議在開發代碼中使用此語句。說明:TRUNCATE TABLE 在功能上與不帶 WHERE 子句的 DELETE 語句相同
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/281217.html