從一份Java規(guī)范說起

最近在微信群里看到阿里的Java編碼規(guī)范,主要是Java后端的代碼編寫規(guī)范與約定,分為強(qiáng)制、參考、推薦3個(gè)不同等級(jí),涵蓋了日常開發(fā)的很多細(xì)節(jié)。

趟了無數(shù)的坑,才能寫出這份規(guī)范。

這篇博客也是主要寫一下自己對(duì)這份規(guī)范的理解與實(shí)踐。


自工作以來,雖年限不長(zhǎng),但是閱讀了不少代碼編寫規(guī)范,包含日常Java開發(fā)約定、框架使用注意事項(xiàng)、DB使用規(guī)范。逐漸習(xí)慣按照一些約定進(jìn)行編程,但任何團(tuán)隊(duì)的開發(fā)素質(zhì)都是參差不齊的,編程習(xí)慣也是有很大差異的。個(gè)人經(jīng)歷了eclipse與intellij idea,svn與git,windows與mac等切換,若工作年限長(zhǎng)一些,可能經(jīng)歷的會(huì)更多。廢話不多說,逐節(jié)談一下理解吧。

1、編程規(guī)約

1.1、命名規(guī)約

主要是POJO、變量、方法名、類名、包名、命名英文化的強(qiáng)制要求,方便閱讀維護(hù)。提到一個(gè)問題:bool類型的變量不能以is開頭。這個(gè)問題遇到過不止一次,開發(fā)的時(shí)候需要注意。還有2個(gè)命名習(xí)慣問題,
一是方法內(nèi)的局部變量命名。個(gè)人習(xí)慣是用ide推薦的命名(intellij idea),若非Java的對(duì)象類型,一般是對(duì)象類型的lowerCamelCase風(fēng)格,有時(shí)也會(huì)命名成實(shí)際的作用。
二是數(shù)字2、4,由于這2個(gè)數(shù)字英文同to,for,所以有時(shí)可以見到這種命名,最著名的就是Log4j。但是個(gè)人不太推薦,雖然任何規(guī)范里都沒禁止過。

1.2、常量定義

提到了禁止魔術(shù)值的使用,常量的定義要分層分類;
常量的分層分類,若有縱表或提供平臺(tái)級(jí)服務(wù),那么常量的分層分類便很有必要;
還有一種case, 就是基礎(chǔ)類型轉(zhuǎn)換時(shí),推薦使用括號(hào)包含起來,更易閱讀:

long secondOfHour = (long) day * 60 * 60;//不推薦
long secondOfHour = (long) (day * 60 * 60);//推薦

1.3、格式規(guī)約

包含大括號(hào)的使用、系統(tǒng)關(guān)鍵字之間留空格、禁止使用tab、換行原則。禁止tab或用4個(gè)空格替換,這一條去github上看一些個(gè)人源碼就知道,會(huì)導(dǎo)致格式混亂。

1.4、oop規(guī)約

這段較長(zhǎng),凸顯其重要之處。撿重要的說吧。
覆寫方法時(shí),需要加@Override關(guān)鍵字;
構(gòu)造方法與POJO對(duì)象的set/get方法內(nèi)禁止有邏輯,遇到過在set方法內(nèi)寫了一段邏輯,難以排查;
對(duì)象equals判等時(shí),確定有值的在前,避免NPE:

bool isInvalid = Enum.PhoneUser.getValue.equals(dto.getUserType());

所有POJO的數(shù)據(jù)類型必須使用包裝類型,并且不允許有默認(rèn)值;使用方負(fù)責(zé)判空。見過許多代碼,有對(duì)接口返回或者POJO的字段不判空就直接用的,也有對(duì)包裝類型未判空就intValue/equals的;
序列化類新增屬性時(shí),勿修改 serialVersionUID 字段,否則會(huì)導(dǎo)致反序列化失敗;在不兼容變時(shí)一定修改該值;
慎用 Object 的 clone 方法來拷貝對(duì)象,原因是這個(gè)方法是淺拷貝,如果要使用,最好覆寫該P(yáng)OJO的clone方法;

1.5、集合處理

equals與hashcode要修改需要成對(duì),不可單獨(dú)修改其中一個(gè);
Set、Map的key值對(duì)象均需要覆寫這2個(gè)方法,否則無效;
注意Arrays、ArrayList.sublist的使用;
提到了Map類集合k/v對(duì)null的容忍以及線程安全問題;
以及集合的有序性、穩(wěn)定性;
集合這一塊在實(shí)際使用時(shí)出現(xiàn)的問題是最多的,也不是一兩句可以說完的,這份規(guī)范對(duì)這一塊比較簡(jiǎn)略。
比如下面這段代碼,一般來說是不會(huì)有問題的,

        for (HybridQueryOrderDO hybridQueryOrderDO : list){
            System.out.println(hybridQueryOrderDO.getMobile());
        }

