天天看點

《阿裡巴巴 Java 開發手冊》讀書筆記

偶然看到阿裡巴巴居然出書了???趁着滿減活動(節約節約....)我趕緊買來準備看看,剛拿到的時候掂量了好多下,總覺得商家給我少發了一本書,結果打開才知道..原來這本書這麼小....

編碼規範的重要性

别人都說我們是搬磚的碼農,但我們知道自己是追求個性的藝術家。也許我們不會過多在意自己的外表和穿着,但在我們不羁的外表下,骨子裡追求着代碼的美、系統的美、設計的美,代碼規範其實就是一個對程式美的定義。—— 引自 序

如果有一天在我們的項目中看到了這樣的代碼:

或者是這樣的代碼:

這樣美不美呢?或許看着是還挺美的,但是如果需要修改,是不是人傻啦?

那這樣的代碼呢?

作為一個對自己有一定要求的程式猿,是不是第一反應就是:

  • 重寫!
  • 原作者是誰?錘他!

規範不一,就會像下圖中的小鴨和小雞對話一樣,語言不通,一臉囧相。雞同鴨講也恰恰形容了人與人之間溝通的痛點,自說自話,無法達成一緻意見。再舉一個生活中的例子,交通規則靠左行駛還是靠右行駛,兩者孰好孰壞并不重要,重要的是必須要在統一的方向上通行,表面上限制了自由,但實際上是保障了公衆的人身安全。試想,如果沒有規定靠右行駛,那樣的路況肯定擁堵不堪,險象環生。同樣,過分自由随意、天馬行空的代碼會嚴重的傷害系統的健康,影響到可擴充性以及可維護性。

  • 總結:代碼規範很重要!

關于編碼規範的三大聖戰

衆所周知,網際網路公司的優勢在于效率,它是企業核心競争力。展現在産品開發領域,就是夠溝通效率和研發效率。對于溝通效率的重要性,可以從程式猿三大 “編碼理念之争” 說起:

  • 縮進采用空格鍵,還是 Tab 鍵
  • if 單行語句需要大括号還是不需要大括号
  • 左大括号不換行,還是單獨另起一行

在美劇《矽谷》中,有這樣的一個經典鏡頭:

  • 程式媛:Kid? 我們似乎很久沒有一起睡了。
  • 程式猿:現在?不可能!我永遠不會和使用空格來縮進的人睡在一起!
  • 程式媛:(瘋狂敲 space 氣走了程式猿)
  • 程式猿:(甩了一句)一個 Tab 可以代替 8個 空格!

    之後程式猿就因為視圖一步跨下八個階梯而摔了....

Tab 鍵和空格鍵的争議确實存在,并且在知乎上讨論得火熱:寫代碼時,縮進使用 tab 還是空格?

  • 總結:使用 4 個空格好,在《阿裡巴巴 Java 開發手冊》中也明确支援了這樣的做法。下面也引用一張圖來調侃一下。

if 單語句是否需要換行,也是争論不休的話題。相對來說,寫過格式縮進類程式設計語言的開發者, 更加習慣于不加大括号。《手冊》中明确 if/for 單行語句必須加大括号,因為單行語句的寫法,容易在添加邏輯時引起視覺上的錯誤判斷。此外,if 不加大括号還會有局部變量作用域的問題。

左大括号是否單獨另起一行?因為 Go 語言的強制不換行,在這點上,“程式設計理念之争” 的硝煙味似乎沒有那麼濃。如果一定要給一個理由,那麼換行的代碼可以增加一行,對于按代碼行數考核工作量的公司員工,肯定傾向于左大括号前換行。《手冊》明确左大括号不換行!

  • 總結: 其實,很多程式設計方式客觀上沒有對錯之分,一緻性很重要,可讀性很重要,團隊溝通效率很重要。

第1章:程式設計規約

這一章是對傳統意義上的代碼規範,包括變量命名、代碼風格、控制語句、代碼注釋等基本的變成習慣,以及從高并發場景中提煉出來的集合處理技巧與并發多線程的注意事項。

1.1 命名風格

第一條:【強制】代碼中的命名均不能以下劃線或美元符号開始,也不能以下劃線或美元符号結束。

  • 反例:

    _name

    /

    $name

    name_

    name$

盡管

$

可以作為辨別符使用,然而我們應該盡量避免對其使用。

  • 原因:

    $

    通常在編譯器生成的辨別符名稱中使用,如果我們也使用這個符号,可能會有一些意想不到的錯誤發生....
  • 意想不到的錯誤示例:
