解讀阿里巴巴 Java 代碼規範(2): 從代碼處理等方面解讀阿里巴巴 Java 代碼規範

解讀阿里巴巴 Java 代碼規範(2): 從代碼處理等方面解讀阿里巴巴 Java 代碼規範

前言

2017 年阿里雲棲大會,阿里發佈了針對 Java 程序員的《阿里巴巴 Java 開發手冊(終極版)》,這篇文檔做爲阿里數千位 Java 程序員的經驗積累呈現給公衆,並隨之發佈了適用於 Eclipse 和 Intellim 的代碼檢查插件。爲了可以深刻了解 Java 程序員編碼規範,也爲了深刻理解爲何阿里這樣規定,是否規定有誤,本文以阿里發佈的這篇文檔做爲分析起源,擴大範圍至業界其餘公司的規範,例如谷歌、FaceBook、微軟、百度、華爲,並搜索網絡上技術大牛發表的技術文章,深刻理解每一條規範的設計背景和目標。html

因爲解讀文章僅有兩篇,因此按照阿里的篇幅權重分爲上篇僅針對 Java 語言自己的編碼規約,下篇包含日誌管理、異常處理、單元測試、MySQL 規範、工程規範等方面內容進行解讀。本文是下篇,主要針對編碼規約部分進行解讀,注意,本文所附代碼限於篇幅可能並不完整,也可能因爲機器不一樣,存在運行結果不徹底一致的狀況,請讀者見諒。前端

異常日誌

異常處理

不要捕獲 RuntimeException

阿里強制規定 Java 類庫中的 RuntimeException 能夠經過預先檢查進行規避,而不該該經過 catch 來處理,例如 IndexOutOfBoundsException、NullPointerException 等。java

個人理解

RuntimeException,也被稱爲運行時異常,一般是因爲代碼中的 bug 引發的,正確的處理方式是去檢查代碼,經過添加數據長度判斷,判斷對象是否爲空等方法區規避,而不是靠捕獲來規避這種異常。程序員

事務中的異常須要回滾

阿里強制規定有 try 塊放到了事務代碼中,catch 異常後,若是須要回滾事務,必定要注意手動回滾事務。正則表達式

個人理解

try catch 代碼塊中對異常的處理,可能會遺漏事務的一致性,當事務控制不使用其餘框架管理時,事務須要手動回滾。實際使用若是引入第三方的框架對事務進行管理,好比 Spring,則根據第三方框架的實際實現狀況,肯定是否有必要手動回滾。當第三方事務管理框架自己就會對於異常進行拋出時須要作事務回滾。例如 Spring 在@Transactional 的 annotation 註解下,會默認開啓運行時異常事務回滾。算法

不能在 finally 塊中使用 return

阿里強制要求 finally 塊中不使用 return,由於執行完該 return 後方法結束執行,不會再執行 try 塊中的 return 語句。數據庫

個人理解

咱們來看一個示例,代碼如清單 1 所示。apache

清單 1 示例代碼

public class demo{
 public static void main(String[] args){
 System.out.println(m_1());
 }
 public int m_1(){
 int i = 10;
 try{
 System.out.println("start");
 return i += 10;
 }catch(Exception e){
 System.out.println("error:"+e);
 }finally{
 if(i>10){
 System.out.println(i);
 }
 System.out.println("finally");
 return 50;
 }
 }}

輸出以下清單 2 所示。安全

清單 2 清單 1 程序運行輸出

start20finally50

對此現象能夠經過反編譯 class 文件很容易找到緣由,如清單 3 所示。服務器

清單 3 反編譯文件

public class demo{
 public static void main(String[] args){
 System.out.println(m_1());}
 public int m_1(){
 int i = 10;
 try{
 System.out.println("start");
 return i;
 }catch(Exception e){
 System.out.println("error:"+e);
 }finally{
 if(i>10){
 System.out.println(i);
 }
 System.out.println("finally");
 i = 50;
 }
 return i;
 }
 }}

首先 i+=10;被分離爲單獨的一條語句,其次 return 50;被加在 try 和 catch 塊的結尾,"覆蓋"了 try 塊中原有的返回值。若是咱們在 finally 塊中沒有 return,則 finally 塊中的賦值語句不會改變最後的返回結果,代碼如清單 4 所示。