但是下面這種情況,就會(huì)有問題了:

public static void main(String[] args) {
        List<HybridQueryOrderDO> list = Lists.newArrayList();
        list.add(null);
        list.add(HybridQueryOrderDO.queryByMainOrderIds(Lists.newArrayList(1)));
        for (HybridQueryOrderDO hybridQueryOrderDO : list){
            System.out.println(hybridQueryOrderDO.getMobile());
        }
    }

現(xiàn)實(shí)情況出現(xiàn)過這種case:

        List<HybridQueryOrderDO> list = Lists.newArrayList();
        //other logic
        list.addAll(reserveOrderRemoteService.queryOrderByIdList(Lists.newArrayList(1,2,3)));

對(duì)于這種情況有三步:團(tuán)隊(duì)規(guī)范、codeview、工具檢測(cè)。

1.6、并發(fā)處理

并發(fā)處理是Java開發(fā)里的熱點(diǎn)問題,自Java5引入并發(fā)集合包之后一直在改進(jìn)這方面的使用,包括Java8的lambda表達(dá)式、stream流。這份規(guī)范里沒有重復(fù)這些,而是更多的從實(shí)際使用中來約定、限制。
并發(fā),我認(rèn)為要理解、注意這幾點(diǎn):

臨界資源、線程安全、性能

這份規(guī)范里提到了以下幾點(diǎn):
創(chuàng)建線程時(shí)指定名稱,方便異常時(shí)定位回溯;
禁止自行創(chuàng)建線程,而應(yīng)通過線程池,要注意線程池也可能會(huì)oom;
加鎖時(shí),若有多個(gè)條件,那么解鎖時(shí)也要按順序,否則會(huì)deadLock;
高并發(fā)時(shí),簡(jiǎn)化、減少鎖的使用,提高性能;
HashMap 在容量不夠進(jìn)行 resize 時(shí)由于高并發(fā)可能出現(xiàn)死鏈,導(dǎo)致 CPU 飆升。不論是Java8以下的樹結(jié)構(gòu),或者Java8的樹+紅黑樹結(jié)構(gòu),均可能出現(xiàn)這種情況;

并發(fā)處理和集合一樣,也不是靠一份規(guī)范就解決問題,更重要的是實(shí)踐、總結(jié)。而出現(xiàn)并發(fā)問題時(shí),排查起來相對(duì)集合問題困難很多,這就需要在寫代碼時(shí)很小心,并且多做測(cè)試、一起review,養(yǎng)成良好的編程習(xí)慣,在出問題時(shí)方能快速定位、排查、解決。

1.7、控制語(yǔ)句

這里更多的是一些推薦習(xí)慣問題。但是我在維護(hù)項(xiàng)目時(shí),也遇到了一些不良習(xí)慣。

if(true) //do something
if(false) return null;

這種寫法有時(shí)就會(huì)惹禍。我一直推薦在if后無論何時(shí)均要加上{}。
對(duì)于這里的推薦的,將復(fù)雜邏輯判斷的結(jié)果賦值給一個(gè)有意義的布爾變量名,以提高可讀性,這一點(diǎn)我認(rèn)為對(duì)于條件特別多,并且存在與或非關(guān)系時(shí)更方便閱讀:

bool isNewUser = //logic
bool isNotPdUser = //logic
bool isInitStatus = //logic
bool isCanRefund = (isNewUser || isNotPdUser) && isInitStatus;

對(duì)于if、else嵌套層級(jí)較深的,這里也給出了兩種方案:衛(wèi)語(yǔ)句/狀態(tài)模式。在實(shí)際開發(fā)時(shí),有時(shí)采用衛(wèi)語(yǔ)句反而更難維護(hù)。

if (refunded){

}else if (unRefund){
  if (verified){

  }else if (init){
    if (unpay){

    }else{

    }
  }
}

實(shí)際上對(duì)于業(yè)務(wù)層,對(duì)于復(fù)雜的業(yè)務(wù),出現(xiàn)這種嵌套條件語(yǔ)句不足為奇,若改成衛(wèi)語(yǔ)句,若有修改,想必也是很麻煩的,而遇到熟悉業(yè)務(wù)的人,很可能會(huì)出現(xiàn)漏條件的情況。
規(guī)范里還提到了方法中對(duì)參數(shù)的校驗(yàn)問題,以下幾種情況是要對(duì)參數(shù)做校驗(yàn)的:

