1. 程式人生 > >談談Google與微信H5牛牛的Java開發規範

談談Google與微信H5牛牛的Java開發規範

基本類 cli 多好 而且 spring final關鍵字 永久 zab rop

多年前,Google發布微信H5牛牛搭建平臺(h5.fanshubbs.com)來定義Java編碼時應遵循的微信牛牛Q_1687054422規範;今年年初阿裏則發布阿裏巴巴Java 開發手冊,並隨後叠代了多個版本,直至9月份又發布了微信H5牛牛。這兩大互聯網巨頭的初衷,都是希望能夠統一標準,使業界編碼達到一致性,提升溝通和研發效率,這對於我們碼農無疑是很贊的一筆福利呀。筆者將兩份規範都通讀了一遍,其中列舉的不少細則跟平時的編碼習慣基本是符合的,不過還是有不少新奇的收獲,忍不住記錄在此,供日後念念不忘~
Java開發規範總覽
無規矩不成方圓,編碼規範就如同微信H5牛牛搭建平臺(h5.fanshubbs.com),有了Http、TCP等各種協議,計算機之間才能有效地通信,同樣的,有了一致的微信H5牛牛搭建平臺(h5.fanshubbs.com)編碼規範,程序員之間才能有效地合作。道理大家都懂,可現實中的我們,經常一邊吐槽別人的代碼,一邊寫著被吐槽的代碼,究其根本,就是缺乏遵從編碼規範的意識!
一、Google Java Style
  Google的java開發規範主要分為6大部分:源文件基本規範、源文件結構、代碼格式、命名、編程實踐和Javadoc,各部分概要如下:
1、源文件基本規範(source file basics):文件名、文件編碼、特殊字符的規範要求 2、源文件結構(source file structure):版權許可信息、package、import、類申明的規約 3、代碼格式(formatting):大括號、縮進、換行、列長限制、空格、括號、枚舉、數組、switch語句、註4、解、註釋、和修飾符等格式要求 5、命名(Naming):標識符、包名、類名、方法名、常量名、非常量成員名、參數名、局部變量的命名規範 6、編程實踐(Programming Practices):@override、異常捕獲、靜態成員、Finalizers等用法規約
二、阿裏巴巴Java開發手冊
  阿裏的Java開發手冊相對於前者更上一層樓,它除了基本的編程風格的微信H5牛牛規約外,還給出了日誌、單元測試、安全、MySQL、工程結構等代碼之外的規約,據說是阿裏近萬名開發同學集體智慧的結晶,相當了得,還是挺值得借鑒一下的。各部分概要如下:
1、編程規約:命名風格、常量、代碼格式、OOP、集合處理、並發、控制語句、註釋等 2、異常日誌:異常處理、日誌的命名、保留時間、輸出級別、記錄信息等 3、單元測試:AIR原則(Automatic,Independent,Repeatable)、單側的代碼目錄、目標,單側的寫法,即BCDE原則(Border,Correct,Design,Error) 4、安全規約:權限校驗、數據脫敏、參數有效校驗、CSRF安全過濾、防重放限制、風控策略等 5、MySQL數據庫:建表、索引、SQL語句、ORM映射等 6、工程結構:應用分層、二方庫依賴(坐標命名、接口約定、pom配置)、服務器端各項配置(TCP超時、句柄數、JVM參數等)
熟知的規範
  對於大家已經爛熟於心並已習慣遵守的一些微信H5牛牛編碼規範,比如類名、常量的命名、數組的定義、Long類型的字面等,就不在此一一列出了,只想就一些平時編碼中較容易個性化,並可能會存在爭議的規範進行一番探討。為了便於說明,用G表示規範出自於Google Java Style,A表示規範出自於阿裏巴巴Java開發手冊。
[A]IDE的text file encoding設置為UTF-8;IDE中文件的換行符使用Unix格式,不要使用Windows格式([G]文件編碼:UTF-8)
  看似簡單的一個編碼約定,在實際開發過程中卻經常出現不一致,由於我們是中文操作系統,系統編碼是GBK。當兩個協作的開發人員IDE,一個采用系統默認編碼,一個設置為UTF-8,那麽二人看對方寫的中文註釋就各自都是亂碼了,很尷尬。對於“換行符使用Unix格式”,這個在編寫shell和hive腳本時踩過好幾次坑,而且錯誤提示很隱晦,一時半會還真察覺不出來,只能說這個規範請務必遵守!