清單 4 finally 塊示例代碼

public class demo{
 public static void main(String[] args){
 System.out.println(m_1());}
 public static int m_1(){
 int i = 10;
 try{
 System.out.println("start");
 return i += 10;
 }catch(Exception e){
 System.out.println("error:"+e);
 }finally{
 if(i>10){
 System.out.println(i);
 }
 System.out.println("finally");
 i = 50;
 }
 return i;
 }}

輸出結果如清單 5 所示。

清單 5 清單 4 程序運行輸出

startfinally10

日誌規約

不可直接使用日誌系統

阿里強制規定應用中不可直接使用日誌系統(Log4j、Logback)中的 API,而應依賴使用日誌框架 SLF4J 中的 API,使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。

個人理解

SLF4J 即簡單日誌門面模式,不是具體的日誌解決方案,它只服務於各類各樣的日誌系統。在使用 SLF4J 時不須要指定哪一個具體的日誌系統,只須要將使用到的具體日誌系統的配置文件放到類路徑下去。

正例如清單 6 所示。

清單 6 正例代碼

import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class HelloWorld{private static final Logger logger =
 LoggerFactory.getLogger(HelloWorld.class);
 public static void main(String[] args){
 logger.info("please use SLF4J,rather than logback or log4j");
 }}

反例如清單 7 所示。

清單 7 反例代碼

import org.apache.log4j.Logger;public class HelloWorld{
 private static final Logger logger =
 LoggerFactory.getLogger(HelloWorld.class);
 public static void main(String[] args){
 logger.info("please use SLF4J,rather than logback or log4j");
 }}

日誌文件保留時間

阿里強制規定日誌文件至少保存 15 天,由於有些異常具有以"周"爲頻次發生的特色。

個人理解

日誌保留時間推薦 15 天以上,可是保留時間也不宜過長,通常不超過 21 天,不然形成硬盤空間的浪費。對於一些長週期性執行的邏輯,能夠根據實際狀況調整該保存時間,同時也須要保證日誌可以監控到關鍵的應用。

對於長週期執行的邏輯,可使用特定的 appender,並使用不一樣的日誌清理規則,如時間、大小等。如一月執行一次的定時任務,能夠將日誌輸出到新的日誌文件,而後經過大小限定的規則進行清理,並不必定要使用時間清理的邏輯。

安全規約

權限控制校驗

阿里強制要求對於隸屬於用戶我的的頁面或者功能必須進行權限控制校驗。

個人理解

涉及到對於數據的增刪改查,必須有權限的控制和校驗,要有一個黑白名單的控制,不能依賴於前臺頁面的簡單控制,後臺要有對於完整的權限控制的實現。這樣就能儘量地防治數據的錯誤修改。

用戶傳入參數校驗

阿里強制要求用戶請求傳入的任何參數必須作有效校驗。

個人理解

對於用戶輸入的任何參數,前端頁面上都必需要作必定的有效性校驗,而且在數據發送至服務器的時候在頁面上給出驗證結果提示,那麼在用戶請求傳入的任務參數,後臺一樣也要對其有效性進行驗證,防止前端頁面未能過濾或者暫時沒法驗證的錯誤參數。忽略參數的驗證會致使的問題不少,page size 過大會致使內存溢出、SQL 溢出等,只有驗證才能儘量地減小這些問題的出現,進而減小錯誤的排查概率。

單元測試

單元測試應該自動執行

阿里強制單元測試應該是全自動執行的,而且非交互式的。測試框架一般是按期執行的,執行過程必須徹底自動化纔有意義。輸出結果須要人工檢查的測試不是一個號的單元測試。單元測試中不許使用 System.out 來進行人肉驗證,必須使用 assert 來驗證。

個人理解

這條原則比較容易理解。單元測試是整個系統的最小測試單元,針對的是一個類中一個方法的測試,若是這些測試的結果須要人工校驗是否正確,那麼對於驗證人來講是一項痛苦並且耗時的工做。另外,單元測試做爲系統最基本的保障,須要在修改代碼、編譯、打包過程當中都會運行測試用例,保障基本功能,自動化的測試是必要條件。其實自動化測試不只是單元測試特有的,包括集成測試、系統測試等,都在慢慢地轉向自動化測試,以下降測試的人力成本。