1) 調(diào)用頻次低的方法。
2) 執(zhí)行時(shí)間開銷很大的方法,參數(shù)校驗(yàn)時(shí)間幾乎可以忽略不計(jì),但如果因?yàn)閰?shù)錯(cuò)誤導(dǎo)致
   中間執(zhí)行回退,或者錯(cuò)誤,那得不償失。
3) 需要極高穩(wěn)定性和可用性的方法。
4) 對(duì)外供的開放接口,不管是 RPC/API/HTTP 接口。 
5) 敏感權(quán)限入口。

對(duì)于下面的內(nèi)部實(shí)現(xiàn)process,參數(shù)校驗(yàn)應(yīng)該是接口A/B的各有一份主要入?yún)⒌男r?yàn),process有一份全面的參數(shù)校驗(yàn),絕對(duì)不要A、B各有一套完整的參數(shù)校驗(yàn),process內(nèi)已有,否則邏輯有調(diào)整,A/B都完蛋。


接口參數(shù)校驗(yàn).png

1.8、注釋規(guī)約

關(guān)于注釋,有2種論調(diào),一種是代碼自注釋,包括類、方法、局部變量,名如其實(shí),這樣閱讀起來無障礙,讀完即可知其意;還有一種是代碼需要詳盡的注釋,最好是包含邏輯、變更人、日期等。
從實(shí)際來看,若業(yè)務(wù)穩(wěn)定,采用任意注釋方式均可,但若業(yè)務(wù)快速發(fā)展,那么采取代碼自注釋+核心邏輯簡(jiǎn)要注釋更靠譜,并要求代碼提交時(shí)帶上業(yè)務(wù)變更信息,這樣方便查看變更。
有一點(diǎn)是規(guī)范里沒提到的,就是如果有中文注釋,ide的編碼要調(diào)成utf-8,否則暴露出去之后,別人就可能無法閱讀了。

1.9、其他

比較散,就不細(xì)說了。

這些就是關(guān)于java的開發(fā)規(guī)范,但是在實(shí)際中,還有一些場(chǎng)景并沒有提到,包括:

1)第三方工具包的使用

guava包、Google工具包、Apache工具包、lombok工具,遇到不會(huì)的,或者看到項(xiàng)目里有現(xiàn)成的工具,秉承拿來主義,會(huì)直接使用而不去深究背后的邏輯,這個(gè)時(shí)候很容易出意外。在用這些工具代碼前,最好是了解一下源碼或者實(shí)現(xiàn),這樣才能避免踩坑或挖坑。

2)spring注解與spring配置文件

不論使用哪一種都可以,但是對(duì)于一個(gè)類,無特殊情況,不要采用spring注解+xml配置文件,或set、get方法+lombok等混用的情況,采用其中一個(gè)即可。

3)重視ide側(cè)邊欄語(yǔ)法提醒

很多開發(fā)同學(xué),都可能沒注意到Intellij Idea代碼區(qū)有語(yǔ)法提醒,有不少語(yǔ)法、邏輯錯(cuò)誤都會(huì)被檢測(cè)到。推薦消滅黃色的提醒。當(dāng)然,也有誤提醒,但是我相信能看到這篇文章的同學(xué)都能判斷的出來。

2、異常日志

異常處理是Java開發(fā)中必須要做好的一件事,日志是在追查問題、case重現(xiàn)時(shí)必不可少的工具。

2.1、異常處理

對(duì)于異常而言,有以下約定:
1)Java類庫(kù)中繼承自RuntimeException的異常,無需顯式捕獲;
2)try catch代碼要盡量簡(jiǎn)短,不能大段catch。盡量不要在循環(huán)中try catch;
3)異常捕獲了必須要處理,最起碼要打印一條日志,否則捕獲這個(gè)異常無意義。不處理可以將異常逐層上拋;
4)finally要合理使用;
5)防止NPE是基本素養(yǎng);
實(shí)際開發(fā)時(shí),經(jīng)常有自定義異常的場(chǎng)景,如流程控制。這種情況是要捕獲或者嚴(yán)格處理對(duì)應(yīng)的異常類型,而不能直接catch RuntimeException,否則業(yè)務(wù)異常很可能就失效了。如果是catch了多層異常,那么此時(shí)就要注意先后順序,否則可能出現(xiàn)deadcode。
還有一種情況和異常有關(guān),當(dāng)返回類型是包裝類型,帶一些輔助字段時(shí),嚴(yán)禁將異常堆棧塞入msg字段。首先是業(yè)務(wù)方很可能對(duì)堆棧信息無法處理,其次異常堆棧的大量信息序列化與反序列化都很耗時(shí),若是高請(qǐng)求量接口,還會(huì)耗費(fèi)帶寬。