package test;

public class User$VIP {
	public static void main(String[] args) {
		User user = new User();
		User.VIP vip = user.new VIP();
		vip.print();
	}
}

class User{
	class VIP{
		void print(){
			System.out.println("成員類");
		}
	}
}
           

仔細閱讀以下,似乎并沒有什麼問題,代碼也比較簡單,但正在我們編譯的時候,IDEA提示我們:

定義了重複的代碼?歸根到底,都是

$

惹的禍!因為

$

被編譯器所使用,在源檔案(.java 檔案)編譯成位元組碼(.class 檔案)後,會稱為頂層類型與嵌套類型之間的連接配接符。例如,如果存在一個頂層類 A,在其内聲明了一個成員類 B,那麼編譯之後就會産生兩個 class 檔案,分别為

A.class

A$B.class

就本程式來說,會生成 3 個 class 檔案(如果可以編譯的話),分别是

User$VIP.class

(頂層類)、

User.class

User$VIP.class

(User 類的成員類,也就是類 VIP)。由于試圖存在兩個

User$VIP.class

是以才會報錯!

第三至第六條:【強制】

  • 類名使用 UpperCamelCase 風格,方法名、參數名、成員變量、局部變量都同意使用 lowerCamelCase 風格,必須遵從駝峰形式。
  • 變量命名全部大寫,單詞兼用下劃線隔開,力求予以表達完整清楚,不要嫌名字太長。

正例:MAX_STOCK_COUNT / PRIZE_NUMBER_EVERYDAY

反例:MAX_COUNT / PRIZE_NUMBER

  • 抽象類命名使用 Abstract 或 Base 開頭;異常類命名使用 Exception 結尾;測試類命名以它要測試的類名開始,以 Test 結尾。

第八條:【強制】 POJO 類中布爾類型的變量都不要加 is 字首,否則部分架構解析會引起序列化錯誤。

反例:定義為基本資料類型

Boolen isDeleted;

的屬性,它的方法名稱也是

isDeleted()

,RPC 架構在反向解析的時候,“誤以為” 對應的屬性名稱是

deleted

,導緻屬性擷取不到抛出異常。

第十二條:【推薦】 如果子產品、類、方法使用了設計模式,應在命名時展現出具體模式

  • 說明: 将設計模式展現在名字中,有利于閱讀者快速了解架構設計理念。
正例:

public class OrderFactory;

public class LoginProxy;

public class ResourceObserver;

第十三條:【推薦】 接口類中的方法和屬性不要加任何修飾符号(public 也不要加),保持代碼的間接性,并加上有效的 Javadoc 注釋。盡量不要在接口裡定義變量,如果一定要定義變量,必須是與接口方法相關的,并且是整個應用的基礎變量。

接口方法簽名:

void commit();

接口基礎變量:

String COMPANY = "alibaba";

接口定義方法:

public abstract void commit();

  • 說明: 如果 JDK8 中接口允許有預設實作,那麼這個 default 方法,是對所有實作類都有價值的預設實作。

第十四條:接口和實作類的命名規則

  • 1):【強制】 對于 Service 和 DAO 類,基于 SOA 的理念,暴露出來的服務一定是接口,内部的實作類用 Impl 字尾與接口差別。
正例: CacheServiceImpl 實作 CacheServcie 接口
  • 2):【推薦】 如果是形容能力的接口名稱,取對應的形容詞為接口名(通常是 -able 的形式)。
正例: AbstractTranslator 實作 Translatable。

1.2 常量定義

第二條:【強制】 long 或者 Long 初始指派時,使用大寫的 L,不能是小寫的 l。小寫的 l 容易跟數字 1 混淆,造成誤解。

  • 說明:

    Long a = 2l;

    寫得是數字的 21 還是 Long 型的 2?

第三條:【推薦】 不要使用一個常量類維護所有變量,要按常量功能進行歸類,分開維護。

  • 說明: 大而全的變量類,非得使用查找功能才能定位到修改的常量,不利于了解和維護。
正例:緩存相關常量放在類 CacheConsts 下;系統配置相關常量放在 ConfigConsts 下。

1.3 代碼格式