單元測試應該是獨立的

阿里強制保持單元測試的獨立性。爲了保證單元測試穩定可靠且便於維護,單元測試用例之間決不能互相調用,也不能依賴執行的前後次序。反例:method2 須要依賴 method1 的執行,將執行結果做爲 method2 的輸入。

個人理解

單元測試做爲系統的最小測試單元,主要目的是儘量早地測試編寫的代碼,下降後續集成測試期間的測試成本,以及在運行測試用例的時候可以快速地定位到對應的代碼段並解決相關問題。

咱們假設這麼一個場景,method1 方法被 10 個其餘 method 方法調用,若是 10 個 method 方法的測試用例都須要依賴 method1,那麼當 methdo1 被修改致使運行出錯的狀況下,會致使 method1 以及依賴它的 10 個 method 的全部測試用例報錯,這樣就須要排查這 11 個方法到底哪裏出了問題,這與單元測試的初衷不符,也會大大的增長排查工做量,因此單元測試必須是獨立的,不會由於受到外部修改(這裏的修改包括了依賴方法的修改以及外部環境的修改),編寫單元測試時遇到的這類依賴可使用 mock 來模擬輸入和指望的返回,這樣因此來的方法內部邏輯的變動就不會影響到外部的實現。

BCDE 原則

阿里推薦編寫單元測試代碼遵照 BCDE 原則,以保證被測試模塊的交付質量。

個人理解

BCDE 原則逐一解釋以下:

B(Border):確保參數邊界值均被覆蓋。

例如:對於數字,測試負數、0、正數、最小值、最大值、NaN(非數字)、無窮大值等。對於字符串,測試空字符串、單字符、非 ASCII 字符串、多字節字符串等。對於集合類型,測試空、第一個元素、最後一個元素等。對於日期,測試 1 月 1 日、2 月 29 日、12 月 31 日等。被測試的類自己也會暗示一些特定狀況下的邊界值。對於邊界狀況的測試必定要詳盡。

C(Connect):確保輸入和輸出的正確關聯性。

例如,測試某個時間判斷的方法 boolean inTimeZone(Long timeStamp),該方法根據輸入的時間戳判斷該事件是否存在於某個時間段內,返回 boolean 類型。若是測試輸入的測試數據爲 Long 類型的時間戳,對於輸出的判斷應該是對於 boolean 類型的處理。若是測試輸入的測試數據爲非 Long 類型數據,對於輸出的判斷應該是報錯信息是否正確。

D(Design):任務程序的開發包括單元測試都應該遵循設計文檔。

E(Error):單元測試包括對各類方法的異常測試,測試程序對異常的響應能力。

除了這些解釋以外,《單元測試之道(Java 版)》這本書裏面提到了關於邊界測試的 CORRECT 原則:

一致性(Conformance):值是否符合預期格式(正常的數據),列出全部可能不一致的數據,進行驗證。

有序性(Ordering):傳入的參數的順序不一樣的結果是否正確,對排序算法會產生影響,或者是對類的屬性賦值順序不一樣會不會產生錯誤。

區間性(Range):參數的取值範圍是否在某個合理的區間範圍內。

引用/耦合性(Reference):程序依賴外部的一些條件是否已知足。前置條件:系統必須處於什麼狀態下,該方法才能運行。後置條件,你的方法將會保證哪些狀態發生改變。

存在性(Existence):參數是否真的存在,引用爲 Null,String 爲空,數值爲 0 或者物理介質不存在時,程序是否能正常運行。

基數性(Cardinality):考慮以"0-1-N 原則",當數值分別爲 0、一、N 時,可能出現的結果,其中 N 爲最大值。

時間性(Time):相對時間指的是函數執行的依賴順序,絕對時間指的是超時問題、併發問題。

建表的是與否規則

阿里強制要求若是遇到須要表達是與否的概念時,必須使用 is_xxx 的方法命令,數據類型是 unsigned tinyint,1 表示是,0 表示否。

說明:任務字段若是爲非負數,必須是 unsigned。

正例:表達邏輯刪除的字段名 is_deleted,1 表示刪除,0 表示未刪除。