public class Response<T> implements Serializable {
    private static final long serialVersionUID = -1L;
    private boolean success = true;
    private int code = RemoteCode.SUCCESS.getVal();
    private String msg = "成功";
    private T result;
   ……
}

2.2、日志打印

日志是在排查線上問題時(shí)最重要的工具之一。一個(gè)具有良好格式的日志記錄,會(huì)很方便的排查線上問題。但是一個(gè)冗余、混亂的日志,也會(huì)造成嚴(yán)重后果。
1)日志要保存一定周期,分文件、分級(jí)輸出;
2)避免重復(fù)打印日志,配置文件內(nèi)additivity=false。這個(gè)配置的意思是,若配置文件的appender若有繼承關(guān)系時(shí),則同一份日志只會(huì)在子appender內(nèi)輸出,而不會(huì)同時(shí)輸出。減少了一定的日志輸出;
3)輸出異常日志時(shí),應(yīng)同時(shí)輸出現(xiàn)場(chǎng)和異常堆棧信息,否則很可能是無效日志;
開發(fā)時(shí),遇到幾個(gè)常見的問題。逐一說明。
1)出現(xiàn)了e.printTrace,這樣隨意的代碼打印不出來想要的結(jié)果;
2)注意公司日志框架可能存在的問題。比如下面這條日志輸出的源碼:

    /**
     * Error level message
     */
    void error(Object message, Throwable t);

    /**
     * Error level message
     */
    void error(Object message);

若習(xí)慣性的logger.error(e),那么最終將輸出的是java.lang.Exception,不要說現(xiàn)場(chǎng)了,連堆棧信息都拿不到;
3)若有條件,可以將異常與系統(tǒng)打點(diǎn)/熔斷/降級(jí)結(jié)合,進(jìn)行相應(yīng)的處理;
除此之外,很多時(shí)候去線上排查日志,需要多臺(tái)機(jī)器進(jìn)行并行查詢,單臺(tái)機(jī)器逐個(gè)進(jìn)行g(shù)rep的話,效率很低。后面會(huì)開一篇單獨(dú)講如何利用linux的一些命令并行訪問日志。

3、MySQL規(guī)約

數(shù)據(jù)庫(kù)是開發(fā)時(shí)經(jīng)常涉及的,其中也是有很多的隱藏知識(shí)點(diǎn)。

3.1、建表規(guī)約

建表作為常見操作,有以下幾點(diǎn)注意事項(xiàng):
1)表名、字段名在命名時(shí)要審慎,字段的類型也要審慎。例如字段名,字段改名,innoDB的做法是先生成一張臨時(shí)表,將字段改名,隨后將原表數(shù)據(jù)同步過來,然后將原表改名或者軟刪,最后將臨時(shí)表改成原表名,這樣就實(shí)現(xiàn)了字段改名。從步驟就可以看出,對(duì)于大表,同步數(shù)據(jù)是一個(gè)大過程,并且同步過程中還要避免數(shù)據(jù)同步問題。加字段也是類似的步驟。
以下是偽操作

create table tmp;
sync table data from origin to tmp;
rename table origin as origin_tmp;
rename table tmp as origin;
……(other check and sync operations);
delete origin_tmp;

2)對(duì)于超大的varchar字段,考慮單獨(dú)建一張表,相應(yīng)字段為text類型,同時(shí)進(jìn)行主鍵關(guān)聯(lián),避免對(duì)原表的索引效率影響;
3)任意表內(nèi)必須存在主鍵id,創(chuàng)建時(shí)間與更新時(shí)間。更新時(shí)間在進(jìn)行歸檔時(shí)特別有效。而新增時(shí)間對(duì)于追溯有一定作用。除非是枚舉表,否則這幾個(gè)字段都不應(yīng)該缺失;
除此之外,還有幾點(diǎn)在設(shè)計(jì)時(shí)可供參考:
1)對(duì)于一些核心大表,在設(shè)計(jì)時(shí),可適當(dāng)留幾個(gè)字段備用,可在業(yè)務(wù)未來擴(kuò)展時(shí)進(jìn)行改名。字段改名的成本是小于新增字段的。

3.2、索引規(guī)約