[A]代碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
  大多數程序員還是都會遵從英文的命名方式,但在實際工作中還真有遇到過拼音與英文混用的命名,比如創建報文的函數命名為createBaowen,看起來怪怪的,有點不倫不類。
[A]抽象類命名使用Abstract或Base開頭;異常類使用Exception結尾;測試類以它要測試的類的名稱開始,以Test結尾
  以微信H5牛牛源碼為例,其抽象類都是以Abstract開頭,異常類以Exception結尾,測試類則是以Tests結尾。
[A]POJO類中布爾類型的變量,都不要加is,否則部分框架解析會引起序列化錯誤。
  這個問題一說大家都知道,但實際卻是很容易被忽視!因為Boolean通常表達“是”或“否”的意思,可能一遇到布爾變量,大家會習慣性地將它與is關聯起來,“很自然”地就會以is開頭定義變量。但筆者想說的是,這其實反應了至少兩個問題:1、對JavaBean屬性命名規範不熟;2、對框架解析POJO的原理不熟,如RPC反向解析、spring MVC參數綁定、MyBatis處理映射等。
private boolean isActive;//lombok、Eclipse生成getter、setter的結果如下,框架會誤把變量解析成activepublic boolean isActive() {
return isActive;
}public void setActive(boolean isActive) {
this.isActive = isActive;
}
  在搞清這兩個問題前,還是建議老老實實按規範來吧。
包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,類名若有復數含義,則可使用復數形式。
  實際工作中看到過包名包含下劃線的,如org.sherlockyb.usermanage.dao,還是有必要統一一下。
[A]不允許任何魔法值(即未經定義的常量)直接出現在代碼中。?反例:String key = "Id#taobao
" + tradeId; ? cache.put(key, value);
  避免硬編碼問題是每個程序員都應該具備的基本素養,硬編碼所帶來的可讀性差、維護困難等問題,眾所周知。
[A,G]采用空格縮進,禁止使用tab字符。
  這是Google和微信H5牛牛一致的規約,只不過前者是一個tab對應2個空格,後者則是4個空格。之所以不提倡tab鍵,是因為不同的IDE對tab鍵的“翻譯”默認有所差異,容易因不同程序員的個性化而導致同一份代碼的格式混亂。
[A,G]單行字符數限制不超過120/100個字符,超出需要換行,換行時遵循如下規則: 1)[A,G]第二行相對於第一行縮進4個空格,從第三行開始,不再繼續縮進。 2)[A]運算符或方法調用的點符號與下文一起換行([G]若是非賦值運算符,則在該符號前斷開;若是賦值運算符或foreach中的分號,則在該符號後斷開)。 4)[A]方法調用時,多個參數,需要換行時,在逗號後進行([G]逗號與前面的內容留在同一行)。 5)在括號前不要換行。  對於單行字符限制,阿裏的是120,Google的是100。個人覺得120略長,特別是當用筆記本碼代碼時,對於超限的代碼行,經常要用橫向滾動條,不太友好,個人推薦100的限制。
沒有必要增加若幹空格來使某一行的字符與上一行對應位置的字符對齊。
  在變量較多時,這種對齊是一種累贅。雖說有IDE的自動格式化功能,但多人協作時,難保各自的格式化沒有差異,會因格式變化而造成不必要的代碼行改動,無疑會給你的代碼合並徒增困擾。
方法體內的執行語句組、變量的定義語句組、不同的業務邏輯之間或者不同的語義之間插入一個空行。相同業務邏輯和語義之間不需要插入空行。
  代碼分塊就如同文章分段,整潔的代碼具有更強的自解釋性。
外部正在調用或者二方庫依賴的接口,不允許修改方法簽名,避免對接口調用方產生影響。作為提供方,接口過時必須加@Deprecated註解,並清晰地說明采用的新接口或者新服務是什麽;作為調用方,有義務去考證過時方法的新實現是什麽。
br/>  對於單行字符限制,阿裏的是120,Google的是100。個人覺得120略長,特別是當用筆記本碼代碼時,對於超限的代碼行,經常要用橫向滾動條,不太友好,個人推薦100的限制。
沒有必要增加若幹空格來使某一行的字符與上一行對應位置的字符對齊。
  在變量較多時,這種對齊是一種累贅。雖說有IDE的自動格式化功能,但多人協作時,難保各自的格式化沒有差異,會因格式變化而造成不必要的代碼行改動,無疑會給你的代碼合並徒增困擾。