個人理解

命名使用 is_xxx 第一個好處是比較清晰的,第二個好處是使用者根據命名就能夠知道這個字段的取值範圍,也方便作參數驗證。

類型使用 unsigned 的好處是若是隻存整數,unsigned 類型有更大的取值範圍,能夠節約磁盤和內存使用。

對於表的名字,MySQL 社區有本身推薦的命名規範:

  1. 表包含多個英文單詞時,須要用下劃線進行單詞分割,這一點相似於 Java 類名的命名規範,例如 master_schedule、security_user_permission;
  2. 因爲 InnoDB 存儲引擎自己是針對操做系統的可插拔設計的,因此原則上全部的表名組成所有由小寫字母組成;
  3. 不容許出現空格,須要分割一概採用下劃線;
  4. 名字不容許出現數字,僅包含英文字母;
  5. 名字須要總長度少於 64 個字符。

數據類型精度考量

阿里強制要求存放小數時使用 decimal,禁止使用 float 和 double。

說明:float 和 double 在存儲的時候,存在精度損失的問題,極可能在值的比較時,獲得不正確的結果。若是存儲的數據範圍超過 decimal 的範圍,建議將數據拆成整數和小數分開存儲。

個人理解

咱們先來看看各個精度的範圍。

Float:浮點型,4 字節數 32 位,表示數據範圍-3.4E38~3.4E38

Double:雙精度型,8 字節數 64 位,表示數據範圍-1.7E308~1.7E308

Decimal:數字型,16 字節數 128 位,不存在精度損失,經常使用於銀行帳目計算

在精確計算中使用浮點數是很是危險的,在對精度要求高的狀況下,好比銀行帳目就須要使用 Decimal 存儲數據。

實際上,全部涉及到數據存儲的類型定義,都會涉及數據精度損失問題。Java 的數據類型也存在 float 和 double 精度損失狀況,阿里沒有指出這條規約,就全文來講,這是一個比較嚴重的規約缺失。

Joshua Bloch(著名的 Effective Java 書做者)認爲,float 和 double 這兩個原生的數據類型自己是爲了科學和工程計算設計的,它們本質上都採用單精度算法,也就是說在較寬的範圍內快速得到精準數據值。可是,須要注意的是,這兩個原生類型都不保證也不會提供很精確的值。單精度和雙精度類型特別不適用於貨幣計算,由於不可能準確地表示 0.1(或者任何其餘十的負冪)。

舉個例子,如清單 8 所示。

清單 8 示例代碼

float calnUM1;double calNum2;calNum1 = (float)(1.03-.42);calNum2 = 1.03-.42;System.out.println("calNum1="+ calNum1);System.out.println("calNum2="+ calNum2);System.out.println(1.03-.42);calNum1 = (float)(1.00-9*.10);calNum2 = 1.00-9*.10;System.out.println("calNum1="+ calNum1);System.out.println("calNum2="+ calNum2);System.out.println(1.00-9*.10);

輸出結果如清單 9 所示。

清單 9 清單 8 示例代碼運行輸出結果

calNum1=0.61calNum2=0.61000000000000010.6100000000000001calNum1=0.1calNum2=0.099999999999999980. 09999999999999998

從上面的輸出結果來看,若是寄但願於打印時自動進行四捨五入,這是不切實際的。

咱們再來看一個實際的例子。假設你有 1 塊錢,如今每次購買蛋糕的價格都會遞增 0.10 元,爲咱們一共能夠買幾塊蛋糕。口算一下,應該是 4 塊(由於 0.1+0.2+0.3+0.4=1.0),咱們寫個程序驗證看看,如清單 10 所示。

清單 10 示例代碼

//錯誤的方式double funds1 = 1.00;int itemsBought = 0;for(double price = .10;funds>=price;price+=.10){
 funds1 -=price;
 itemsBought++;}
 System.out.println(itemsBought+" items boughts.");
 System.out.println("Changes:"+funds1);
 //正確的方式 final BigDecimal TEN_CENTS = new BigDecimal(".10");
 itemsBought = 0;
 BigDecimal funds2 = new BigDecimal("1.00");for(BigDecimal price = TEN_CENTS;funds2.compareTo(price)>0;price =
 price.add(TEN_CENTS)){
 fund2 = fund2.substract(price);
 itemsBought++;
 }
 System.out.println(itemsBought+" items boughts.");
 System.out.println("Changes:"+funds2);