索引對(duì)于日常的DB使用效率是明顯的,索引的好壞差異非常明顯。但是這份規(guī)約對(duì)于索引部分寫的很克制,內(nèi)容很少,實(shí)際在使用時(shí)的注意事項(xiàng)遠(yuǎn)不止這里提到的幾點(diǎn)。
1)業(yè)務(wù)具有唯一性的數(shù)據(jù),必須加上唯一索引。應(yīng)用層無法保證不產(chǎn)生臟數(shù)據(jù);
2)多表join關(guān)聯(lián)查詢時(shí),需要注意關(guān)聯(lián)字段要索引,并且類型相同。這里有一個(gè)坑點(diǎn)時(shí),若類型不同,那么DB會(huì)進(jìn)行隱式類型轉(zhuǎn)換,這樣可能會(huì)導(dǎo)致索引失效。

select a.name from table_a a,table_b where a.id=b.out_id;

若a.id與b.out_id類型不一致,那么此時(shí)會(huì)有隱式類型轉(zhuǎn)換,并且可能會(huì)導(dǎo)致索引失效;
3)模糊匹配時(shí)只能左匹配。以下前兩種模糊匹配是無法走索引的,效率奇低,第三個(gè)是正確用法;

select * from table where name like %ssss%;(模糊匹配全文中含ssss的,錯(cuò)誤用法)
select * from table where name like %ssss;(模糊匹配全文以ssss結(jié)尾的,錯(cuò)誤用法)
select * from table where name like ssss%;(模糊匹配全文以ssss開頭的,錯(cuò)誤用法)

即使這樣,也不推薦過多使用like進(jìn)行模糊匹配操作;
4)利用覆蓋索引,避免回表查詢影響效率。
5)利用延遲關(guān)聯(lián)或者子查詢優(yōu)化超多翻頁(yè)問題。大翻頁(yè)在泛條件查詢時(shí)會(huì)出現(xiàn):

select * from table where add_date > '2010' limit 1000000,100 ;

由于MySQL InnoDB在進(jìn)行翻頁(yè)時(shí),會(huì)遍歷拿到前1000000條數(shù)據(jù),然后拋棄掉,再偏移100,拿到數(shù)據(jù)進(jìn)行返回,這樣在查詢時(shí)效率會(huì)非常低,改成如下幾種方式:

select * from table where id in (select id from table where add_date > '2010' limit 1000000,100);
select * from table where id > (select id from table where add_date > '2010' limit 1000000,1) limit 20;

除了列舉的這兩種寫法,還有別的技巧就不一一贅述。
不過要注意的是,這種超大翻頁(yè)的場(chǎng)景應(yīng)在系統(tǒng)設(shè)計(jì)時(shí)就應(yīng)避免,如不允許無條件查詢,或者僅允許一頁(yè)一頁(yè)翻,不允許輸入頁(yè)數(shù)快速跳轉(zhuǎn)等。
6)創(chuàng)建索引時(shí)需要注意區(qū)分度。區(qū)分度高的才是一個(gè)合格的索引。區(qū)分計(jì)算有公式:

count(distinct left(列名, 索引長(zhǎng)度))/count(*)

在實(shí)際使用時(shí),對(duì)于不確定的sql,可以使用explain函數(shù)來確定是否走索引了,避免無索引查詢。

3.3、SQL規(guī)約

這節(jié)實(shí)際中更多的是約定于解釋,我選擇幾條我自己遇到過的來說明。
1)不使用外鍵,采用邏輯關(guān)聯(lián)解決外鍵關(guān)聯(lián)。外鍵在數(shù)據(jù)變更操作時(shí)會(huì)有級(jí)聯(lián)更新,影響DB操作效率,不推薦。
2)刪除數(shù)據(jù)時(shí)可以先查詢,避免寫錯(cuò)了產(chǎn)生悲劇。雖然很多公司有DBA審查SQL,但是不能依賴DBA的操作,需要養(yǎng)成良好習(xí)慣;

3.5、ORM規(guī)約

采用SSH/SSI/SSM結(jié)構(gòu)之后,底層的orm一般用hibernate/ibatis/mybatis的居多,那么在實(shí)際使用時(shí),有一些注意事項(xiàng)。
1)不要在xml配置文件中使用${},易出現(xiàn)注入風(fēng)險(xiǎn)。這個(gè)一般都會(huì)被公司級(jí)的sql掃描工具掃描到。
2)在更新時(shí),對(duì)于未改變的數(shù)據(jù),最好不要傳入?yún)?shù),原因如規(guī)范中所說:

一是易出錯(cuò);二是效率低;三是 binlog 增加存儲(chǔ)

3)spring中的@Transactional,會(huì)影響qps。同時(shí)在使用事務(wù)時(shí),需要注意異常回滾后的關(guān)聯(lián)回滾,如緩存、業(yè)務(wù)、消息撤回、統(tǒng)計(jì)調(diào)整等。

4、工程規(guī)約

這部分比較多是一些約定。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容