一、研發流程規范
二、SQL編碼規范
數據庫命名規范:數據庫名一律小寫,必須以字母開頭。庫名包含多個單詞的,以下划線“_”分隔。如果采用分庫方案,分庫編號從“0”開始,用“0”左補齊為四位。
表名規范:表名一律小寫,必須以字母開頭。表名中包含多個單詞的,以下划線“_”分隔。如果采用分表方案,同時分表編號從“0”開始,用“0”左補齊為四位。建議使用‘數據庫名_表名’形式,例如:tkn_users。
字段名和字段類型規范:字段名一律小寫,必須以字母開頭,言簡意賅且不含拼寫錯誤的單詞(限用有歧義的縮寫形式)。字段名中包含多個單詞的,以“_”分隔。字段類型越小越好,並留有一定余地。字段類型盡量設置為not null(特別是primary key和unique key引用的字段,更要注意這一點);數字類型盡量設置為unsigned(防止溢出之后數值變負);不要保存default數值,以免表結構中存在業務邏輯。primary key引用的字段,主表必須為數字型非負非空自增id,分表必須為數字型非負非空自增id或數字型非負非空id。注意約定俗成的字段,如user_id、gmt_create和gmt_modified。意義相同的情況下,要使用約定俗成的字段。其中,gmt_create和gmt_modified字段需要定義為datetime not null,user_id需要定義為bigint unsigned。在表中使用擴展字段如features時,盡量使用key-value形式存儲,每一個key_value使用‘;’隔開,key全部小寫,必須以字母開頭,多個單詞使用'_'分割,不適用含有歧義的縮寫形式。
索引規范:不要使用含有null的列:只要列中包含有NULL值都將不會被包含在索引中,復合索引中只要有一列含有NULL值,那么這一列對於此復合索引就是無效的。所以我們在數據庫設計時不要讓字段的默認值為NULL。盡量使用段索引:對串列進行索引,如果可能應該指定一個前綴長度。例如,如果有一個CHAR(255)的列,如果在前10個或20個字符內,多數值是惟一的,那么就不要對整個列進行索引。短索引不僅可以提高查詢速度而且可以節省磁盤空間和I/O操作。最好使用數字做索引。擴展索引:索引修改盡量使用擴展索引,不要新建索引。比如表中已經有a的索引,現在要加(a,b)的索引,那么只需要修改原來的索引即可。索引列排序:MySQL查詢只使用一個索引,因此如果where子句中已經使用了索引的話,那么order by中的列是不會使用索引的。因此數據庫默認排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列創建復合索引。like語句操作:一般情況下不鼓勵使用like操作,如果非使用不可,如何使用也是一個問題。like “%aaa%” 不會使用索引而like “aaa%”可以使用索引。不要在列上進行運算:select * from users where YEAR(adddate)<2007,將在每個行上進行運算,這將導致索引失效而進行全表掃描,因此我們可以改成:select * from users where adddate<’2007-01-01′。索引順序:mysql會一直向右匹配直到遇到范圍查詢(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d的順序可以任意調整。
此外,分表查詢:分表查詢必須添加分表鍵作為參數,以防止tddl對表進行join查詢,如無法使用分表鍵需要考慮分表鍵設置是否合理。單表數據量:單表數據量不超過500W,超過1000W的表需考慮使用分表,如無法使用分表需要控制表記錄數,歷史數據考慮遷移。
三、異常處理規范
在項目IDCM中,異常ServiceException繼承自RuntimeException。
package com.alibaba.tboss.exception; public class ServiceException extends RuntimeException { private static final long serialVersionUID = -91784960796452539L; protected String errCode; public ServiceException(String errorMsg){ super(errorMsg); } public ServiceException(String errorMsg, Exception e){ super(errorMsg, e); } public ServiceException(ErrorCode msgObj){ super(msgObj.getFullMsg()); this.errCode = msgObj.getCode(); } public ServiceException(ErrorCode msgObj, String... arg){ super(msgObj.getFullMsg(arg)); this.errCode = msgObj.getCode(); } public ServiceException(ErrorCode msgObj, Throwable cause){ super(msgObj.getFullMsg(), cause); this.errCode = msgObj.getCode(); } public String getErrCode() { return this.errCode; } public void setErrCode(String errCode) { this.errCode = errCode; } }
異常的總體原則:忽略不能處理的異常,留給框架統一處理。仔細定義業務異常,謹慎處理,不要吃掉任何異常。
數據訪問層:DAO中的方法可以不聲明拋出異常(推薦)或聲明拋出DAOException(這個異常是SqlMapClientDaoSupportEx類封裝結果,沒有什么作用)。數據訪問層一般不要定義業務異常。
//正確的聲明方式: CustomerSettings getCustomerSettingsById(long customerId); // 推薦 CustomerSettings getCustomerSettingsById(long customerId) throws DAOException; //不推薦的聲明方式:這樣會強制要求調用它的類捕獲這個異常處理 CustomerSettings getCustomerSettingsById(long customerId) throws Exception;
業務層:BO中的方法可以不聲明拋出異常(推薦)或聲明拋出BOException或其他的checked exception(業務異常)。對於業務異常,需要強制外部調用處理的,需合理規范和定義業務異常類。
//正確的聲明方式: public Long updateTrade(String tradeNo,String alitradeno,String totalFee); public Long updateTrade(String tradeNo,String alitradeno,String totalFee) throws BOException; //對於一些特殊的業務異常: public Long updateTrade(String tradeNo,String alitradeno,String totalFee) throws TradeException; public class TradeException extends Exception {...}
展現層:Controller中的方法可以聲明拋出Exception,並且在處理過程中不catch任何Exception。框架會捕獲並做后續操作,框架在捕到異常后,會打到root logger中,並且重定向到通用的錯誤頁面。如果你需要對特定的異常做特殊處理(如跳轉到其他的錯誤頁面)的話,才需要考慮抓住異常,這時請正確地記錄這個異常,以方便后期跟蹤問題。
其他規范:如果不是一定要處理,盡可能忽略RuntimeException(包括DAOException和BOException等),留給框架處理。如果需要抓異常,盡量抓某一個特定的異常(如TradeException),不要將所有Exception全部抓住。抓住異常后,要么記錄異常詳細信息到日志文件,要么帶上原有異常重新拋出,不要兩樣都做,避免重復記錄。不要兩樣都不做,會吃掉異常。不要使用e.printStackTrace(),因為有性能問題,而且不能指定記錄日志的文件。
// 不推薦這種方式:抓住異常log一下再拋出來,這是多余的,框架會為我們處理。 // 當然,如果需要拋出的是checked exception則另當別論 try { .... } catch (Exception e) { log.error("abcd", e); throw new BOException("abcd", e); } // 吃掉了異常,外面不知道怎么回事 try { .... } catch (Exception e) { } // 沒有正確記錄異常,日志文件中記錄的信息太少不利於錯誤跟蹤 try { .... } catch (Exception e) { log.error("abcd" + e.getMessage); } try { .... } catch (Exception e) { log.error("abcd" + e); } try { .... } catch (Exception e) { log.error(e); } // 正確的使用 try { .... } catch (Exception e) { log.error("abcd", e); }
在我們的項目IDCM中,是如何處理異常信息的呢?
RPC層處理異常信息:
@ResourceMapping("rackList") public DataResult<Rack> queryRackList(@RequestParams PagePara pagePara, @RequestParams Rack rack, @RequestParam(name = "operateType") String operateType) { DataResult<Rack> dataResult = new DataResult<Rack>(); try { // TODO 業務邏輯處理 } catch (Exception e) { logger.error("queryRackList err : ", e); if (e instanceof ServiceException) throw (ServiceException) e; else throw new ServiceException("查詢數據失敗"); } return dataResult; }
BoImpl層的異常信息:
@Override public DataResult<WorkOrderMain> queryOrderPagination(WorkOrderMain orderMain, PagePara pagePara) { DataResult<WorkOrderMain> dr = new DataResult<WorkOrderMain>(); try { // TODO 調用dao層的業務邏輯 } catch (Exception e) { logger.error(" WorkOrderLogisticsBoImpl_queryOrderPagination_error [orderMain={}]:", JSON.toJSON(orderMain).toString(), e); throw new ServiceException(ErrorCode.Query_Error, e); } return dr; }
四、日志規范
日志的作用: 記錄重要數據、操作以備日后核對;記錄案發現場,方便日后定位問題。日志的分類: 按日志的用途可以分為系統異常日志,應用相關日志,用戶操作日志等。不同類型的日志分開記錄,系統異常日志一般記錄在root logger下;如velocity相關的日志也可以分記到不同的日志文件中,方便問題查找。記錄日志INFO或DEGUB級別的應該先做log.isXXXenabled()判斷。(如果是應用日志就打算記錄成INFO級別則不用這一條)業務代碼中,盡可能少用DEBUG級別的日志,容易混淆業務邏輯。在通用的底層代碼中,可以適當用一些INFO和DEBUG級別的日志。在調試代碼時可以利用這些日志信息,避免過於深入的跟蹤。
異常日志需要打印異常的詳細信息,如stackTrace等,方便問題查找。現在的框架會統一處理異常。自己捕獲異常處理后如果不繼續往外拋出,不要忘記記錄該異常。
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; Log logger = LogFactory.getLog(getClass());//名字統一用logger Log log = LogFactory.getLog(getClass()); //不推薦
構造log格式:必須有的字段:時間(%d{yy-MM-dd HH:mm:ss})、日志級別( %-5p)、類名(%c)、行號(%L) log內容(%m%n)
//正確 logger.error("xxxxx",e); //錯誤 logger.error("xxxxx"+e);
異常log,需要錯誤堆棧,異常對象,要放到第二個參數。
# 正確 $!{param} # 錯誤 $param
變量前面一定要有!。
#推薦
$!{param}
最好加上大括號(不強制),變量名前后加上{}。
五、Java編碼規范
重載方法不應該分開:當一個類有多個構造函數,或者多個同名成員方法時,這些函數應該寫在一起,不應該被其他成員分開。
命名:包名:包名全部用小寫字母,通過 . 將各級連在一起。不應該使用下划線。類名:類型的命名,采用以大寫字母開頭的大小寫字符間隔的方式(UpperCamelCase)。class命名一般使用名詞或名詞短語。interface的命名有時也可以使用形容詞或形容詞短語。annotation沒有明確固定的規范。測試類的命名,應該以它所測試的類的名字為開頭,並在最后加上Test結尾。例如:HashTest 、 HashIntegrationTest。(PS:經常見到寫成TestXXXX的類名,以后注意了)。方法名:方法命名,采用以小寫字母開頭的大小寫字符間隔的方式(lowerCamelCase)。方法命名一般使用動詞或者動詞短語。在JUnit的測試方法中,可以使用下划線,用來區分測試邏輯的名字,經常使用如下的結構:test<MethodUnderTest>_<state> 。例如:testPop_emptyStack 。測試方法也可以用其他方式進行命名。常量名:常量命名,全部使用大寫字符,詞與詞之間用下划線隔開。(CONSTANCE_CASE),常量一般使用名詞或者名詞短語命名。PS:常量是一個靜態成員變量,但不是所有的靜態成員變量都是常量。在選擇使用常量命名規則給變量命名時,需要明確這個變量是否是常量。例如,如果這個變量的狀態可以發生改變,那么這個變量幾乎可以肯定不是常量。只是計划不會發生改變的變量不足以成為一個常量,但是我們這里把這種情況也定義為常量,所以也要大寫,下面是常量和非常量的例子:
// 常量 static final int NUMBER = 5; static final List<String> NAMES = Collections.unmodifiableList(Arrays.asList("ed", "ann")); static final ImmutableList<String> NAMES2 = ImmutableList.of("ed", "ann"); static final SomeMutableType[] EMPTY_ARRAY={}; enum SomeEnum{ENUM_CONSTANT} //不是常量 static String nonFinal = "non-final"; final String nonStatic = "non-static"; static final Set<String> mutableCollection = new HashSet<String>();//集合不是不可變的 //下面三種情況雖然內部存放對象可能會改變,但是屬於計划不會發生改變的變量,我們也算做常量,所以也要大寫。 static final ImmutableSet<SomeMutableType> MUTABLE_ELEMENTS=ImmutableSet.of(mutable); static final Logger LOGGER=Logger.getLogger(MyClass.getName()); static final String[] NON_EMPTY_ARRAY = { "these", "can", "change" };
非常量的成員變量名:非常量的成員變量命名(包括靜態變量和非靜態變量),采用lowerCamelCase命名。一般使用名詞或名詞短語。參數名:參數命名采用lowerCamelCase命名。#應該避免使用一個字符作為參數的命名方式#。特殊變量或參數名:DTO(Data Transfer Object):在變量、類名、參數等地方使用的時候,如果要用DTO結尾做名字,必須用大寫。例如:UserDTO.class、userDTO、userDTOs、userDTOList。DO(Data Object咱們這邊經常用在DB操作):在變量、類名、參數等地方使用的時候,如果要用DO結尾做名字,必須用大寫。文件后綴:Service表明這個類是個服務類,里面包含了給其他類提同業務服務的方法。DAO這個類封裝了數據訪問方法。Impl這個類是一個實現類,而不是接口(通常是實現DAO和Service)。
六、代碼格式規范
if(a!=null)return true 需要改成: if(a!=null){ return true; }
花括號一般用在if, else, for, do, 和 while等語句。甚至當它的實現為空或者只有一句話時,也需要使用。
if(){ if(){ if(){ } } }
if語句的嵌套層數保證在3層以內。太多層嵌套,最后自己都看不懂。直觀感受一下,加上代碼塊,就會很復雜,而且還會有else。尤其是在代碼不斷增加需求的過程中,要重構一些代碼,造成代碼if嵌套混亂。
類的文件代碼長度不要超過1000行。方法的長度不要超過150行,超出的考慮拆分一下。
public enum EnumExample { EXAMPLE_ONE, EXAMPLE_TWO; } public enum EnumExample { /** 例子一 */ EXAMPLE_ONE("1"), /** 例子二 */ EXAMPLE_TWO("2"); private EnumExample(String index) { } }
枚舉經常需要被外部引用,如果名字不能直接表達,需要加javadoc。枚舉名需要全大寫,單詞用“_”分割。
局部變量:局部變量不應該習慣性地放在語句塊的開始處聲明,而應該盡量離它第一次使用的地方最近的地方聲明,以減小它們的使用范圍。局部變量應該在聲明的時候就進行初始化。如果不能在聲明時初始化,也應該盡快完成初始化。
switch必須有default:每個switch語句中,都需要顯式聲明default標簽。即使沒有任何代碼也需要顯示聲明。
修飾符的順序:多個類和成員變量的修飾符,按Java Lauguage Specification中介紹的先后順序排序。具體是:
public protected private abstract static final transient volatile synchronized native strictfp
除注釋外的代碼,不允許出現中文。經常看到log里面用中文,這樣不允許,還有用中文直接equals比較,也同樣不允許。
項目中禁止使用System.out.println()。如果有log需求,直接用log4j好了,可以隨意指定輸出位置。
@Override public String toString() { //xxxxxx } }
@Override:@override 都應該使用。@override annotations只要是符合語法的,都應該使用。PS:如果沒寫Override intellij&eclipse默認都會有警告出現。
異常捕獲,不應該被忽略:一般情況下,catch住的異常不應該被忽略,而是都需要做適當的處理。例如將錯誤日志打印出來,或者如果認為這種異常不會發生,則應該作為斷言異常重新拋出。
try { return Integer.parseInt(response); }catch (NumberFormatException ok){ // 不是數字沒關系,繼續往下走就行啦 }
如果這個catch住的異常確實不需要任何處理,也應該通過注釋做出說明。
try { emptyStack.pop(); fail(); } catch (NoSuchMethodException expected) { }
在測試類里,有時會針對方法是否會拋出指定的異常,這樣的異常是可以被忽略的。但是這個異常通常需要命名為: expected。
七、javadoc規范
對外的接口一定要有javadoc(強制),例如一些hsf服務什么的接口必須要有。
第一種: /** * 我多行我開心 */ 第二種: /** 我單行我快樂 */
@從句:所有標准的@從句,應該按照如下的順序添加:@param、@return、@throws、@deprecated。並且這四種@從句,不應該出現在一個沒有描述的Javadoc塊中。
何處應該使用Javadoc:至少,Javadoc應該應用於所有的public類、public和protected的成員變量和方法。和少量例外的情況。例外情況如下:例外一:方法本身已經足夠說明的情況。當方法本身很顯而易見時,可以不需要javadoc。例如:getFoo。沒有必要加上javadoc說明“Returns the foo”。單元測試中的方法基本都能通過方法名,顯而易見地知道方法的作用。因此不需要增加javadoc。注意:有時候不應該引用此例外,來省略一些用戶需要知道的信息。例如:getCannicalName 。當大部分代碼閱讀者不知道canonical name是什么意思時,不應該省略Javadoc,認為只能寫/** Returns the canonical name. */例外二:重載方法:重載方法有時不需要再寫Javadoc。
八、二方庫版本控制規范
對外公開的api類二方庫版本控制規范 版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下: 主版本號:當你做了不兼容的API 修改。 次版本號:當你做了向下兼容的功能性新增(新增接口、接口兼容性修改等)。 修訂號:當你做了向下兼容的問題修正或者實現的修改。 其他版本版本編譯信息加到“主版本號.次版本號.修訂號”的后面,作為延伸。 好處:版本號及其更新方式包含了相鄰版本間的底層代碼和修改內容的信息,通過版本號可以知道如何升級,例如:次版本/修訂版本升級了,就可以考慮升級,因為是向后兼容的。 版本控制規范: 標准的版本號XYZ格式,其中X、Y和Z為非負的整數,X是主版本、Y是次版本號、而Z為修訂號。每個元素必須以數值來遞增。例如:1.9.1 -> 1.10.0 -> 1.11.0。 有版本號的二方庫發布后,禁止改變該版本的內容。任何修改都必須增加新版本發行(snapshot除外)。 主版本號為零(0.yz)的軟件處於開發初始階段,一切都可能隨時被改變。這樣的公共API 不應該被視為穩定版。 1.0.0 的版本號用於界定公共API 的形成。這一版本之后所有的版本號更新都基於公共API 及其修改內容。 修訂號Z(xyZ | x > 0)“必須”在只做了向下兼容的修正時才遞增。這里的修正指的是針對不正確結果而進行的內部修改(例如:修bug)或者在內部程序有大量新功能或改進被加入時遞增(我們業務需求經常干的)。在任何公共API的功能被標記為棄用時也“必須”遞增(例如:廢棄了某個接口)。 次版本號Y(xYz | x > 0)“必須”在有向下兼容的新功能出現時遞增。(例如:新增接口/接口參數從int變為Integer類似),但每當次版本號(Y)遞增時,修訂號(Z)“必須”歸零。 主版本號X(Xyz | X > 0)“必須”在有任何不兼容的修改被加入公共API時遞增。其中“可以”包括次版本號(Y)及修訂級別(Z)的改變。每當主版本號遞增時,次版本號和修訂號“必須”歸零。 先行版本(例如SNAPSHOT版本、alpha等)可以標注在修訂版(Z)之后,先加上一個連接號()再加上一連串以句點分隔的標識符號來修飾。標識符號“必須”由ASCII碼的英數字和連接號[0-9A-Za-z]組成,且“禁止”留白。數字型的標識符號“禁止”在前方補零。被標上先行版本號則表示這個版本並非穩定而且可能無法達到兼容的需求。范例:1.0.0-snapshot、1.0.0-alpha、1.0.0-alpha.1、 1.0.0-0.3.7、1.0.0-x.7.z.92。 我們的實踐方案 根據上面的規范,我們的對外二方庫,可以遵循上面的方式開發,不能使用SNAPSHOT版本,對內的使用或者不適用SNAPSHOT都可以。 如果是對外的api(例如:market.open.share),有對應的open.client這種sdk,open.share和open.client必須聯動升級,例如:open.share 主次版本修改open.client也必須跟着修改,並且版本一致。 外部使用,market舉例,針對有open.client這種sdk的對外api二方庫,使用api的時候,直接依賴open.client就可以了,不需要手動指定open.share,原因見上一條。 對外公布的二方庫里面增加文件(README.md) 記錄每個主版本和次版本號的升級日志。 對外open的二方庫只能依賴其他open的二方庫或者三方庫,不能依賴自己的非open的二方庫。 PS: 對內的二方庫使用SNAPSHOT版本,要大寫
九、三方庫的使用
import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; List<String> keys = Lists.newArrayListWithCapacity(infos.size()); Map<K, V> resultMap = Maps.newHashMapWithExpectedSize(tmpList.size()); Set<String> sets=Sets.newHashSet();
創建集合&Map:推薦使用guava的靜態方法,創建集合。
字符串操作:優先使用:StringUtils(org.apache.commons.lang3.StringUtils)常用方法:isBlank、isNotBlank。
數組操作:優先使用:ArrayUtils(org.apache.commons.lang3.ArrayUtils)。常用方法:isEmpty、isNotEmpty、toString(final Object array)。
集合操作:優先使用:CollectionUtils(org.apache.commons.collections4.CollectionUtils)常用方法:isEmpty、isNotEmpty、size。
Map操作:優先使用:MapUtils(org.apache.commons.collections4.MapUtils)常用方法:isEmpty、isNotEmpty。
除上面以外還有NumberUtils、DateFormatUtils、DateUtils等優先使用org.apache.commons.lang3這個包下的,不要使用org.apache.commons.lang包下面的。原因是commons.lang這個包是從jdk1.2開始支持的所以很多1.5/1.6的特性是不支持的,例如:泛型。
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; public class User { private String name; private User(String name) { this.name = name; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } public static void main(String[] args) { System.out.println(new User("testname")); } } 輸出:User[name=testname]
重載toString方法:優先使用:ToStringBuilder(org.apache.commons.lang3.builder.ToStringBuilder)。
相關第三方庫依賴如下:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
十、消滅警告規范
消滅警告:警告本來就是可能有問題的代碼,還是有可能會影響正常功能。或者至少去掉會讓代碼更簡潔,PS:警告eclipse左側會顯示屎黃色的小標。
用findbugs掃一下:好處:幫助我們發現很多問題和優化意見(貌似最多的就是空指針)。首先裝個findbugs插件,裝好了右鍵執行,完成后在Bug Explorer視圖里面看結果。
十一、codereview規范
codereview:發布代碼前在aone指定codereview的人,進行代碼審查。但是代碼審查並不是說只找一個人,這可以是團隊多人一起做。進行reivew的人也不一定必須是老人,有時我們可能覺得“經驗比較淺,不能對別人codereivew”,其實並不一定,俗話說:三個臭皮匠,頂個諸葛亮。即使是經驗欠缺,多個人review,也能完成高質量的代碼。
怎么做codereivew呢?
- codereivew的人要做那些事情,簡單來說就是看代碼,具體看什么下面是一些建議:
- 實現正確嗎(如果能看出來最好,看不出來,那也沒辦法,reivew的人不是測試)
- 代碼實現清晰易讀嗎 (優美的代碼,會讓人看得入迷的)
- 是不是最好的實現,有沒有重構的空間
- 有沒有沒有考慮到的點,特別是對其他部分代碼會否造成影響(例如:發布沒兼容老業務,會不會對線上造成影響)
- 測試代碼同樣需要審查(有空的話,測試用例也過一下)
十二、