運行輸出如清單 11 所示。

清單 11 清單 10 示例代碼運行輸出結果

3 items boughts.Changes:0.39999999999999994 items boughts.Changes:0.00

這裏咱們能夠看到使用了 BigDecimal 解決了問題,實際上 int、long 也能夠解決這類問題。採用 BigDecimal 有一個缺點,就是使用過程當中沒有原始數據這麼方便,效率也不高。若是採用 int 方式,最好不要在有小數點的場景下使用,能夠在 100、10 這樣業務場景下選擇使用。

使用 Char

阿里強制要求若是存儲的字符串長度幾乎相等,使用 Char 定長字符串類型。

個人理解

我這裏不討論 MySQL,而是聊聊另外一種主流關係型數據庫-PostgreSQL。在 PostgreSQL 中建議使用 varchar 或者 text,而不是 char,這是由於它們之間沒有性能區別,可是 varchar、text 能支持動態的長度調整,存儲空間也更節省。

在 PostgreSQL 官方文檔中記錄了這兩種類型的比較,以下所示:

SQL 定義了兩種基本的字符類型:character varying(n)和 character(n),這裏的 n 是一個正整數。兩種類型均可以存儲最多 n 個字符的字符串(沒有字節)。試圖存儲更長的字符串到這些類型的字段裏會產生一個錯誤,除非超出長度的字符都是空白,這種狀況下該字符串將被截斷爲最大長度。若是要存儲的字符串比申明的長度短,類型爲 character 的數值將會用空白填滿,而類型爲 character varying 的數值就不會填滿。

若是咱們明確地把一個數值轉換成 character varying(n)或 character(n),那麼超長的數值將被截斷成 n 個字符,且不會拋出錯誤。這也是 SQL 標準的要求。

varchar(n)和 char(n)分別是 character varying(n)和 character(n)的別名,沒有申明長度的 character 等於 character(1);若是不帶長度說明可使用 character varying,那麼該類型接受任何長度的字符串。

另外,PostgreSQL 提供 text 類型,它能夠存儲任何長度的字符串。儘管類型 text 不是 SQL 標準,可是許多其餘 SQL 數據庫系統也有它。

Character 類型的數值物理上都用空白填充到指定的長度 n,而且以這種方式存儲。不過,填充的空白是暫時無語義的。在比較兩個 character 值的時候,填充的空白都不會被關注,在轉換成其餘字符串類型的時候,character 值裏面的空白會被刪除。請注意,在 character varying 和 text 數值的,結尾的空白是有語義的,而且當使用模式匹配時,如 LIKE,使用正則表達式。

一個簡短的字符串(最多 126 個字節)的存儲要求是 1 個字節加上實際的字符串,其中包括空格填充的 character。更長的字符串有 4 個字節的開銷,而不是 1。長的字符串將會自動被系統壓縮,所以在磁盤上的物理需求可能會更少些。更長的數值也會存儲在數據表裏面,這樣它們就不會干擾對短字段值的快速訪問。無論怎樣,容許存儲的最長字符串大概是 1GB。容許在數據類型聲明中出現的 n 的最大值比這個還小。修改該行也沒有什麼意義,由於在多字節編碼下字符和字節的數目可能差異很大。若是你想存儲沒有特定上限的長字符串,那麼使用 text 或沒有長度聲明的 character varying,而不要選擇一個任意長度限制。

從性能上分析,character(n)一般是最慢的,在大多數狀況下,應該使用 text 或者 character varying。

工程結構

應用分層

服務間依賴關係

阿里推薦默認上層依賴於下層,箭頭關係表示可直接依賴,如:開放接口層能夠依賴於 Web 層,也能夠直接依賴於 Service 層。

個人理解

《軟件架構模式》一書中介紹了分層架構思想:

分層架構是一種很常見的架構模式,它也被叫作 N 層架構。這種架構是大多數 Java EE 應用的實際標準。許多傳統 IT 公司的組織架構和分層模式十分的類似,因此它很天然地成爲大多數應用的架構模式。