public static void main(String[] args){
    // 注釋的雙斜線與注釋内容之間有且僅有一個空格
    // 縮進 4 個空格
    String say = "hello";
    // 運算符的左右必須有 1 個空格
    int flag = 0;
    // 關鍵字 if 與括号之間必須有 1 個空格,括号内的 f與左括号、
    // 0 與右括号之間不需要空格
    if (flag == 0) {
        System.out.println(say);
    }
    // 左大括号前加空格且不換行;左大括号後換行
    if (flag == 1) {
        System.out.println("world");
    // 右大括号前換行,右大括号後有 else,不用換行
    } else {
        System.out.println("ok");
    // 在右大括号後直接結束,則必須換行
    }
}
           

第八條:【強制】 方法參數在定義和傳入時,多個參數逗号後邊必須加空格。

正例:下例中實參的“one”,後邊必須要有一個空格。

method("one", "two", "three");

1.4 OOP 規約

第二條:【強制】 所有的複寫方法,必須加 @Override 注解。

  • 說明: getObject() 與 get0bject() 的問題。一個是字母 O,一個是數字 0,

    加 @Override 注解可以準确判斷是否覆寫成功。另外,如果在抽象類中對方法簽名進行修改,其實作類會馬上編譯報錯。

第七條:【強制】 所有相同類型的包裝類對象之間值得比較,全部使用 equals 方法

  • 說明: 對于

    Intergre var = ?

    在 -128~127 範圍内的指派, Integer 對象是在 IntegerCache.cache 中産生的,會複用已有的對象,這個區間内的 Integer 值可以直接使用 == 進行判斷,但是這個區間之外的所有資料,都會在堆上産生,并不會複用已有對象。這是一個大坑,推薦使用 equals 方法進行判斷。

第十二條:【強制】 POJO 類必須寫 toString 方法。在使用 IDE 中的工具 source>generate toString 時,如果繼承了另一個 POJO 類,注意在前面加一下 super.toString。

  • 說明: 在方法執行抛出異常時,可以直接調用 POJO 的 toString() 方法列印其屬性值,便于排查問題。

1.5 集合處理

第七條:【強制】 不要在 foreach 循環裡進行元素的 remove / add 操作。remove 元素請使用 Iterator 方式,如果并發操作,需要對 Iterator 對象加鎖。

// 正例
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
	String item = iterator.next();
	if (删除元素的條件) {
		iterator.remove();
	}
}
// 反例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    // 如果把 1 改為 2 再試一下看看是否相同
	if ("1".equals(item)) {
		list.remove(item);
	}
}
           

第十一條:【推薦】 高度注意 Map 類集合 K/V 能不能存儲 null 值得情況

1.6 并發處理

第三條:【強制】 線程資源必須通過線程池提供,不允許在應用中自行顯式建立線程。

  • 說明: 使用線程池的好處是減少在建立和銷毀線程上所消耗的時間以及系統資源,解決資源不足的問題。如果不适用線程池,有可能造成系統建立大量同類線程而導緻消耗完記憶體或者 “過渡切換” 的問題。

1.7 控制語句

第二條:【強制】 在 if / else / for / while / do 語句中,必須使用大括号。即使隻有一行代碼,也應該避免采用單行的編碼方式:

if (condition) statements;

第三條:【強制】 在高并發場景中,避免使用 “等于” 判斷作為終端或退出的條件

  • 說明: 如果并發控制沒有處理好,容易産生等值判斷被 “擊穿” 的情況,應使用大于或小于的區間判斷條件來代替。
反例:判斷剩餘獎品數量等于 0 時,終止發放獎品,但因為并發處理錯誤導緻獎品數量瞬間變成了負數,這樣的話,活動無法終止。

第四條:【推薦】 在表達異常的分支時,盡量少用 if-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 Java");
	return;
}
           

1.8 注釋規約

第一條:【強制】 類、類屬性、類方法的注釋必須使用 Javadoc 規範,使用

/**内容*/

格式,不得使用

//xxx

方式

第二條:【強制】 所有的抽象方法(包括接口中的方法)必須要用 Javadoc 注釋,除了傳回值、參數、異常說明外,還必須指出該方法做什麼事情,實作什麼功能。

  • 說明: 對子類的實作要求,或者調用注意事項,請一并說明。

第三條:【強制】 所有的類都必須添加建立者和建立日期。

1.9 其他

第三條:【強制】 背景輸送給網頁的變量必須加$!{var}——中間是感歎号

  • 說明: 如果 var=null 或者不存在,那麼 ${var} 會直接顯示在頁面上。

