無(wú)規(guī)矩不成方圓,編碼規(guī)范就如同協(xié)議,有了Http、TCP等各種協(xié)議,計(jì)算機(jī)之間才能有效地通信,同樣的,有了一致的編碼規(guī)范,程序員之間才能有效地合作。道理大家都懂,可現(xiàn)實(shí)中的我們,經(jīng)常一邊吐槽別人的代碼,一邊寫(xiě)著被吐槽的代碼,究其根本,就是缺乏遵從編碼規(guī)范的意識(shí)!多年前,Google發(fā)布Google Java Style來(lái)定義Java編碼時(shí)應(yīng)遵循的規(guī)范;今年年初阿里則發(fā)布阿里巴巴Java 開(kāi)發(fā)手冊(cè),并隨后迭代了多個(gè)版本,直至9月份又發(fā)布了pdf終極版。這兩大互聯(lián)網(wǎng)巨頭的初衷,都是希望能夠統(tǒng)一標(biāo)準(zhǔn),使業(yè)界編碼達(dá)到一致性,提升溝通和研發(fā)效率,這對(duì)于我們碼農(nóng)無(wú)疑是很贊的一筆福利呀。筆者將兩份規(guī)范都通讀了一遍,其中列舉的不少細(xì)則跟平時(shí)的編碼習(xí)慣基本是符合的,不過(guò)還是有不少新奇的收獲,忍不住記錄在此,供日后念念不忘~
Java開(kāi)發(fā)規(guī)范總覽
一、Google Java Style
Google的java開(kāi)發(fā)規(guī)范主要分為6大部分:源文件基本規(guī)范、源文件結(jié)構(gòu)、代碼格式、命名、編程實(shí)踐和Javadoc,各部分概要如下:
1、源文件基本規(guī)范(source file basics):文件名、文件編碼、特殊字符的規(guī)范要求
2、源文件結(jié)構(gòu)(source file structure):版權(quán)許可信息、package、import、類申明的規(guī)約
3、代碼格式(formatting):大括號(hào)、縮進(jìn)、換行、列長(zhǎng)限制、空格、括號(hào)、枚舉、數(shù)組、switch語(yǔ)句、注4、解、注釋、和修飾符等格式要求
5、命名(Naming):標(biāo)識(shí)符、包名、類名、方法名、常量名、非常量成員名、參數(shù)名、局部變量的命名規(guī)范
6、編程實(shí)踐(Programming Practices):@override、異常捕獲、靜態(tài)成員、Finalizers等用法規(guī)約
二、阿里巴巴Java開(kāi)發(fā)手冊(cè)
阿里的Java開(kāi)發(fā)手冊(cè)相對(duì)于前者更上一層樓,它除了基本的編程風(fēng)格的規(guī)約外,還給出了日志、單元測(cè)試、安全、MySQL、工程結(jié)構(gòu)等代碼之外的規(guī)約,據(jù)說(shuō)是阿里近萬(wàn)名開(kāi)發(fā)同學(xué)集體智慧的結(jié)晶,相當(dāng)了得,還是挺值得借鑒一下的。各部分概要如下:
1、編程規(guī)約:命名風(fēng)格、常量、代碼格式、OOP、集合處理、并發(fā)、控制語(yǔ)句、注釋等
2、異常日志:異常處理、日志的命名、保留時(shí)間、輸出級(jí)別、記錄信息等
3、單元測(cè)試:AIR原則(Automatic,Independent,Repeatable)、單側(cè)的代碼目錄、目標(biāo),單側(cè)的寫(xiě)法,即BCDE原則(Border,Correct,Design,Error)
4、安全規(guī)約:權(quán)限校驗(yàn)、數(shù)據(jù)脫敏、參數(shù)有效校驗(yàn)、CSRF安全過(guò)濾、防重放限制、風(fēng)控策略等
5、MySQL數(shù)據(jù)庫(kù):建表、索引、SQL語(yǔ)句、ORM映射等
6、工程結(jié)構(gòu):應(yīng)用分層、二方庫(kù)依賴(坐標(biāo)命名、接口約定、pom配置)、服務(wù)器端各項(xiàng)配置(TCP超時(shí)、句柄數(shù)、JVM參數(shù)等)
熟知的規(guī)范
對(duì)于大家已經(jīng)爛熟于心并已習(xí)慣遵守的一些編碼規(guī)范,比如類名、常量的命名、數(shù)組的定義、Long類型的字面等,就不在此一一列出了,只想就一些平時(shí)編碼中較容易個(gè)性化,并可能會(huì)存在爭(zhēng)議的規(guī)范進(jìn)行一番探討。為了便于說(shuō)明,用G表示規(guī)范出自于Google Java Style,A表示規(guī)范出自于阿里巴巴Java開(kāi)發(fā)手冊(cè)。
[A]IDE的
text file encoding設(shè)置為UTF-8;IDE中文件的換行符使用Unix格式,不要使用Windows格式([G]文件編碼:UTF-8)
看似簡(jiǎn)單的一個(gè)編碼約定,在實(shí)際開(kāi)發(fā)過(guò)程中卻經(jīng)常出現(xiàn)不一致,由于我們是中文操作系統(tǒng),系統(tǒng)編碼是GBK。當(dāng)兩個(gè)協(xié)作的開(kāi)發(fā)人員IDE,一個(gè)采用系統(tǒng)默認(rèn)編碼,一個(gè)設(shè)置為UTF-8,那么二人看對(duì)方寫(xiě)的中文注釋就各自都是亂碼了,很尷尬。對(duì)于“換行符使用Unix格式”,這個(gè)在編寫(xiě)shell和hive腳本時(shí)踩過(guò)好幾次坑,而且錯(cuò)誤提示很隱晦,一時(shí)半會(huì)還真察覺(jué)不出來(lái),只能說(shuō)這個(gè)規(guī)范請(qǐng)務(wù)必遵守!
[A]代碼中的命名嚴(yán)禁使用拼音與英文混合的方式,更不允許直接使用中文的方式。
大多數(shù)程序員還是都會(huì)遵從英文的命名方式,但在實(shí)際工作中還真有遇到過(guò)拼音與英文混用的命名,比如創(chuàng)建報(bào)文的函數(shù)命名為createBaowen,看起來(lái)怪怪的,有點(diǎn)不倫不類。
[A]抽象類命名使用Abstract或Base開(kāi)頭;異常類使用Exception結(jié)尾;測(cè)試類以它要測(cè)試的類的名稱開(kāi)始,以Test結(jié)尾
以spring源碼為例,其抽象類都是以Abstract開(kāi)頭,異常類以Exception結(jié)尾,測(cè)試類則是以Tests結(jié)尾。
[A]POJO類中布爾類型的變量,都不要加is,否則部分框架解析會(huì)引起序列化錯(cuò)誤。
這個(gè)問(wèn)題一說(shuō)大家都知道,但實(shí)際卻是很容易被忽視!因?yàn)锽oolean通常表達(dá)“是”或“否”的意思,可能一遇到布爾變量,大家會(huì)習(xí)慣性地將它與is關(guān)聯(lián)起來(lái),“很自然”地就會(huì)以is開(kāi)頭定義變量。但筆者想說(shuō)的是,這其實(shí)反應(yīng)了至少兩個(gè)問(wèn)題:1、對(duì)JavaBean屬性命名規(guī)范不熟;2、對(duì)框架解析POJO的原理不熟,如RPC反向解析、spring MVC參數(shù)綁定、MyBatis處理映射等。
private boolean isActive;
//lombok、Eclipse生成getter、setter的結(jié)果如下,框架會(huì)誤把變量解析成active
public boolean isActive() {
return isActive;
}
public void setActive(boolean isActive) {
this.isActive = isActive;
}
在搞清這兩個(gè)問(wèn)題前,還是建議老老實(shí)實(shí)按規(guī)范來(lái)吧。
包名統(tǒng)一使用小寫(xiě),點(diǎn)分隔符之間有且僅有一個(gè)自然語(yǔ)義的英語(yǔ)單詞。包名統(tǒng)一使用單數(shù)形式,類名若有復(fù)數(shù)含義,則可使用復(fù)數(shù)形式。
實(shí)際工作中看到過(guò)包名包含下劃線的,如org.sherlockyb.user_manage.dao,還是有必要統(tǒng)一一下。
[A]不允許任何魔法值(即未經(jīng)定義的常量)直接出現(xiàn)在代碼中。
反例:String key = "Id#taobao_" + tradeId;
cache.put(key, value);
避免硬編碼問(wèn)題是每個(gè)程序員都應(yīng)該具備的基本素養(yǎng),硬編碼所帶來(lái)的可讀性差、維護(hù)困難等問(wèn)題,眾所周知。
[A,G]采用空格縮進(jìn),禁止使用tab字符。
這是Google和ali一致的規(guī)約,只不過(guò)前者是一個(gè)tab對(duì)應(yīng)2個(gè)空格,后者則是4個(gè)空格。之所以不提倡tab鍵,是因?yàn)椴煌腎DE對(duì)tab鍵的“翻譯”默認(rèn)有所差異,容易因不同程序員的個(gè)性化而導(dǎo)致同一份代碼的格式混亂。
[A,G]單行字符數(shù)限制不超過(guò)120/100個(gè)字符,超出需要換行,換行時(shí)遵循如下規(guī)則:
1)[A,G]第二行相對(duì)于第一行縮進(jìn)4個(gè)空格,從第三行開(kāi)始,不再繼續(xù)縮進(jìn)。
2)[A]運(yùn)算符或方法調(diào)用的點(diǎn)符號(hào)與下文一起換行([G]若是非賦值運(yùn)算符,則在該符號(hào)前斷開(kāi);若是賦值運(yùn)算符或foreach中的分號(hào),則在該符號(hào)后斷開(kāi))。
4)[A]方法調(diào)用時(shí),多個(gè)參數(shù),需要換行時(shí),在逗號(hào)后進(jìn)行([G]逗號(hào)與前面的內(nèi)容留在同一行)。
5)在括號(hào)前不要換行。
對(duì)于單行字符限制,阿里的是120,Google的是100。個(gè)人覺(jué)得120略長(zhǎng),特別是當(dāng)用筆記本碼代碼時(shí),對(duì)于超限的代碼行,經(jīng)常要用橫向滾動(dòng)條,不太友好,個(gè)人推薦100的限制。
沒(méi)有必要增加若干空格來(lái)使某一行的字符與上一行對(duì)應(yīng)位置的字符對(duì)齊。
在變量較多時(shí),這種對(duì)齊是一種累贅。雖說(shuō)有IDE的自動(dòng)格式化功能,但多人協(xié)作時(shí),難保各自的格式化沒(méi)有差異,會(huì)因格式變化而造成不必要的代碼行改動(dòng),無(wú)疑會(huì)給你的代碼合并徒增困擾。
方法體內(nèi)的執(zhí)行語(yǔ)句組、變量的定義語(yǔ)句組、不同的業(yè)務(wù)邏輯之間或者不同的語(yǔ)義之間插入一個(gè)空行。相同業(yè)務(wù)邏輯和語(yǔ)義之間不需要插入空行。
代碼分塊就如同文章分段,整潔的代碼具有更強(qiáng)的自解釋性。
外部正在調(diào)用或者二方庫(kù)依賴的接口,不允許修改方法簽名,避免對(duì)接口調(diào)用方產(chǎn)生影響。作為提供方,接口過(guò)時(shí)必須加@Deprecated注解,并清晰地說(shuō)明采用的新接口或者新服務(wù)是什么;作為調(diào)用方,有義務(wù)去考證過(guò)時(shí)方法的新實(shí)現(xiàn)是什么。
接口契約,是使用方和調(diào)用方良好協(xié)作的有效保障,請(qǐng)務(wù)必遵守。
所有的相同類型的包裝類對(duì)象之間值的比較,全部用equals方法比較。
說(shuō)明:對(duì)于Integer var = ?在-128至127范圍內(nèi)的賦值,Integer對(duì)象是在IntegerCache.cache產(chǎn)生,會(huì)復(fù)用已有對(duì)象,這個(gè)區(qū)間內(nèi)的Integer值可以直接使用==進(jìn)行判斷,但是這個(gè)區(qū)間之外的所有數(shù)據(jù),都會(huì)在堆上產(chǎn)生,并不會(huì)復(fù)用已有對(duì)象,這是個(gè)大坑,推薦使用equals方法進(jìn)行判斷。
這里補(bǔ)充幾點(diǎn),除了Integer,其他包裝類型如Long、Byte等都有各自的cache。這里只提到了等值比較,對(duì)于>,<等非等值比較,沒(méi)必要手動(dòng)拆箱去比較,包裝類型之間直接可以比較大小,親測(cè)有效。例如:
Long a = new Long(1000L);
Long b = new Long(222L);
Long c = new Long(2000L);
Assert.isTrue(a > b && a < c); //斷言成功
[A]關(guān)于基本數(shù)據(jù)類型與包裝數(shù)據(jù)類型的使用標(biāo)準(zhǔn)如下:
1)所有的POJO類屬性必須使用包裝數(shù)據(jù)類型。
2)RPC方法的返回值和參數(shù)必須使用包裝數(shù)據(jù)類型。
3)所有的局部變量使用基本數(shù)據(jù)類型。
說(shuō)明:POJO類屬性沒(méi)有初值是提醒使用者在需要使用時(shí),必須自己顯式地進(jìn)行賦值,任何NPE問(wèn)題,或者入口檢查,都由使用者來(lái)保證。
基本類型作為入?yún)⒑头祷刂涤卸喾N弊病,如不情愿的默認(rèn)值,NPE風(fēng)險(xiǎn)等,除了局部變量,其他慎用。
序列化類新增屬性時(shí),請(qǐng)不要修改serialVersionUID字段,避免反序列化失??;如果完全不兼容升級(jí),避免反序列化混亂,那么請(qǐng)修改serialVersionUID值。
serialVersionUID是Java為每個(gè)序列化類產(chǎn)生的版本標(biāo)識(shí):版本相同,相互之間則可序列化和反序列化;版本不同,反序列化時(shí)會(huì)拋出InvalidClassException。因不同的jdk編譯很可能會(huì)生成不同的serialVersionUID默認(rèn)值,通常需要顯式指定,如1L。
[A]final可以聲明類、成員變量、方法、以及本地變量,下列情況使用final關(guān)鍵字:
1)不允許被繼承的類,如:String類。
2)不允許修改引用的域?qū)ο螅纾篜OJO類的域變量。
3)不允許被重寫(xiě)的方法,如:POJO類的setter方法。
4)不允許運(yùn)行過(guò)程中重新賦值的局部變量,如傳遞給匿名內(nèi)部類的局部變量。
final關(guān)鍵字有諸多好處,比如JVM和Java應(yīng)用都會(huì)緩存final變量,以提高性能;final變量可在多線程環(huán)境下放心共享,無(wú)需額外的同步開(kāi)銷;JVM會(huì)對(duì)final修飾的方法、變量及類進(jìn)行優(yōu)化等,詳情可見(jiàn)深入理解Java中的final關(guān)鍵字。
慎用Object的clone方法來(lái)拷貝對(duì)象。
說(shuō)明:對(duì)象的clone方法默認(rèn)是淺拷貝,特別是引用類型成員。若想實(shí)現(xiàn)深拷貝,需要重寫(xiě)clone方法實(shí)現(xiàn)屬性對(duì)象的拷貝。
Java中的賦值操作都是值傳遞,比如我們常用來(lái)“復(fù)制”DTO的工具,無(wú)論是spring的BeanUtils.copyProperties,還是Apache commons的BeanUtils.cloneBean,實(shí)際上也只是兩個(gè)DTO之間成員的引用復(fù)制,成員指向的對(duì)象還是同一個(gè),用到此類工具的時(shí)候要有這個(gè)意識(shí),不然容易踩坑。
[A]類成員與方法訪問(wèn)控制從嚴(yán):
1)如果不允許外部直接通過(guò)new來(lái)創(chuàng)建對(duì)象,那么構(gòu)造方法必須是private。
2)工具類不允許有public或default構(gòu)造方法。
3)類非static成員變量并且與子類共享,必須是protected。
4)類非static成員變量并且僅在本類使用,必須是private。
5)類static成員變量如果僅在本類使用,必須是private。
6)若是static成員變量,必須考慮是否為final。
7)類成員方法只供類內(nèi)部調(diào)用,必須是private。
8)類成員方法只對(duì)繼承類公開(kāi),那么限制為protected。
說(shuō)明:任何類、方法、參數(shù)、變量,嚴(yán)控訪問(wèn)范圍。過(guò)于寬泛的訪問(wèn)范圍,不利于模塊解耦。
最小權(quán)限原則(Principal of least privilege,POLP)是每個(gè)程序員應(yīng)遵守的,可有效避免數(shù)據(jù)以及功能受到錯(cuò)誤或惡意行為的破壞。
[A]ArrayList的subList結(jié)果不可強(qiáng)轉(zhuǎn)成ArrayList,否則會(huì)拋出ClassCastException異常。
這里補(bǔ)充一點(diǎn),SubList并未實(shí)現(xiàn)Serializable接口,若RPC接口的List類型參數(shù)接受了SubList類型的實(shí)參,則在RPC調(diào)用時(shí)會(huì)報(bào)出序列化異常。比如我們常用的guava中的Lists.partition,切分后的子list實(shí)際都是SubList類型,在傳給RPC接口之前,需要用new ArrayList()包一層,否則會(huì)報(bào)序列化異常。
[A]在subList場(chǎng)景中,高度注意對(duì)原集合元素個(gè)數(shù)的修改,會(huì)導(dǎo)致子列表的遍歷、增加、刪除均會(huì)產(chǎn)生ConcurrentModificationException異常。
這個(gè)還是得從源碼的角度來(lái)解釋。SubList在構(gòu)造時(shí)實(shí)際是直接持有了原list的引用,其add、remove等操作實(shí)際都是對(duì)原list的操作,我們不妨以add為例:
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification(); // 檢查this.modCount與原list的modCount是否一致
l.add(index+offset, element); // 原list新增了一個(gè)元素
this.modCount = l.modCount; // 將原list更新后的modCount同步到this.modCount
size++;
}
可以看出,SubList生成之后,通過(guò)SubList進(jìn)行add、remove等操作時(shí),modCount會(huì)同步更新,所以沒(méi)問(wèn)題;而如果此后還對(duì)原list進(jìn)行add、remove等操作,SubList是感知不到modCount的變化的,會(huì)造成modCount不一致,從而報(bào)出ConcurrentModificationException異常。故通常來(lái)講,從原list取了SubList之后,是不建議再對(duì)原list做結(jié)構(gòu)上的修改的。
[A]使用工具類Arrays.asList()把數(shù)組轉(zhuǎn)換成集合時(shí),不能使用其修改集合相關(guān)的方法,它的add/remove/clear方法會(huì)拋出UnsupportedOperationException異常。
類似的,guava的Maps.toMap方法,返回的是一個(gè)ImmutableMap,是不可變的,不能對(duì)其調(diào)用add、remove等操作,使用時(shí)應(yīng)該有這個(gè)意識(shí)!
在JDK7版本及版本以上,Comparator必須滿足:1)x,y比較結(jié)果和y,x比較結(jié)果相反;2)x>y,y>z,則x>z;3)x=y,則x,z比較結(jié)果和y,z比較結(jié)果相同。不然Arrays.sort,Collections.sort會(huì)報(bào)IllegalArgumentException異常。
JDK從1.6升到1.7之后,默認(rèn)排序算法由MergeSort變?yōu)?a target="_blank">TimSort,對(duì)于任意兩個(gè)比較元素x、y,其Comparator結(jié)果一定要是確定的,特別是對(duì)于x=y的情況,確定返回0,否則可能出現(xiàn)Comparison method violates its general contract!錯(cuò)誤。
[A]線程池不允許使用Executors去創(chuàng)建,而是通過(guò)ThreadPoolExecutor的方式,這樣的處理方式讓寫(xiě)的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
說(shuō)明:Executors返回的線程池對(duì)象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允許的請(qǐng)求隊(duì)列長(zhǎng)度為Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM。
2)CachedThreadPool和ScheduledThreadLocal:允許的創(chuàng)建線程數(shù)為Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致OOM。
現(xiàn)在一般很少會(huì)用Executors去創(chuàng)建線程池了,通常會(huì)使用spring的ThreadPoolExecutorFactoryBean或者guava的MoreExecutors.listeningDecorator對(duì)前者包裝一下,對(duì)于像線程數(shù)、隊(duì)列大小等都是通過(guò)配置來(lái)設(shè)定。
[A]高并發(fā)時(shí),同步調(diào)用應(yīng)該去考量鎖的性能損耗。能用無(wú)鎖數(shù)據(jù)結(jié)構(gòu),就不要用鎖;能鎖區(qū)塊,就不要鎖整個(gè)方法體;能用對(duì)象鎖,就不要用類鎖。
一句話概括就是,能不鎖就不鎖,即便鎖,也盡量使鎖的粒度最小化。
[A]表達(dá)異常分支時(shí),少用if-else方式,可使用衛(wèi)語(yǔ)句代替。對(duì)于
if()...else if()...else...方式,請(qǐng)勿超過(guò)3層。對(duì)于超過(guò)的,可使用衛(wèi)語(yǔ)句、策略模式、狀態(tài)模式等來(lái)實(shí)現(xiàn)。
if(condition) {
...
return obj;
}
// 接著寫(xiě)else的業(yè)務(wù)邏輯代碼;
冗長(zhǎng)的if-else可讀性差,維護(hù)困難,推薦使用衛(wèi)語(yǔ)句,邏輯清晰明了。
[A]代碼修改的同時(shí),注釋也做同步修改,尤其是參數(shù)、返回值、異常、核心邏輯等的修改。
這個(gè)在實(shí)際工程代碼中還真看到過(guò)不少,代碼與注釋牛頭不對(duì)馬嘴,盡量別留坑給后來(lái)者,應(yīng)該算在程序猿的基本素養(yǎng)之內(nèi)吧。
謹(jǐn)慎注釋掉代碼。在上方詳細(xì)說(shuō)明,而不是簡(jiǎn)單的注釋掉。如果無(wú)用,則刪除。
說(shuō)明:代碼被注釋掉有兩種可能:1)后續(xù)會(huì)恢復(fù)此段代碼邏輯。2)永久不用。前者如果沒(méi)有備注信息,難以知曉注釋動(dòng)機(jī)。后者建議直接刪掉(代碼倉(cāng)庫(kù)保存了歷史代碼)。
這個(gè)就更無(wú)力吐槽了,比上一條更常見(jiàn),so,這條規(guī)范強(qiáng)烈推薦!
1)對(duì)于注釋的要求:第一、能準(zhǔn)確反映設(shè)計(jì)思想和代碼邏輯;第二、能描述業(yè)務(wù)含義,使別人能迅速了解到代碼背后的信息;第三、好的命名、代碼結(jié)構(gòu)是自解釋性的,注釋力求精簡(jiǎn)準(zhǔn)確、表達(dá)到位。避免過(guò)多過(guò)濫的注釋。
2)finally塊必須對(duì)資源對(duì)象、流對(duì)象進(jìn)行關(guān)閉,有異常也要做try-catch。若是JDK7及以上,可使用try-with-resources。不能再finally塊中使用return,finally塊中的return返回后方法結(jié)束執(zhí)行,不會(huì)再執(zhí)行try塊中的return語(yǔ)句。
3)防止NPE,是程序員的基本素養(yǎng),注意NPE產(chǎn)生的場(chǎng)景:
1.返回類型為基本數(shù)據(jù)類型,return包裝數(shù)據(jù)類型的對(duì)象時(shí),自動(dòng)拆箱有可能產(chǎn)生NPE
2.數(shù)據(jù)庫(kù)的查詢結(jié)果可能為null。
3.遠(yuǎn)程調(diào)用返回對(duì)象時(shí),一律要求進(jìn)行空指針判斷,防止NPE。
4.對(duì)于Session中獲取的數(shù)據(jù),建議NPE檢查,避免空指針。
5.級(jí)聯(lián)調(diào)用obj.getA().getB().getC();一連串調(diào)用,易產(chǎn)生NPE。正例:使用JDK8的Optional類來(lái)防止NPE問(wèn)題。
4)在代碼中使用“拋異?!边€是“返回錯(cuò)誤碼”,對(duì)于公司外的http/api開(kāi)放接口必須使用“錯(cuò)誤碼”;而應(yīng)用內(nèi)部推薦異常拋出;跨應(yīng)用間RPC調(diào)用優(yōu)先考慮使用Result方式,封裝isSuccess()方法、“錯(cuò)誤碼”、“錯(cuò)誤簡(jiǎn)短信息”。
5)避免出現(xiàn)重復(fù)的代碼(Don't Repeat Yourself),即DRY原則。
以上幾條,皆是毫無(wú)爭(zhēng)議的基本規(guī)范,且行且遵守。
1)日志文件推薦至少保存15天,因?yàn)橛行┊惓>邆湟浴爸堋睘轭l次發(fā)生的特點(diǎn)。
2)對(duì)trace/debug/info級(jí)別的日志輸出,必須使用條件輸出形式或者使用占位符的方式。以避免不必要的字符串拼接,浪費(fèi)系統(tǒng)資源。
3)避免重復(fù)打印日志,浪費(fèi)磁盤(pán)空間,對(duì)于特定包的日志,務(wù)必設(shè)置additivity=false。
4)異常信息應(yīng)該包括兩類信息:案發(fā)現(xiàn)場(chǎng)信息和異常堆棧信息。如果不處理,則通過(guò)關(guān)鍵字throws往上拋。
關(guān)于日志的幾條不錯(cuò)的規(guī)范。日志作為服務(wù)器行為的日常軌跡,對(duì)于統(tǒng)計(jì)分析、故障排錯(cuò)意義巨大,要慎重對(duì)待才是。
1)好的單元測(cè)試必須遵守AIR原則。
A:Automatic(自動(dòng)化)。全自動(dòng)執(zhí)行,非交互式的。使用assert驗(yàn)證,而非System.out。
I:Independent(獨(dú)立性)。單側(cè)用例之間不能產(chǎn)生依賴,互相獨(dú)立。
R:Repeatable(可重復(fù))。可重復(fù)執(zhí)行,不能受到外界環(huán)境的影響。對(duì)于外部依賴,通過(guò)spring等DI框架注入一個(gè)本地(內(nèi)存)實(shí)現(xiàn)或者M(jìn)ock實(shí)現(xiàn)。
2)單元測(cè)試的基本目標(biāo):語(yǔ)句覆蓋率達(dá)到70%;核心模塊的語(yǔ)句覆蓋率和分支覆蓋率都要達(dá)到100%。
3)編寫(xiě)單元測(cè)試代碼遵守BCDE原則:
B:Border,邊界值測(cè)試,包括循環(huán)邊界、特殊取值、特殊時(shí)間點(diǎn)、數(shù)據(jù)順序等。
C:Correct,正確的輸入,并得到預(yù)期的結(jié)果。
D:Design,與設(shè)計(jì)文檔相結(jié)合,來(lái)編寫(xiě)單元測(cè)試。
E:Error,強(qiáng)制錯(cuò)誤信息輸入(如:非法數(shù)據(jù)、異常流程、非業(yè)務(wù)允許輸入等),并得到預(yù)期結(jié)果。
關(guān)于單元測(cè)試的幾條不錯(cuò)的規(guī)范。單元測(cè)試是代碼質(zhì)量的有效保障!太多的想當(dāng)然、自以為是,往往會(huì)跳過(guò)單測(cè),最終自食其果。曾經(jīng)的筆者也犯過(guò)類似毛病,還好及時(shí)糾正。
新奇的收獲
這里將列出一些筆者覺(jué)得有新收獲的規(guī)范,有的是平時(shí)編碼過(guò)程中沒(méi)有嚴(yán)格遵守的,比如switch中default偶爾加偶爾不加;有的則是目前還不太清楚的規(guī)范。
[A]杜絕完全不規(guī)范的縮寫(xiě),避免望文不知義。
反例:AbstractClass的“縮寫(xiě)”命名成AbsClass;condition的“縮寫(xiě)”命名成condi,此類隨意縮寫(xiě)嚴(yán)重降低了代碼的可閱讀性。
說(shuō)來(lái)慚愧,這類不規(guī)范的縮寫(xiě),筆者之前還真干過(guò)幾次。有時(shí)候是覺(jué)著變量太長(zhǎng),導(dǎo)致明明邏輯很簡(jiǎn)單的一條語(yǔ)句,就超過(guò)了列限制,于是乎主觀地縮寫(xiě)命名,如mergedRegionReportDtos縮寫(xiě)為mRegReportDtos,accountIdToHourReportDtos縮寫(xiě)為accountIdToHrDtos,相當(dāng)混亂有木有!所以,如果對(duì)英文單詞的縮寫(xiě)拿不定的話,還是直接用原單詞吧,長(zhǎng)點(diǎn)就長(zhǎng)點(diǎn),可讀性很重要。
[A]如果模塊、接口、類、方法使用了設(shè)計(jì)模式,在命名時(shí)體現(xiàn)出具體模式,有利于閱讀者快速理解架構(gòu)設(shè)計(jì)理念。類示例:OrderFactory、LoginProxy、ResourceObserver。
沒(méi)啥好說(shuō)的,同樣是為了提升代碼的自解釋性。spring源碼中隨處可見(jiàn)這樣的命名風(fēng)格:AbstractAutowireCapableBeanFactory、Cglib2AopProxy、BeanDefinitionParserDelegate等
[A]接口類中的方法和屬性不要加任何修飾符號(hào)(public也不要加),保持代碼的簡(jiǎn)潔性,并加上有效的Javadoc注釋。盡量不要在接口里定義變量,如果一定要定義變量,肯定是與接口方法有關(guān),并且是整個(gè)應(yīng)用的基礎(chǔ)常量。
正例:接口方法簽名:void f();
接口基礎(chǔ)常量表示:String COMPANY = "alibaba";
反例:接口方法定義:public abstract void f();
說(shuō)明:JDK8中接口允許有默認(rèn)實(shí)現(xiàn),那么這個(gè)default方法,是對(duì)所有實(shí)現(xiàn)類都有價(jià)值的默認(rèn)實(shí)現(xiàn)。
目前所見(jiàn)過(guò)的組內(nèi)代碼,有太多的接口中方法都是加了public,也許是后來(lái)的編碼者看到前任留下的已有方法都加了,為了保持一致,于是乎也加了public。說(shuō)到底還是最初的良好規(guī)范沒(méi)有形成,導(dǎo)致給后來(lái)者以錯(cuò)誤的指引!簡(jiǎn)單才是美,把public 去掉吧。
[A]接口的命名規(guī)則:如果是形容能力的接口名稱,取對(duì)應(yīng)的形容詞做接口名(通常是-able的形式)
正例:AbstractTranslator實(shí)現(xiàn)Translatable
Log4j中的AppenderAttachable,JDK中的AutoCloseable,Appendable等。
[A]各層命名規(guī)約:
A)Service/DAO層方法命名前綴規(guī)約
1)獲取對(duì)象時(shí),單個(gè)用get/多個(gè)用list;2)獲取統(tǒng)計(jì)值用count
3)插入用save/insert;4)刪除用remove/delete;5)修改用update
關(guān)于資源的CRUD,這塊的方法命名相當(dāng)亂,太容易個(gè)性化了!至少目前組內(nèi)代碼,要啥有啥:query與get并存,查詢列表和計(jì)數(shù)的都是get,并未做區(qū)分;一會(huì)兒remove,一會(huì)兒delete;既有save也有insert。當(dāng)你Ctrl+O的時(shí)候,想找個(gè)count某元素的方法時(shí)賊費(fèi)勁,急需統(tǒng)一!
[A]不要使用一個(gè)常量類維護(hù)所有常量,按常量功能進(jìn)行歸類,分開(kāi)維護(hù)。
說(shuō)明:大而全的常量類,非得使用查找功能才能定位到修改的常量,不利于理解和維護(hù)。
正例:緩存相關(guān)常量放在類CacheConsts下,系統(tǒng)配置相關(guān)常量放在類ConfigConsts下。
[A]常量的復(fù)用層次有五層:跨應(yīng)用共享常量、應(yīng)用內(nèi)共享常量、子工程內(nèi)共享常量、包內(nèi)共享常量、類內(nèi)共享常量。
1)跨應(yīng)用共享常量:放置在二方庫(kù)中,通常是client.jar中的constant目錄下。
2)應(yīng)用內(nèi)共享常量:放置在一方庫(kù)中,通常是modules中的constant目錄下。
3)子工程內(nèi)共享常量:當(dāng)前子工程的constant目錄下。
4)包內(nèi)共享常量:當(dāng)前包下單獨(dú)的constant目錄下。
5)類內(nèi)共享常量:直接在類內(nèi)部private static final定義。
常量的維護(hù)也可運(yùn)用設(shè)計(jì)模式思想,單一職責(zé),分層,嚴(yán)格控制作用域,使常量更清晰,易于理解,便于維護(hù)。
[A]類內(nèi)方法定義順序依次是:共有方法或保護(hù)方法 > 私有方法 > getter/setter方法。但有個(gè)規(guī)則特例:[A,G]當(dāng)一個(gè)類有多個(gè)構(gòu)造方法,或者多個(gè)同名方法,這些方法應(yīng)該按順序放置在一起。即重載永不分離。
說(shuō)明:共有方法是類的調(diào)用者和維護(hù)者最關(guān)系的方法,首屏展示最好;保護(hù)方法雖然只是子類關(guān)心,也可能是“模板設(shè)計(jì)模式”下的核心方法;而私有方法外部一般不需要特別關(guān)心,是一個(gè)黑盒實(shí)現(xiàn);因?yàn)槌休d的信息價(jià)值較低,所有Service和DAO的getter/setter方法放在類的最后。
方法的排版要有秩序,這樣在我們Ctrl+O的時(shí)候才能更方便的查閱方法列表。阿里的約定是比較通用的規(guī)則,對(duì)此,Google的看法則不同,它認(rèn)為類的成員順序不存在唯一的通用法則,重要的是,每個(gè)類應(yīng)該以維護(hù)者所能解釋的排序邏輯去排序它的成員。常見(jiàn)的反例:新的方法總是習(xí)慣性地添加到類的結(jié)尾,排序毫無(wú)意義。
[A]對(duì)多個(gè)資源、數(shù)據(jù)庫(kù)表、對(duì)象同時(shí)加鎖時(shí),需要保持一致的加鎖順序,否則可能會(huì)造成死鎖。
說(shuō)明:線程一需要對(duì)表A、B、C依次全部加鎖后才可以進(jìn)行更新操作,那么線程二的加鎖順序也必須是A、B、C,否則可能出現(xiàn)死鎖。
從死鎖產(chǎn)生的條件出發(fā)來(lái)避免死鎖。比如我們根據(jù)一批ids批量更新數(shù)據(jù)庫(kù)記錄時(shí),預(yù)先對(duì)ids排序,也是一種能有效降低死鎖發(fā)生概率的措施。
[A]使用CountDownLatch進(jìn)行異步轉(zhuǎn)同步操作,每個(gè)線程退出前必須調(diào)用countDown方法,線程執(zhí)行代碼注意catch異常,確保countDown方法被執(zhí)行到,避免主線程無(wú)法執(zhí)行至await方法,直到超時(shí)才返回結(jié)果。
避免Random實(shí)例被多線程使用,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一seed導(dǎo)致的性能下降。
說(shuō)明:Random實(shí)例包括java.util.Random的實(shí)例或者M(jìn)ath.random的方式。
正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在JDK7之前,需要編碼保證每個(gè)線程持有一個(gè)實(shí)例。
volatile關(guān)鍵字解決多線程內(nèi)存不可見(jiàn)問(wèn)題。對(duì)于一寫(xiě)多讀,是可以解決變量同步問(wèn)題,但是如果多寫(xiě),同樣無(wú)法解決線程安全問(wèn)題。如果是count++操作,使用如下類實(shí)現(xiàn):
AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推薦使用LongAdder對(duì)象,比AtomicLong性能更好(減少樂(lè)觀鎖的重試次數(shù))。
volatile關(guān)鍵字只是保證了同一個(gè)變量在多線程中的可見(jiàn)性,更多的是用于修飾作為開(kāi)關(guān)狀態(tài)的變量。但是volatile只提供了內(nèi)存可見(jiàn)性,而沒(méi)有提供原子性!volatile變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中重讀該變量的值,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫線程將最近的值刷新到主內(nèi)存,對(duì)于像boolean flag = true等原子性賦值操作是沒(méi)問(wèn)題的,但volatile不能保證復(fù)合操作的原子性,如count++。
[A]除常用方法(如getXxx/isXxx)等外,不要在條件判斷中執(zhí)行其他復(fù)雜的語(yǔ)句,將復(fù)雜邏輯判斷的結(jié)果賦值給一個(gè)有意義的布爾變量名,以提高可讀性。
這個(gè)筆者之前確實(shí)有過(guò)這樣的壞習(xí)慣,為了省略一條賦值語(yǔ)句,將if中的條件搞得比較復(fù)雜,代碼冗長(zhǎng),可讀性也差,得不償失。
[A]參數(shù)校驗(yàn)與否:
需要校驗(yàn)的:1)對(duì)外提供的開(kāi)發(fā)接口,不管是RPC/API/HTTP接口;2)敏感權(quán)限入口;3)需要極高穩(wěn)定性和可用性的方法
不需校驗(yàn)的:1)極有可能被循環(huán)調(diào)用的方法。但在方法說(shuō)明里必須注明外部參數(shù)檢查要求。2)底層調(diào)用頻度較高的方法。如一般Service會(huì)做參數(shù)校驗(yàn),到了DAO層,參數(shù)校驗(yàn)可省略。3)被聲明為private只會(huì)被自己代碼所調(diào)用的方法,如果能確定傳入?yún)?shù)已做過(guò)檢查或者肯定不會(huì)有問(wèn)題,此時(shí)可不校驗(yàn)參數(shù)。
過(guò)多的參數(shù)校驗(yàn),不僅是冗余代碼,而且還影響性能,只在必要的時(shí)候做校驗(yàn)。
1)隸屬于用戶個(gè)人的頁(yè)面或功能必須進(jìn)行權(quán)限控制校驗(yàn)。說(shuō)明:防止沒(méi)有做水平權(quán)限校驗(yàn)就可隨意訪問(wèn)、修改、刪除別人的數(shù)據(jù)。
2)用戶請(qǐng)求傳入的任何參數(shù)必須做有效性校驗(yàn)。忽略參數(shù)校驗(yàn)可能導(dǎo)致:1)page size過(guò)大導(dǎo)致內(nèi)存溢出;2)惡意order by導(dǎo)致數(shù)據(jù)庫(kù)慢查詢;3)任意重定向;4)SQL注入;5)反序列化注入;6)正則輸入源串拒絕服務(wù)ReDos
3)表單、AJAX提交必須執(zhí)行CSRF(Cross-site request forgery)安全過(guò)濾
4)在使用平臺(tái)資源,譬如短信、郵件、電話、下單、支付,必須實(shí)現(xiàn)正確的防重放機(jī)制,如數(shù)量限制、疲勞度控制、驗(yàn)證碼校驗(yàn),避免被濫刷,資損。
5)發(fā)帖、評(píng)論、發(fā)送即時(shí)消息等用戶生成內(nèi)容的場(chǎng)景必須實(shí)現(xiàn)防刷、文本內(nèi)容違禁詞過(guò)濾等風(fēng)控策略。
基本的安全意識(shí)還是要有的,一旦踩了坑,后果不堪設(shè)想。
1)數(shù)據(jù)庫(kù)表達(dá)是與否概念的字段,必須使用is_xxx的方式命名,數(shù)據(jù)類型是unsigned tinyint(1表示是,0表示否)。
2)禁用保留字,如desc、range、match、delayed等,參考MySQL官方保留字。
3)主鍵索引名為pk_字段名;唯一索引名為uk_字段名;普通索引名為idx_字段名。
4)varchar是可變長(zhǎng)字符串,不預(yù)先分配存儲(chǔ)空間,長(zhǎng)度不要超過(guò)5000,如果大于此值,則選用text,獨(dú)立出來(lái)一張表,用主鍵來(lái)對(duì)應(yīng),避免影響其他字段索引效率。
5)字段允許適當(dāng)冗余,以提高查詢性能,但必須考慮數(shù)據(jù)一致性。冗余字段應(yīng)遵守:1.不是頻繁修改;2.不是varchar超長(zhǎng)字段,更不能是text字段。
6)單表行數(shù)超過(guò)500萬(wàn)行或者單表容量超過(guò)2GB,才推薦分庫(kù)分表。
7)頁(yè)面搜索嚴(yán)禁左模糊或者全模糊,如果需要請(qǐng)走搜索引擎來(lái)解決。
8)若有order by的場(chǎng)景,請(qǐng)注意利用索引的有序性。order by最后的字段是組合索引的一部分,并放在索引組合順序的最后,避免出現(xiàn)file_sort的情況,影響查詢性能。
正例:where a=? and b=? order by c; 索引:a_b_c
9)利用覆蓋索引來(lái)進(jìn)行查詢操作,避免回表。很形象的比喻:如果一本書(shū)需要知道第11章是什么標(biāo)題,會(huì)翻開(kāi)第11章對(duì)應(yīng)的那一頁(yè)嗎?目錄(索引列)瀏覽一下就好,這個(gè)目錄就是起到覆蓋索引的目的。覆蓋索引的explain結(jié)果中,extra列會(huì)出現(xiàn):using index。
10)利用延遲關(guān)聯(lián)或子查詢優(yōu)化超多分頁(yè)場(chǎng)景。說(shuō)明:MySQL并不是跳過(guò)offset行,而是取offset+N行,然后放棄前offset行,返回N行,那當(dāng)offset特別大的時(shí)候,效率就非常低下。
11)建組合索引的時(shí)候,區(qū)分度最高的在最左邊。舉極端例子:如果where a=? and b=?,a的列幾乎接近于唯一值,那么只需單建idx_a索引即可。
12)不要使用count(列名)或count(常量)來(lái)替代count(*),count(*)是SQL92定義的標(biāo)準(zhǔn)統(tǒng)計(jì)行數(shù)的語(yǔ)法,跟數(shù)據(jù)庫(kù)無(wú)關(guān),跟NULL和非NULL無(wú)關(guān)。count(列名)會(huì)忽略此列為NULL值的行。
13)不得使用外鍵與級(jí)聯(lián),一切外鍵概念必須在應(yīng)用層解決。外鍵與級(jí)聯(lián)更新適用于單機(jī)低并發(fā),不適合分布式、高并發(fā)集群:級(jí)聯(lián)更新時(shí)強(qiáng)阻塞,存在數(shù)據(jù)庫(kù)更新風(fēng)暴的風(fēng)險(xiǎn);外鍵影響數(shù)據(jù)庫(kù)的插入速度。
14)數(shù)據(jù)訂正時(shí),刪除和修改記錄時(shí),要先select,避免出現(xiàn)誤刪除,確認(rèn)無(wú)誤后才能執(zhí)行更新語(yǔ)句。
15)在表查詢中,一律不要使用*作為查詢的字段列表,需要哪些字段必須明確寫(xiě)明。
16)@Transactional事務(wù)不要濫用。事務(wù)會(huì)影響數(shù)據(jù)庫(kù)的QPS,另外使用事務(wù)的地方需要考慮各方面的回滾方案,包括緩存回滾、搜索引擎回滾、消息補(bǔ)償、統(tǒng)計(jì)修正等。
數(shù)據(jù)庫(kù)操作的一些基本常識(shí),數(shù)據(jù)庫(kù)性能變壞,多數(shù)情況是由于上層應(yīng)用的不合理使用導(dǎo)致的。
高并發(fā)服務(wù)器建議調(diào)小TCP協(xié)議的time_wait超時(shí)時(shí)間。
說(shuō)明:操作系統(tǒng)默認(rèn)240秒后,才會(huì)關(guān)閉處于time_wait狀態(tài)的連接,在高并發(fā)訪問(wèn)下,服務(wù)器端會(huì)因?yàn)樘幱趖ime_wait的連接數(shù)太多,可能無(wú)法建立新的連接,故需要在服務(wù)器上調(diào)小此閾值。對(duì)于Linux服務(wù)器,變更/etc/sysctl.conf中的net.ipv4.tcp_fin_timeout。
個(gè)人補(bǔ)充
這里補(bǔ)充一部分手冊(cè)之外的規(guī)范,一些是筆者在實(shí)際工作中遇到過(guò),實(shí)踐過(guò)的經(jīng)驗(yàn),一些是組內(nèi)大牛分享實(shí)踐的,若有不合理的地方還請(qǐng)大家指正。
1)客戶端socket超時(shí)配置應(yīng)區(qū)分連接超時(shí)和讀超時(shí)。用connect timeout控制連接建立的超時(shí)時(shí)間,用read timeout控制流讀取數(shù)據(jù)的超時(shí)時(shí)間。代碼示例:
socket.connect(new InetSocketAddress(host, port), 2000); //設(shè)置連接超時(shí)為2s。
socket.setSoTimeout(10*1000); //設(shè)置讀超時(shí)為10s。
2)對(duì)于QPS非常高的RPC接口,應(yīng)該將RPC客戶端socket的讀超時(shí)盡量設(shè)短,以便當(dāng)該接口不可用時(shí),能快速超時(shí)返回,使客戶端能及時(shí)處理,避免上層應(yīng)用因此環(huán)節(jié)等待時(shí)間過(guò)長(zhǎng)而將上層服務(wù)打垮。
例如,socket.setSoTimeout(1000),將讀超時(shí)設(shè)置為1s。
3)數(shù)據(jù)庫(kù)查詢時(shí),除了order by需要利用索引的有序性,對(duì)于group by操作,在數(shù)據(jù)量大時(shí),有無(wú)利用索引的性能差異特別大。
4)數(shù)據(jù)庫(kù)批量操作時(shí),要分批進(jìn)行,避免一次操作涉及記錄數(shù)過(guò)多,導(dǎo)致事務(wù)超時(shí)。
例如:根據(jù)ids批量更新數(shù)據(jù),先用Lists.partition分批拆分成多個(gè)子list,然后每個(gè)list走一次更新,使單個(gè)事務(wù)盡快結(jié)束,分批大小一般設(shè)置1000。5)字符串分割時(shí),用Apache Commons中的StringUtils.splitPreserveAllTokens(...)代替JDK中的str.split(..),避免JDK對(duì)末尾空串的過(guò)濾導(dǎo)致結(jié)果與預(yù)期不一致。
寫(xiě)在最后,筆者想用阿里巴巴Java開(kāi)發(fā)手冊(cè)的作者孤盡大神的采訪名言來(lái)結(jié)束此文:
別人都說(shuō)我們是搬磚的碼農(nóng),但我們知道自己是追求個(gè)性的藝術(shù)家。也許我們不會(huì)過(guò)多在意自己的外表和穿著,但在我們不羈的外表下,骨子里追求著代碼的美、系統(tǒng)的美,代碼規(guī)范其實(shí)就是一個(gè)對(duì)程序美的定義。
與原文同步更新。