分層架構模式裏的組件被分紅幾個平行的層次,每一層都表明了應用的一個功能(展現邏輯或者業務邏輯)。儘管分層架構沒有規定自身要分紅幾層幾種,大多數的結構都分紅四個層次,即展現層、業務層、持久層和數據庫層。業務層和持久層有時候能夠合併成單獨的一個業務層,尤爲是持久層的邏輯綁定在業務層的組件當中。所以,有一些小的應用可能只有三層,一些有着更復雜的業務的大應用可能有五層甚至更多的層。

分層架構中的每一層都有着特定的角色和職能。舉個例子,展現層負責全部的界面展現以及交互邏輯,業務層負責處理請求對應的業務。架構裏的層次是具體工做的高度抽象,它們都是爲了實現某種特定的業務請求。好比說展現層並不關心如何獲得用戶數據,它只需在屏幕上以特定的格式展現信息。業務層並不關心要展現在屏幕上的用戶數據格式,也不關心這些用戶數據從哪裏來,它只須要從持久層獲得數據,執行與數據有關的相應業務邏輯,而後把這些信息傳遞給展現層。

分層架構的一個突出特性地組件間關注點分離。一個層中的組件只會處理本層的邏輯。好比說,展現層的組件只會處理展現邏輯,業務層中的組件只會去處理業務邏輯。由於有了組件分離設計方式,讓咱們更容易構造有效的角色和強力的模型,這樣應用變得更好開發、測試、管理和維護。

服務器

高併發服務器 time_wait

阿里推薦高併發服務器建議調小 TCP 協議的 time_wait 超時時間。

說明:操做系統默認 240 秒後纔會關閉處於 time_wait 狀態的鏈接,在高併發訪問下,服務器端會由於處於 time_wait 的鏈接數太多,可能沒法創建新的鏈接,因此須要在服務器上調小此等待值。

正例:在 Linux 服務器上經過變動/etc/sysctl.conf 文件去修改該缺省值(秒):net.ipv4.tcp_fin_timeout=30

個人理解

服務器在處理完客戶端的鏈接後,主動關閉,就會有 time_wait 狀態。TCP 鏈接是雙向的,因此在關閉鏈接的時候,兩個方向各自都須要關閉。先發 FIN 包的一方執行的是主動關閉,後發 FIN 包的一方執行的是被動關閉。主動關閉的一方會進入 time_wait 狀態,而且在此狀態停留兩倍的 MSL 時長。

主動關閉的一方收到被動關閉的一方發出的 FIN 包後,迴應 ACK 包,同時進入 time_wait 狀態,可是由於網絡緣由,主動關閉的一方發送的這個 ACK 包極可能延遲,從而觸發被動鏈接一方重傳 FIN 包。極端狀況下,這一去一回就是兩倍的 MSL 時長。若是主動關閉的一方跳過 time_wait 直接進入 closed,或者在 time_wait 停留的時長不足兩倍的 MSL,那麼當被動關閉的一方早於先發出的延遲包達到後,就可能出現相似下面的問題:

1. 舊的 TCP 鏈接已經不存在了,系統此時只能返回 RST 包

2. 新的 TCP 鏈接被創建起來了,延遲包可能干擾新的鏈接

無論是哪一種狀況都會讓 TCP 再也不可靠,因此 time_wait 狀態有存在的必要性。

修改 net.ipv4.tcp_fin_timeout 也就是修改了 MSL 參數。

結束語

本文主要介紹了異常日誌、安全規約、單元測試、MySQL 數據庫、工程結構等五部分關於編碼規約的要求。因爲篇幅有限,本專題僅提供上、下兩篇文章,所以本文(專題下篇)僅覆蓋了阿里代碼規範的少數內容,更多內容請諮詢本文做者。

參考資源

參考文檔《阿里巴巴 Java 開發手冊(又名阿里巴巴 Java 代碼規約)》。

參考書籍 《Effective Java Second Edition》Joshua Bloch。

原做者:周明耀
原文連接: 從代碼處理等方面解讀阿里巴巴 Java 代碼規範
原出處:IBM Developer

64669c7e6712295f10a8c0382d616e16.jpeg

相關文章
相關標籤/搜索