方法體內的執行語句組、變量的定義語句組、不同的業務邏輯之間或者不同的語義之間插入一個空行。相同業務邏輯和語義之間不需要插入空行。
  代碼分塊就如同文章分段,整潔的代碼具有更強的自解釋性。
外部正在調用或者二方庫依賴的接口,不允許修改方法簽名,避免對接口調用方產生影響。作為提供方,接口過時必須加@Deprecated註解,並清晰地說明采用的新接口或者新服務是什麽;作為調用方,有義務去考證過時方法的新實現是什麽。
所有的相同類型的包裝類對象之間值的比較,全部用equals方法比較。 說明:對於Integer var = ?在-128至127範圍內的賦值,Integer對象是在IntegerCache.cache產生,會復用已有對象,這個區間內的Integer值可以直接使用==進行判斷,但是這個區間之外的所有數據,都會在堆上產生,並不會復用已有對象,這是個大坑,推薦使用equals方法進行判斷。
  這裏補充幾點,除了Integer,其他包裝類型如微信H5牛牛、Byte等都有各自的cache。這裏只提到了等值比較,對於>,<等非等值比較,沒必要手動拆箱去比較,包裝類型之間直接可以比較大小,親測有效。例如:
Long a = new Long(1000L);Long b = new Long(222L);Long c = new Long(2000L);
Assert.isTrue(a > b && a < c); //斷言成功
[A]關於基本數據類型與包裝數據類型的使用標準如下: 1)所有的POJO類屬性必須使用包裝數據類型。 2)RPC方法的返回值和參數必須使用包裝數據類型。 3)所有的局部變量使用基本數據類型。 說明:POJO類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE問題,或者入口檢查,都由使用者來保證。
  基本類型作為入參和返回值有多種弊病,如不情願的默認值,NPE風險等,除了局部變量,其他慎用。
序列化類新增屬性時,請不要修改serialVersionUID字段,避免反序列化失敗;如果完全不兼容升級,避免反序列化混亂,那麽請修改serialVersionUID值。
  serialVersionUID是Java為每個序列化類產生的版本標識:版本相同,相互之間則可序列化和反序列化;版本不同,反序列化時會拋出InvalidClassException。因不同的jdk編譯很可能會生成不同的serialVersionUID默認值,通常需要顯式指定,如1L。
[A]final可以聲明類、成員變量、方法、以及本地變量,下列情況使用final關鍵字: 1)不允許被繼承的類,如:String類。 2)不允許修改引用的域對象,如:POJO類的域變量。 3)不允許被重寫的方法,如:POJO類的setter方法。 4)不允許運行過程中重新賦值的局部變量,如傳遞給匿名內部類的局部變量。
  final關鍵字有諸多好處,比如JVM和Java應用都會緩存final變量,以提高性能;final變量可在多線程環境下放心共享,無需額外的同步開銷;JVM會對final修飾的方法、變量及類進行優化等,詳情可見深入理解Java中的final關鍵字。
慎用Object的clone方法來拷貝對象。 說明:對象的clone方法默認是淺拷貝,特別是引用類型成員。若想實現深拷貝,需要重寫clone方法實現屬性對象的拷貝。
  Java中的賦值操作都是值傳遞,比如我們常用來“微信H5牛牛”DTO的工具,無論是spring的BeanUtils.copyProperties,還是Apache commons的BeanUtils.cloneBean,實際上也只是兩個DTO之間成員的引用復制,成員指向的對象還是同一個,用到此類工具的時候要有這個意識,不然容易踩坑。
[A]類成員與方法訪問控制從嚴: 1)如果不允許外部直接通過new來創建對象,那麽構造方法必須是private。 2)工具類不允許有public或default構造方法。 3)類非static成員變量並且與子類共享,必須是protected。 4)類非static成員變量並且僅在本類使用,必須是private。 5)類static成員變量如果僅在本類使用,必須是private。 6)若是static成員變量,必須考慮是否為final。 7)類成員方法只供類內部調用,必須是private。 8)類成員方法只對繼承類公開,那麽限制為protected。?說明:任何類、方法、參數、變量,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模塊解耦。
  最小權限原則(Principal of least privilege,POLP)是每個程序員應遵守的,可有效避免數據以及功能受到錯誤或惡意行為的破壞。