第四條:【強制】 注意 Math.random() 這個方法傳回的是 double 類型,取值的範圍 0≤x<1(能夠取到零值,注意除零異常),如果向擷取整數類型的随機數,不要将 x 放大 10 的若幹倍然後取整,直接使用 Random 對象的 nextInt 或者 nextLong 方法。

第六條:【推薦】 不要在視圖模闆中加入任何複雜的邏輯。

  • 說明: 根據 MVC 理論,視圖的職責是展示,不要搶模型和控制器的工作。

第4章:安全規約

“安全生産,責任重于泰山。” 這句話同樣适用于軟體生産,本章主要說明程式設計中需要注意的比較基礎的安全準則。

第一條:【強制】 隸屬于使用者個人的頁面或者功能必須進行權限控制校驗

  • 說明: 放置皆有做水準權限校驗就可以随意通路、修改、删除别人的資料,比如檢視他人的私信内容、修改他人的訂單。

第二條:【強制】 使用者敏感資料禁止直接展示,必須對展示資料進行脫敏。

  • 說明: 個人手機号碼會顯示為 158****9119,隐藏中間 4 位,防止個人隐私洩露。

第三條:【強制】 使用者輸入的 SQL 參數嚴格使用參數綁定或者 METADATA 字段值限定,防止 SQL 注入,禁止字元串拼接 SQL 通路資料庫。

第四條:【強制】 使用者請求傳入的任何參數必須做有效性驗證

  • 說明: 忽略參數校驗可能導緻如下情況。

    1)page size 過大導緻記憶體溢出

    2)惡意 order by 導緻資料庫慢查詢

    3)任意重定向

    4)SQL 注入

    5)反序列化注入

    6)正則輸入源串拒絕服務 ReDoS

    Java 代碼用正則來驗證用戶端的輸入,有些正則寫法驗證普通使用者輸入沒有問題,但是如果攻擊人員使用的是特殊構造的字元串來驗證,則有可能導緻死循環。

第五條:【強制】 禁止向 HTML 頁面輸出未經安全過濾或未正确轉義的使用者資料。

第六條:【強制】 表單、AJAX 送出必須執行 CSRF 安全過濾

第七條:【強制】 在使用平台資源,譬如短信、郵件、電話、下單、支付,必須實作正确的防重放限制,如數量限制、疲勞度控制、驗證碼校驗,避免被濫刷、資損。

  • 說明:如注冊時發送驗證碼到手機,如果沒有限制次數和頻率,那麼可以利用此功能騷擾到其它使用者,并造成短信平台資源浪費。

第5章:MySQL 資料庫

5.1 建表規約

第二條:【強制】 表名、字段名必須使用小寫字母或數字 , 禁止出現數字開頭,禁止兩個下劃線中間隻出現數字。資料庫字段名的修改代價很大,因為無法進行預釋出,是以字段名稱需要慎重考慮。

  • 說明: MySQL 在 Windows 下不區分大小寫,但在 Linux 下預設區分大小寫。是以,資料庫名、表明、字段名都不允許出現任何大寫字母,避免節外生枝。

正例: getter _ admin , task _ config , level 3_ name

反例: GetterAdmin , taskConfig , level 3 name

第四條:【強制】禁用保留字,如 desc 、 range 、 match 、 delayed 等,請參考 MySQL 官方保留字。

第五條: 【強制】主鍵索引名為 pk_ 字段名;唯一索引名為 uk _字段名 ; 普通索引名則為 idx _字段名。

  • 說明: pk_ 即 primary key;uk _ 即 unique key;idx _ 即 index 的簡稱。

第六條:【強制】小數類型為 decimal ,禁止使用 float 和 double 。

  • 說明:float 和 double 在存儲的時候,存在精度損失的問題,很可能在值的比較時,得到不正确的結果。如果存儲的資料範圍超過 decimal 的範圍,建議将資料拆成整數和小數分開存儲。

第八條:【強制】 varchar 是可變長字元串,不預先配置設定存儲空間,長度不要超過 5000,如果存儲長度大于此值,定義字段類型為 text ,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率。

第九條:【強制】表必備三字段: id , gmt _ create , gmt _ modified

  • 說明:其中 id 必為主鍵,類型為 unsigned bigint 、單表時自增、步長為 1。 gmt _ create ,gmt _ modified 的類型均為 date _ time 類型。

第十條: 【推薦】表的命名最好是加上“業務名稱_表的作用”。

正例: tiger _ task / tiger _ reader / mpp _ config

第十五條:【參考】合适的字元存儲長度,不但節約資料庫表空間、節約索引存儲,更重要的是提升檢索速度。

正例:如下表,其中無符号值可以避免誤存負數,且擴大了表示範圍。
對象 年齡區間 類型 表示範圍
150 歲之内 unsigned tinyint 無符号值:0 到 255
數百歲 unsigned smallint 無符号值:0 到 65535
恐龍化石 數千萬年 unsigned int 無符号值:0 到約 42.9 億
太陽 約 50 億年 unsigned bigint 無符号值:0 到約 10 的 19 次方

5.2 索引規約

第五條: 【推薦】如果有 order by 的場景,請注意利用索引的有序性。 order by 最後的字段是組合索引的一部分,并且放在索引組合順序的最後,避免出現 file _ sort 的情況,影響查詢性能。

正例: where a =? and b =? order by c; 索引: a _ b _ c

反例:索引中有範圍查找,那麼索引有序性無法利用,如: WHERE a >10 ORDER BY b; 索引 a _ b 無法排序。

第九條: 【推薦】建組合索引的時候,區分度最高的在最左邊。

正例:如果 where a =? and b =? , a 列的幾乎接近于唯一值,那麼隻需要單建 idx _ a 索引即可。

  • 說明: 存在非等号和等号混合判斷條件時,在建索引時,請把等号條件的列前置。如: where a >? and b = ? 那麼即使 a 的區分度更高,也必須把 b 放在索引的最前列。

5.3 SQL 語句

第一條:【強制】不要使用 count( 列名 ) 或 count( 常量 ) 來替代 count( * ) , count( * ) 是 SQL 92 定義的标準統計行數的文法,跟資料庫無關,跟 NULL 和非 NULL 無關。

  • 說明: count( * ) 會統計值為 NULL 的行,而 count( 列名 ) 不會統計此列為 NULL 值的行。

第六條: 【強制】不得使用外鍵與級聯,一切外鍵概念必須在應用層解決。

  • 說明: ( 概念解釋 ) 學生表中的 student _ id 是主鍵,那麼成績表中的 student _ id 則為外鍵。如果更新學生表中的 student _ id ,同時觸發成績表中的 student _ id 更新,則為級聯更新。外鍵與級聯更新适用于單機低并發,不适合分布式、高并發叢集 ; 級聯更新是強阻塞,存在資料庫更新風暴的風險 ; 外鍵影響資料庫的插入速度。

第八條: 【強制】資料訂正時,删除和修改記錄時,要先 select ,避免出現誤删除,确認無誤才能執行更新語句。

5.4 ORM 映射

整個規約對自己來說都挺有用的,因為正好涉及到這方面,幸好感覺臉不怎麼疼。

第一條:【強制】在表查詢中,一律不要使用 * 作為查詢的字段清單,需要哪些字段必須明确寫明。

  • 說明: 1 ) 增加查詢分析器解析成本。2 ) 增減字段容易與 resultMap 配置不一緻。

第二條:【強制】 POJO 類的 布爾 屬性不能加 is ,而資料庫字段必須加 is _,要求在 resultMap 中進行字段與屬性之間的映射。

  • 說明: 參見定義 POJO 類以及資料庫字段定義規定,在 中 增加映射,是必須的。在 MyBatis Generator 生成的代碼中,需要進行對應的修改。

第三條:【強制】不要用 resultClass 當傳回參數,即使所有類屬性名與資料庫字段一一對應,也需要定義 ; 反過來,每一個表也必然有一個與之對應。

  • 說明: 配置映射關系,使字段與 DO 類解耦,友善維護。

第七條:【強制】更新資料表記錄時,必須同時更新記錄對應的 gmt _ modified 字段值為目前時間。

第九條:【參考】@ Transactional 事務不要濫用。事務會影響資料庫的 QPS ,另外使用事務的地方需要考慮各方面的復原方案,包括緩存復原、搜尋引擎復原、消息補償、統計修正等。

總結

浏覽了一遍,還是學習到了很多東西吧,上面也僅僅隻是總結了對我自己比較收益,現階段我能吸收能實際感受得到的規約,如果想要 PDF 版的可以在這裡下載下傳:戳這裡

歡迎轉載,轉載請注明出處!

簡書ID:@我沒有三顆心髒

github:wmyskxz

歡迎關注公衆微信号:wmyskxz_javaweb

分享自己的Java Web學習之路以及各種Java學習資料

繼續閱讀