[A]ArrayList的subList結果不可強轉成ArrayList,否則會拋出ClassCastException異常。
  這裏補充一點,SubList並未實現Serializable接口,若RPC接口的List類型參數接受了SubList類型的實參,則在RPC調用時會報出序列化異常。比如我們常用的guava中的Lists.partition,切分後的子list實際都是SubList類型,在傳給RPC接口之前,需要用new ArrayList()包一層,否則會報序列化異常。
[A]在subList場景中,高度註意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均會產生ConcurrentModificationException異常。
  這個還是得從源碼的角度來解釋。SubList在構造時實際是直接持有了原list的引用,其add、remove等操作實際都是對原list的操作,我們不妨以add為例:
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification(); // 檢查this.modCount與原list的modCount是否一致
l.add(index+offset, element); // 原list新增了一個元素
this.modCount = l.modCount; // 將原list更新後的modCount同步到this.modCount
size++;
}
  可以看出,SubList生成之後,通過SubList進行add、remove等操作時,modCount會同步更新,所以沒問題;而如果此後還對原list進行add、remove等操作,SubList是感知不到modCount的變化的,會造成modCount不一致,從而報出ConcurrentModificationException異常。故通常來講,從原list取了SubList之後,是不建議再對原list做結構上的修改的。
[A]使用工具類Arrays.asList()把數組轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear方法會拋出UnsupportedOperationException異常。
  類似的,guava的Maps.toMap方法,返回的是一個ImmutableMap,是不可變的,不能對其調用add、remove等操作,使用時應該有這個意識!
在JDK7版本及版本以上,Comparator必須滿足:1)x,y比較結果和y,x比較結果相反;2)x>y,y>z,則x>z;3)x=y,則x,z比較結果和y,z比較結果相同。不然Arrays.sort,Collections.sort會報IllegalArgumentException異常。
  JDK從1.6升到1.7之後,默認排序算法由MergeSort變為TimSort,對於任意兩個比較元素x、y,其Comparator結果一定要是確定的,特別是對於x=y的情況,確定返回0,否則可能出現Comparison method violates its general contract!錯誤。
[A]線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。?說明:Executors返回的線程池對象的弊端如下: 1)FixedThreadPool和SingleThreadPool:允許的請求隊列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。 2)CachedThreadPool和ScheduledThreadLocal:允許的創建線程數為Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。
  現在一般很少會用Executors去創建線程池了,通常會使用spring的ThreadPoolExecutorFactoryBean或者guava的MoreExecutors.listeningDecorator對前者包裝一下,對於像線程數、隊列大小等都是通過配置來設定。
[A]高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
  一句話概括就是,能不鎖就不鎖,即便鎖,也盡量使鎖的粒度最小化。
[A]表達異常分支時,少用if-else方式,可使用衛語句代替。對於if()...else if()...else...方式,請勿超過3層。對於超過的,可使用衛語句、策略模式、狀態模式等來實現。
if(condition) {
...
return obj;
}// 接著寫else的業務邏輯代碼;
  冗長的if-else可讀性差,維護困難,推薦使用衛語句,邏輯清晰明了。
[A]代碼修改的同時,註釋也做同步修改,尤其是參數、返回值、異常、核心邏輯等的修改。
  這個在微信H5牛牛工程代碼中還真看到過不少,代碼與註釋牛頭不對馬嘴,盡量別留坑給後來者,應該算在程序猿的基本素養之內吧。
謹慎註釋掉代碼。在上方詳細說明,而不是簡單的註釋掉。如果無用,則刪除。?說明:代碼被註釋掉有兩種可能:1)後續會恢復此段代碼邏輯。2)永久不用。前者如果沒有備註信息,難以知曉註釋動機。後者建議直接刪掉(代碼倉庫保存了歷史代碼)。
  這個就更無力吐槽了,比上一條更常見,so,這條微信H5牛牛規範強烈推薦!

談談Google與微信H5牛牛的Java開發規範