1. 程式人生 > >阿里巴巴-Java開發手冊-筆記小炒

阿里巴巴-Java開發手冊-筆記小炒

雙11買了本Java開發手冊,通讀了一遍,學習了較多規範問題,這些規範自己慢慢經歷可能需要較長一段時間。
這裡記下筆記,以後隨時也有的學習。

一致性很重要,無邊無際爭論的時間成本與最後的收益是成反比的。

  1. 縮排使用四個空格,不使用tab
  2. if單語句必須加大括號,換行
  3. 左大括號不換行

程式設計規約

以下程式設計規約

命名風格

強制部分

  1. 程式碼中的命名均不能以下畫線或美元符號開始,也不能以下畫線或美元符號結束
  2. 程式碼命名禁止使用拼音與英文混合的方式,更不能直接使用中文方式
  3. 類名使用UpperCamelCase風格,但DO/BO/DTO/VO/AO/PO等情形例外
  4. 方法名、引數名、成員變數、區域性變數都統一使用lowerCamelCase風格,必須遵從駝峰形式
  5. 常量名全部大寫,單詞用下畫線隔開,力求語義表達完整,不要嫌名字長
  6. 抽象類名使用Abstract或Base開頭;異常類名使用Exception結尾;測試類名以它要測試類名開始,以Test結尾
  7. 型別與中括號之間無空格相連定義陣列。例如String[] args
  8. POJO類中布林型別的變數都不要假is字首,否則部分框架解析會引起序列化錯誤。即deleted 而不是isDeleted
  9. 包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一用單數,但類名如果有複數含義,則用複數形式,例如MessageUtils
  10. 杜絕完全不規範的縮寫,避免詞不達意,例如AbstractClass寫成AbsClass
    ,而condition縮寫成condi
    推薦部分
  11. 為了達到程式碼,任何自定義變數命名時,使用盡量完整單詞來表達其意,不要隨意使用int a等隨意方式
  12. 如果模組、介面、類、方法使用了設計模式,應在命名時體現出具體模式,例如public class OrderFactory以及public class LoginProxypublic class ResourceObserver等。
  13. 介面類中的方法和屬性不要加任何修飾符號(public也不要加),保持程式碼簡潔性,並加上有效的Javadoc註釋。儘量不要在接口裡定義變數,如果一定要定義,必須是與介面方法相關的,並且是整個應用的基礎廠常量。
    例如
    正例
void commit();
String COMPANY = "alibaba";

反例

public abstract void commit();
  1. 介面和實現類命名規則:
  • 如果是Service和DAO類,基於SOA理念,暴露出來的服務一定是介面,內部實現類用Impl字尾與介面區別,例如CacheServiceImpl實現CacheService介面
  • 如果是形容能力的介面名稱,取對應的形容詞為介面名(通常是-able形式)。例如AbstractTranslator 實現 Translatable
  1. 列舉類名建議帶上Enum字尾,列舉成員需要全大寫,單詞間用下畫線隔開。列舉其實就是特殊的常量類,切構造方法被預設強制為私有。
  2. 各層命名規約:
  • Service/DAO層方法命名規約如下:
    • 獲取 單個物件的方法用get作為字首
    • 獲取多個物件方法用list作為字首
    • 獲取統計值方法用count作為字首
    • 插入方法用save/insert作為字首
    • 刪除的方法用remove/delete作為字首
    • 修改的方法用update作為字首
  • 領域模型命名規約如下:
    • 資料物件:xxxDO,xxx為資料表名
    • 資料傳輸物件:xxxDTO,xxx為業務領域相關名稱
    • 展示物件:xxxVO,xxx一般為網頁名稱
    • POJO是DO/DTO/BO/VO的統稱,禁止命名成xxxPOJO

常量定義

強制

  1. 不允許任何魔法值(即未經預先定義的常量)直接出現在程式碼中,例如
String key = "Id#taobao_"+tradeId;   //此處Id#taobao來的好隨意
cache.put(key, value);
  1. long或者Long初始賦值時,使用大寫的L,不能使用小寫的l。小寫的l容易跟數字1混淆。
    

推薦

  1. 不要使用一個常量類維護所有常量,要按照常量功能分類,分開維護,例如快取CacheConsts而配置ConfigConsts
  2. 常量服用層有5層:
  • 跨應用共享常量:放在二方庫中,通常在client.jar中的constant目錄下
  • 應用內共享常量:紡織在一方庫中,通常在子模組中的constant目錄下
  • 子工程內部共享常量:即在當前子工程的constant目錄下
  • 包內共享常量:即在當前包下單獨的constant目錄下
  • 類內共享常量:直接在類內部private static final定義
  1. 如果變數值僅在一個範圍內變化,則用enum型別來定義。
public enum SeasonEnum{
   SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
   int seq;
   SeasonEnum(int seq){
       this.seq = seq;
   }
}

程式碼格式

強制

  1. 大括號使用約定,如果大括號內為空,則簡潔的寫成{} 即可,不需要換行
  2. 左小括號和字元之間不能出現空格,右小括號和字元之間也不能出現空格。
  3. if /for /while /switch /do 等保留子與括號之間必須加空格
  4. 任何二目、三目運算子的左右兩邊都需要加一個空格,例如=&&、加減乘除等
  5. 採用四個空格縮排,禁止使用Tab控制符
  6. 註釋的雙鞋線與註釋內容之間有且僅有一個空格
  7. 單行字元數不超過120個,超出則需要換行
  • 第二行相對於第一行縮排四個空格,從第三行開始,不再持續縮排
  • 運算子與下文一起換行
  • 方法呼叫的點符號與下文一起換行
  • 方法呼叫中多個引數需要換行時,在逗號後進行
  • 在括號前不要換行
  1. 方法引數在定義和傳入時,多個引數逗號後必須加空格
  2. IDE的text file encoding設定為UTF-8,IDE中檔案換行符使用UNIX格式,不要使用Windows格式
  3. 沒必要增加若干空格來使某一行的字元和上一行對應位置的字元對齊
  4. 不同邏輯、不同語義、不同業務程式碼之間插入一個空行分割開,以提升可讀性,沒必要插入多行

OOP規約

  1. 通過避免一個類的物件引用訪問此類的靜態變數或靜態方法,造成無謂編譯器解析成本,直接用類名訪問即可
  2. 所有覆寫方法,必須加@Override註解
  3. 相同引數型別,相同業務含義,才可以使用可變引數,避免使用Object
  4. 對外部正在呼叫或者二方庫依賴的介面,不允許修改方法簽名,以避免對介面呼叫方產生影響。若介面過時,必須加@Deprecated註解,並清晰說明採用的新介面或者新服務是什麼。
  5. 不能使用過時的類或方法
  6. Object的equals方法容易丟擲空指標異常,應該使用常量或者有值物件來呼叫equals
"test".equals(object)    // 推薦
object.equals("test")    // 不推薦,反例
  1. 所有相同的包裝類物件之間值的比較,全部使用equals方法
  2. 關於基本型別與包裝類使用標準:
  • 所有的POJO類屬性必須使用包裝資料型別(資料庫查出null,基本型別會有接受NPE問題)
  • RPC方法呼叫返回值和引數必須使用包裝型別資料
  • 區域性變數使用基本資料型別
  1. 在DO/DTO/VO等POJO類時,不要設定任何屬性預設值
  2. 當序列化類新增屬性時,請不要修改serialVersionUID 屬性,以避免反序列化失敗;如果完全不相容升級,避免反序列化混亂,那麼請修改serialVersionUID
  3. 構造方法裡面禁止加入任何業務邏輯,如果有初始化邏輯,請放在init方法中
  4. POJO類必須寫toString方法,如果繼承了另一個POJO類,注意在前面加一下super.toString()
    推薦
  5. 當使用索引訪問Stringsplit方法得到的陣列時,需要在最後一個分隔符後做有無內容的檢查,否則會有丟擲IndexOutOfBoundsException 的風險。
  6. 當一個類有多個構造方法,或者多個同名方法時,這些方法應該按順序放置在一起,便於閱讀
  7. 類內定義方法的順序:公有方法或保護方法>私有方法>getter/setter方法
  8. 在setter方法中,引數名稱與類成員變數名稱一致,使用this賦值。在getter/setter方法中,不要增加業務邏輯,否則會增加排查問題難度
  9. 在迴圈體內,字串的連線方式使用StringBuilderappend方法進行擴充套件。
    以下方式中,在反編譯出的位元組碼檔案顯示每次迴圈都會new出一個StringBuilder物件,然後進行append操作,最後通過toString返回,造成資源浪費
String str = "start";
for (int i = 0; i < 100; i++) {
    str = str + "hello";
} 
  1. final可以宣告類、成員變數 、方法及本地變數,下列情況使用final關鍵字:
  • 不允許被繼承的類,例如String
  • 不允許被修改的引用的域物件,比如POJO的域物件
  • 不允許被重寫的方法,例如POJO的setter方法
  • 不允許執行過程中重新賦值的變數
  • 避免上下文重複使用一個變數,使用final描述可以強制重新定義一個變數,方便更好的重構
  1. 類方法與成員控制從嚴:
  • 如果不允許外部直接通過new來建立物件,那麼構造方法必須為private
  • 工具類不允許有public或default構造方法
  • 類非static成員變數並且與子類共享,必須限制為protected
  • 成員變數(或static成員變數)如果僅為本類使用,必須限制為private
  • 只對內部呼叫,必須限制為private,類成員方法只對繼承類公開,限制為protected
    對任何類、方法、引數、變數,嚴控訪問範圍。過於寬泛的訪問範圍,不利於模組解藕。思考,如果是一個private的方法,想刪除就刪除;可如果是一個public的service方法,或者一個public的成員變數,刪除時手心不得冒點汗嗎?變數像自己的小孩,應儘量讓它在自己的視線範圍內。作用於太大,如果任其無限制到處跑,你會擔心的。

集合處理

  1. 關於hashCode和equals的處理,遵循:
  • 只要重寫equals,必須重寫hashCode
  • 因為Set儲存的是不重複物件,依據hashCode和equals進行判斷,所以Set儲存物件必須重寫這兩種方法
  • 如果自定義物件作為Map的鍵,那麼必須重寫hashCode和equals方法
  1. ArrayList的subList結果不可強轉成ArrayList,否則會丟擲ClassCastException異常。
    subList返回的是ArrayList的內部類SubList,並不是ArrayList,而是ArrayList的一個檢視,對於SubList子列表的所有操作,都最終會反映到原列表上
  2. 在subList場景中,高度注意對原集合元素個數的修改,會導致子列表的遍歷、增加、刪除均產生ConcurrentModificationException異常
  3. 使用集合轉陣列的方法,必須使用集合的toArray(T[] array)傳入型別完全一樣的陣列,大小就是list.size()。
    當使用toArray帶參方法,入參分配的陣列空間不夠大時,toArray方法內部將重新分配記憶體空間,並返回新陣列地址;如果陣列元素大於實際所需,下標[list.size()]的陣列元素將置為null,其他則保持原值,因此最好初啊入與集合大小一致的陣列。
  4. 在使用Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear方法會丟擲UnsupportedOperationException
    asList 的返回物件是一個Arrays內部類,並沒有實現集合的修改方法,體現的是介面卡模式,只是轉化介面,後臺仍是陣列。
  5. 泛型萬用字元<? extends T>用來接受返回的資料,此寫法不能使用add方法,而<? super T>不能使用get方法,因為作為其介面賦值時,容易出錯。
    根據PECS(Producer Extends Consumer Super)原則:第一,頻繁網外讀取內容的,適<? extends T>;第二,經常往裡插入,適用<? super T>
  6. 不要在foreach迴圈裡使用remove/add操作。remove元素請使用Iterator方式,如果併發操作,需要對Iterator物件加鎖
  7. 在JDK7即以上版本中,Comparator要滿足以下三個條件,否則Array.sort,Collections.sort會報IllegalArgumentException
  • x, y的比較和y,x的比較結果相反
  • x>y,y>z,則x>z
  • x=y,則x,z比較結果和y,z比較結果相同
    **推薦
  1. 在集合初始化時,指定集合初始值大小
  2. 使用entrySet遍歷Map類集合K/V,而不是用keySet方式遍歷。
    keySet其實遍歷2次,一次轉為Iterator物件,另一次是從hashMap中去除key所對應的value,如果是JDK8,則使用Map.foreach方法。
  3. 高度注意Map類集合K/V能不能儲存null值的情況
集合類 Key Value Super 說明
HashTable 不允許為null 不允許為null Dictionary 執行緒安全
ConcurrentHashMap 不允許為null 不允許為null AbstractMap 鎖分段,Java8使用CAS
TreeMap 不允許為null 允許為null AbstractMap 執行緒不安全
HashTable 允許為null 允許為null Dictionary 執行緒不安全
  1. 合理利用集合的有序性(sort)和穩定性(order),避免集合的無序和不穩定性帶來影響。
  • ArrayList是order/susort
  • HashMap是unorder/unsort
  • TreeSet是order/sort
  1. 利用Set元素的唯一特性,可以快速對一個集合進行去重操作,避免使用List的contains方法進行遍歷,對比去重操作。

併發處理

  1. 獲取單例物件需要保證執行緒安全,其中的方法也要保證執行緒安全。
    資源驅動類,工具類,單例工廠類都需要注意
  2. 在建立執行緒或執行緒池時,請指定有意義名稱,方便出錯時回溯
  3. 執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示建立執行緒
    在使用執行緒池的好處是減少在建立和銷燬執行緒上所消耗的時間以及系統資源,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題
  4. 執行緒池不允許使用Executors建立,而是通過ThreadPoolExecutor的方式建立,這樣的處理方式能讓編寫程式碼的工程師更加明確執行緒池的執行規則,避免資源耗盡的風險。
    Executors返回的執行緒池的物件弊端如下:
  • FixedThreadPool和SingleThreadPool,允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,從而導致OOM。
  • CachedThreadPool和ScheduledThreadPool;,允許建立的執行緒數量為Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致OOM
  1. SimpleDateFormat是不安全的類,一般不要定義為static變數,如果定義為static,必須加鎖,或者使用DateUtils工具類。
    如果是JAVA8的應用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat。
  2. 在高併發場景中,同步呼叫應該去考慮鎖的效能損耗。能用無鎖資料結構,就不要用鎖,能鎖區塊,就不要鎖整個方法體,能用物件鎖,就不要用類鎖
  3. 在對多個資源,資料庫表,物件同時加鎖時,需要保持一致的加鎖順序,否則肯呢個會造成死鎖。
  4. 在併發修改同一記錄時,為避免更新丟失,需要加鎖。要麼在應用層加索,要麼在快取層加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新標誌。
    如果每次訪問,衝突概率小於20%,推薦使用樂觀鎖,否則使用悲觀鎖,樂觀鎖的重試次數不得小於3次。
  5. 對於多執行緒並行處理定時任務的情況,在Timer執行多個TimeTask時,只要其中之一沒有捕獲丟擲異常,其他任務便會自動終止執行。如果在處理定時任務時,使用ScheduledExecutorService,則沒有這個問題。
  6. 使用CountDownLatch進行非同步轉同步操作,每個執行緒退出前必須呼叫countDown方法,執行緒執行程式碼注意catch異常,確保countDown方法被執行,避免主執行緒無法執行至await方法,知道超時才返回結果。
    子執行緒丟擲異常堆疊,不能在主執行緒try-catch到
  7. 避免Random例項被多執行緒使用,雖然該共享例項是執行緒安全的,但會因競爭同一seed導致效能下降。
    此處的Random包括java.util.RandomMatch.random()方式,在JDK7後,可以直接使用API ThreadLocalRandom。
  8. 在併發場景下,通過雙重檢查鎖(double-checked-locking)實現延遲初始化的優化問題隱患,推薦解決中較為簡單一種,即目標屬性宣告為volatile型。
  9. volatile解決多執行緒記憶體不可見問題,對於一寫多讀,可以解決變數同步問題,但是如果多寫,同樣無法解決執行緒安全問題,例如count++。推薦使用AtomicInteger(Atomic*)實現,如果是JDK8,推薦使用LongAdder物件,他比AtomicLong效能更好(減少樂觀鎖的重試次數)
  10. HashMap在容量不夠進行resize時,由於高併發可能出現死鏈,導致cpu佔用飆升,在開發過程中可以使用其他資料結構或加鎖來規避此風險
  11. ThreadLocal無法解決共享物件的更新問題,ThreadLocal物件建議使用static修飾,這個變數是針對一個執行緒內所有操作共享的,所以設定為靜態變數,所有例項共享此靜態變數,也就是說,在類第一次被使用時,只分配一塊儲存空間,所此類的物件(只要是這個執行緒內定義的)都可以操作這個變數。

控制語句

  1. 在一個switch塊內,每個case要麼通過break/return重要,要麼註釋說明程式講繼續執行到哪一個case為止;在一個switch塊內,都必須包含一個default語句並且放在最後,即使它什麼程式碼也沒有。
  2. 在if/else/for/while/do語句中,必須使用大括號,即使只有一行程式碼,也應避免採用單行的編碼方式:if (condition) statements
  3. 在高併發場景中,避免使用“等於”判斷作為中斷或者退出的條件。
    如果併發控制沒有處理好,容易產生等值判斷被“擊穿”的情況,應使用大於或小於的區間判斷條件來代替
  4. 在表達異常的分支時,儘量少使用if - else方式,這種方式可以改寫成:
if (condition) {
    return;
}
  1. 除常用方法(如getXxx/ isXxx)外,不要在條件判斷中執行其他複雜語句,可以講複雜的邏輯變數賦值給一個有意義的布林變數名,以提高可讀性。
    例如:
final boolean existed = (file.open(name, "w") != null && (...) || (...));
if (existed) {
    ...
}
  1. 迴圈體中的語句要考量效能,以下操作儘量移至迴圈體外處理,如定義物件或效能,獲取資料庫連線,避免進行不必要的try-catch操作
  2. 避免採用去翻邏輯運算子
  3. 介面入參保護,這種場景常見的用作批量操作的介面。
  4. 下列情形,需要進行引數校驗:
  • 呼叫頻次低的方法。
  • 執行時間開銷很大的方法。此情形中,引數校驗時間幾乎可以忽略不計,但如果因為引數錯誤導致中間執行回退,或者錯誤,那得不償失。
  • 需要極高穩定性和可用性的方法
  • 對外提供的開放介面,不管是否為RPC/API/HTTP介面。
  • 敏感許可權入口。
  1. 下列情形,不需要進行引數校驗
  • 極有可能被迴圈呼叫的方法,但是方法說明裡必須著名外部引數檢查要求
  • 底層呼叫頻度比較高的方法,畢竟是像純淨水過濾的最後一道,引數錯誤不太可能到底層才暴露問題,一般DAO層與Service層在同一應用中,並部署在同一臺伺服器中,所以DAO的引數校驗可以忽略
  • 被宣告成private只會被自己程式碼所呼叫的方法,如果能夠確定呼叫方法的程式碼傳入引數已經做過檢查或者肯定不會有問題,可以不校驗引數。

註釋規約

  1. 類、類甦醒,類方法的註釋必須用Javadoc規範,使用 /**內從*/,不得使用// xxx方式
  2. 所有抽象方法(包括介面中的方法)必須要用Javadoc註釋,除了返回值,方法,異常說明外,還必須指出該方法做什麼事,實現什麼功能
  3. 所有類都必須新增建立者和建立日期
  4. 方法內部的單行註釋,在被註釋語句上方另起一行,使用//註釋,方法內部的多行註釋,使用/**/註釋,對齊
  5. 所有列舉型別欄位必須有解釋,說明每個資料項的用途。
  6. 與其使用“半吊子”英文來註釋,不如使用中文註釋把問題說清楚。專業名詞與關鍵字保持英文
  7. 在修改程式碼的同時,要對註釋進行相應修改,尤其是引數,返回值,異常,和性邏輯等修改。
  8. 謹慎註釋掉程式碼,要在上方詳細說明,而不是簡單的註釋掉,如果無用,請刪除!
    程式碼註釋有兩種可能:
  • 後續會恢復此段程式碼邏輯
  • 永久不用
  1. 對於註釋程式碼的要求:
  • 能夠準確反映設計思想和程式碼邏輯
  • 能夠描述業務含義,是其他程式設計師能夠迅速瞭解程式碼背後的資訊
  1. 好的命名、程式碼結構是自解釋的,註釋力求精簡準確
  2. 特殊註釋標記,請註明標記人與標記事件。注意及時處理這些標記,通過標記掃描經常清理此類標記。有時候線上故障就來源於這些標記處的程式碼:
  • 代辦事宜 (TODO):(標記人,標記時間,[預計處理時間])表示需要實現,但是目前實現的功能。這些實際上是一個Javadoc的標籤,雖然目前的Javadoc還沒有實現,但已經廣泛使用,且只能應用於類、介面和方法(因為它還是一個Javadoc)標籤
  • 錯誤,不能工作(FIXME):(標記人,標記事件,[預計處理事件])在註釋中使用FIXME標記某程式碼是錯誤的,而且不能工作,需要及時糾正。

其他

  1. 在使用正則表示式時,利用好其預編譯功能呢個,可以有效加快正則匹配速度。
    不要在方法體內定義:
Pattern pattern = Pattern.compile(規則);
  1. 在velocity呼叫POJO類的屬性時,建議直接使用屬性名取值,模板引擎會自動按規範呼叫POJO的getXxx(),如果是boolean基本資料型別(boolean命名不需要加is字首),會自動呼叫siXxx()方法。
  2. 後臺輸送給頁面的變數必須加$!{var}–中間是感嘆號。
    如果var=null或者不存在,那麼${var}會直接顯示也頁面上
  3. 注意Math.random()這個方法返回的是double型別,取值的範圍0<=x<1(能夠取到零值,注意零異常),如果想獲取整數型別的隨機數,不要講x放大10的若干倍然後取整,直接使用Random物件的nextInt或者nextLong方法。
  4. 獲取當前毫秒數用System.currentTimeMillis();而不是用new Date().getTime();
    如果想獲取更加精確的納秒級時間值,則應使用System.nanoTime()的方式,在JDK8中,正對統計時間等場景,推薦使用Instant
  5. 不要在檢視模板中加入任何的複雜的邏輯
  6. 任何資料結構的構造或者初始化,都應該指定大小,避免因資料結構無限增長而耗盡記憶體
  7. 及時清理不再使用的程式碼段或配置資訊。
    對於暫時被註釋掉,後續可能恢復的程式碼段,在註釋程式碼上方,統一規定使用三個斜槓(///)來說明被註釋掉的理由

異常日誌

異常處理是大部分程式設計師積攢多年的痛點。

異常處理

  1. Java類庫中定義的可以通過預檢查方式規避的RuntimeException不應該通過catch方式來處理,例如IndexOutOfBoundsExecptionNullPointerException
    無法通過預檢查的異常不在此列,例如NumberFormatException
  2. 異常不要用來做流程控制,條件控制,例如通過捕獲資料庫丟擲唯一值衝突,來判斷值是否唯一
  3. catch時請分清穩定程式碼和非穩定程式碼,穩定程式碼指的是無論如何都uhui錯誤的程式碼,對於非穩定程式碼的catch,儘可能在異常型別的區分後,再作出對應的異常處理。
  4. 捕獲異常是為了處理它,不要捕獲了卻什麼都不處理而拋棄它,如果不像處理它,請將該異常跑給它的呼叫者。最外層的業務程式碼使用者必須處理異常,將其轉化為使用者可以理解的內容。
  5. 有try塊放到了食物程式碼中,catch異常後,如果需要回滾事務,一定要注意使用rollback事物。
  6. finally塊必須對資源物件,流物件進行關閉操作,如果有異常也要做try-catch操作。
    對於JDK7以上版本,可以使用try-with-resources方式
  7. 不能在finally塊中使用return
  8. 捕獲異常與拋異常必須完全匹配,或者捕獲異常是拋異常的父類。
  9. 方法的返回值可以為null,不強制返回空集合或者空物件,必須添加註釋充分說明在什麼情況下會返回null值,呼叫方需要進行null判斷以防止NPE操作。
  10. 防止NPE,是程式設計師的基本修養,注意NPE產生的場景:
  • 當返回型別為基本資料型別,return包裝資料型別的物件時,自動拆箱有可能產生NPE
public int add () {
   return Integer(xx);    // 如果為null,自動拆箱NPE
}
  • 資料庫查詢結果可能為null
  • 集合裡的元素即使isNotEmpty,取出元素也可能為null。
  • 遠端呼叫返回物件時,一律要求進行空指標判斷,以防止NPE
  • 對與Session中獲取的資料,建議進行NPE檢查,以免避免空指標
  • 級聯呼叫obj.getA().getB().getC();的一串呼叫,易產生NPE
  1. 定義時區unchecked/checked異常,避免直接丟擲new RuntimeException(),更不允許丟擲Exception或者Throwable,應使用有業務定義的異常,推薦業界已定義過的或者自定義的異常,如DAOException/ ServiceException
  2. 對於公司外的HTTP/API開放介面必須使用“錯誤碼”;應用內部推薦異常丟擲;跨應用RPC呼叫優先考慮使用Result方式,封裝isSuccess()方法、錯誤碼、錯誤簡簡訊息等。
  3. 避免出現重複的程式碼(Don’t Repeat Yourself),即DRY原則。

日誌規約

  1. 應用中不可直接使用日誌系統中的API(Log4j,Logback),而應依賴使用日誌框架SLF4J中的API,使用門面模式的日誌框架,有利於維護和各個日誌的日誌處理方式統一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(Abc.class);
  1. 日誌檔案推薦至少儲存15天,因為有些異常具備以周為頻次發生的特點。
  2. 應用中的擴充套件日誌(如打點、臨時監控、訪問日誌)等命名方式:appName_logType_logName.log
    logType為日誌型別,推薦分類有status/monitor/visit等;
    logName為日誌描述:通過檔名就知道日誌屬於哪個應用,那種型別,有什麼目的,這也有利於歸類查詢
  3. trace/debug/info級別的日誌,必須使用條件輸出形式或者佔位符的方式
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);如果日誌級別是warn,上述日誌不會列印,但會執行字串拼接操作,如果symbol是物件,會執行toString方法,浪費了系統資源,但日誌卻沒有被列印。
logger.debug("Processing trade with id: {} and symbol : {}", id, symbol);
  1. 避免重複列印日誌,否則會浪費磁碟空間。務必設定additivity=false;
  2. 異常資訊應該包括兩類:案發現場資訊和異常堆疊資訊,如果不處理,那麼通過throws往上丟擲
  3. 謹慎記錄日誌。生產環境禁止輸出deug日誌;
    有選擇的輸出info日誌;
    如果使用warn記錄剛上線的業務行為資訊,一定要注意日誌輸出量的問題,避免把伺服器磁碟撐爆,並及時刪除這些觀察日誌
  4. 可以使用warn日誌級別記錄使用者輸入引數錯誤情況,避免使用者投訴時無所適從。
    如非必要,請不要在此場景中打出error級別,注意日誌輸出的級別,error級別只記錄系統邏輯出錯,異常等重要錯誤資訊。

單元測試

本章首次提出AIR原則和BCDE原則來進行相應的衡量。

  1. 好的單元測試應該遵守AIR原則:
  • Automatic(自動化)
  • Independent(獨立性)
  • Repeatable(可重複)
  1. 單元測試應該是全自動執行的,並且是非互動式的。測試用例通常被定期執行,執行過程必須完全自動化才有意義。需要人工檢查輸出結果的測試不是一個好的單元測試。單元測試中不準使用System.out進行人肉驗證,必須使用assert驗證
  2. 保持單元測試的獨立性,為了保證單元測試穩定可靠且便於維護,單元測試用例之間絕不能相互呼叫,也不能以來執行的先後順序
  3. 單元測試是可以重複執行的,不能收到外界影響。
    為了不受外界環境影響,要求設計程式碼時,就把SUT的依賴改成注入,在測試時用Spring這樣的DI框架注入一個bending記憶體或者實現Mock實現
  4. 對於單元測試,要保證測試粒度足夠小,有助於精確定位問題,單元測試粒度至少是類級別,一般是方法級別
  5. 核心業務,核心應用,核心模組的增量程式碼確保單元測試通過
  6. 單元測試必須寫在src/test/java工程目錄下,不允許寫在業務程式碼目錄下
  7. 單元測試的基本目標:
  • 語句覆蓋率達到70%
  • 核心模組的語句覆蓋率和分支覆蓋率都要達到100%
    在工程規約的應用分層中提到的DAO層、Manager層、可重用度高的Service層,都應該進行單元測試
  1. 編寫單元測試程式碼應遵守BCDE原則,以保證被測試模組的交付質量
  • B:Border,邊界值測試,包括迴圈邊界、特殊取值、特殊時間點、資料順序等
  • C:Correct,正確的輸入,並得到預期結果
  • D:Design,與設計文件相結合,來編寫單元測試
  • E:Error,強制錯誤資訊輸入(如非法資料,異常流程,非業務允許輸入等),並得到預期結果
  1. 對於資料庫相關的查詢、更新、刪除操作,不能假設資料庫裡是存在的,或直接操作資料庫把資料插入進去,請使用程式插入或者匯入資料的方式來準備資料
  2. 和資料庫相關的單元測試,可以設定自動回滾機制,不給資料庫造成髒資料,或者對單元測試產生的資料有明確的前後綴標識。
    例如:在RDC內部單元測試中,使用RDC_UNIT_TEST的字首標識資料
  3. 對於不可測的程式碼建議做必要的重構,使程式碼變得可測,避免為達到測試要求而書寫不規範的測試程式碼
  4. 在設計評審階段,開發人員需要和測試人員一起確定單元測試範圍,單元測試最好覆蓋所有測試用例(UC)
  5. 單元測試作為一種質量保障手段,不建議專案釋出後補充單元測試用例,建議在專案提前測試完成單元測試。
  6. 為了更方便進行單元測試,業務程式碼應該避免出現以下情況:
  • 構造方法做的事情過多
  • 存在過多的全域性變數和靜態變數
  • 存在過多的外部依賴
  • 存在過多的條件語句
  1. 不要對單元測試有如下誤解:
  • 那是測試工程師乾的事情!
  • 單元測試程式碼是多餘的!汽車的整體功能和各單元部件的測試正常是否是強相關的
  • 單元測試程式碼不需要維護!
  • 單元測試與線上故障沒有辯證關係,好的單元測試能夠最大限度規避線上故障

安全規約

說明程式設計中需要注意比較基礎的安全準則

  1. 屬於使用者個人頁面或者功能必須進行許可權控制校驗
  2. 使用者敏感資料禁止直接展示,必須對展示內容進行脫敏
  3. 使用者輸入的sql引數嚴格使用引數繫結或者METADATA欄位限定,防止sql注入,禁止字串拼接sql訪問資料庫
  4. 使用者請求傳入的任何引數必須做有效性校驗
    忽略引數校驗可能導致如下情況:
  • page size 過大導致記憶體溢位
  • 惡意order by導致資料庫慢查詢
  • 任意重定向
  • sql注入
  • 反序列化注入
  • 正則輸入源拒絕服務ReDoS
  1. 禁止想HTML頁面輸出未經安全過濾或不正確轉義的使用者資料
  2. 表單、AJAX提交必須執行CSRF安全過濾
  3. 在使用平臺資源時,譬如端性,郵件,電話,下單支付,必須實現正確的放重放限制,如數量限制,疲勞度控制,驗證碼校驗,避免濫刷,資損
  4. 針對發帖,評論,傳送即時訊息等使用者生成內容的場景,必須實現防刷,文字內容違禁詞過濾等風控策略等。

MySQL資料庫

底層資料庫規範有助於減少軟體實現的複雜度,降低溝通成本,主要說明建表規範、索引優化準則以及ORM層的處理約定。

建表規約

  1. 表達是否的概念,必須使用is_xxx的方式命名,資料型別是unsigned tinyint(1表示是,0表示否)
    任何欄位如果是非負數,則必須是unsigned
    例如:表達邏輯刪除的欄位名為is_deleted,1表示刪除,0表示未刪除
  2. 表名、欄位名必須使用小寫字母或數字,禁止出現數字開頭,禁止兩個下畫線中間只出現數字,資料庫欄位名的修改代價很大,因為無法進行預釋出,所以欄位名需要慎重考慮。
    MySQL在windows下不區分大小寫,但是在Linux下預設區分大小寫,因此,資料庫名,表名,欄位名都不允許出現任何大寫字母,避免節外生枝。
  3. 表名不使用複數名詞
  4. 禁止使用保留子,如desc、range、match、delayed,請參考MySQL官方保留字
  5. 主鍵索引名為pk_欄位名(primary key),唯一索引名為uk_欄位名(unique_key),普通索引名為idx_欄位名(index)。
  6. 小數型別為decimal,禁止使用float和double。
    在儲存的時候,float和double存在精度損失問題,很可能在比較值的時候,得不到正確結果,如果儲存的資料範圍超過decimal的範圍,建議將資料拆成整數和小數分開儲存。
  7. 如果儲存的字串長度幾乎相等,應該使用char定長字串型別。
  8. varchar是可變長字串,不預先分配儲存空間,長度不要超過5000個字元,如果儲存長度大於此值,則應該定義型別為text,獨立出一張表,用主鍵來對應,避免影響其他欄位索引效率
  9. 表必備三欄位,id,gmt_create,gmt_modified。
    其中,id必為主鍵,型別為unsigned bigint,單表時自增,步長為1,gmt_create和gmt_modified的型別均為date_time型別,前者現在時表示主動建立,後者過去分詞標識被動更新
  10. 表的命名最好加上“業務名稱_表的作用”
  11. 庫名與應用名稱儘量一致
  12. 當修改欄位含義或對欄位標識狀態追加時,需要及時更新欄位註釋
  13. 欄位允許適當冗餘,以提高查詢效能,但必須考慮資料一致,冗餘欄位應該遵循:
  • 不是頻繁修改的欄位
  • 不是varchar超長欄位,更不能是text欄位
    例如:商品類目名稱使用頻率高,欄位長度短,名稱基本一成不變,可在相關聯表中冗餘儲存類目名稱,避免關聯查詢
  1. 當單錶行超過500萬行或者單表容量超過2GB時,才推薦進行分庫分表
    如果預計三年後的資料量無法達到這個級別,請不要在建立時就分庫分表
  2. 設定合適長度字元儲存長度,不但可以節約資料庫表空間和索引儲存,更重要是能夠提升檢索速度
物件 年齡區間 型別 位元組 表示範圍
150歲之間 unsigned tinyint 1 無符號值:0至255
數百歲 unsigned smallint 2 無符號值:0至65535
恐龍化石 數千萬年 unsigned int 4 無符號值:0至42.9億
太陽 約50億年 unsigned bigint 1 無符號值:0至1019

索引規約

  1. 業務上具有唯一特性的欄位,即使是多個欄位的組合,也必須建成唯一索引。
    不要以為唯一索引影響了insert速度,這個速度損耗可以忽略,但會明顯提高查詢速度;另外,即使在應用層做了非常完善的校驗控制,只要沒有唯一索引,根據墨菲定律,必然有髒資料產生
  2. 超過三個表禁止用join,需要join欄位,資料型別必須一致;當多表關聯查詢時,保證被關聯欄位需要有索引
    即使雙表進行join,也要注意表索引,sql效能
  3. 在varchar欄位建立索引,必須指定索引長度,沒必要對全欄位建立索引,根據實際文字區分度決定索引長度即可。
    索引長度與區分度是一對矛盾體,一般對字串型別資料,長度為20的索引,區分度會達到90%以上,可以使用count(distinct left(列名,索引長度)/count(*))的區分度來確定
  4. 頁面搜尋嚴禁左模糊或者全模糊,如需要請通過搜尋引擎來解決。
    索引檔案具有B-Tree的最左字首匹配特性,如果左邊值未確定,那麼無法使用此索引
  5. 如果有order by的場景,請注意利用索引的有序性,order by最後欄位是組合索引的一部分,並且放在索引組合順序的最後,避免出現file_sort情況,影響查詢效能。
    例如:
    where a=? and b=? order by c; 索引a_b_c可用
    where a>10 order by b索引a_b無法排序,其有效性無法利用
  6. 利用覆蓋索引進行查詢操作,避免回表
    例如:如果想知道一本書第11章是什麼標題,我們沒有必要翻開第11章對應那一頁嗎?只瀏覽一下目錄即可,這個目錄起到了覆蓋索引的作用。
    能夠建立索引的種類分為主鍵索引,唯一索引,普通索引,而覆蓋索引只是查詢的一種效果,用explain的結果,extra列會出現using index
  7. 利用延遲關聯或者子查詢優化超多分頁場景
    MySQL並不是跳過offset行,而是去offset+N行,然後返回放棄前offset行,返回N行,當offset特別大的時候,效率非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行SQL改寫。
    正例:先快速定位需要獲取的id段,然後再關聯:
    select a.欄位 from 表1 a, (select id from 表1 where 條件 limit 10000, 20) b where a.id = b.id
  8. SQL效能優化的目標,至少要達到range級別,要求是ref級別,最好是consts。
  • consts單表中最多隻有一個匹配行(主鍵或唯一索引),在優化階段即可讀取到資料
  • ref指使用普通的索引(normal index)
  • range對索引進行範圍檢索
  1. 建立組合索引時候,區分度最高的在最左邊。
    如果where a = ? and b = ?, a列幾乎接近於唯一值,那麼只需要單建idx_a 索引即可。
    如果存在非等號和等號混合的判斷條件,在建立索引時,請把等號條件列前置,入where a > ? and b=?,那麼即使a的區分度更高,也必須把b放在索引的最前列
  2. 應防止欄位型別不同造成的隱式轉換,導致索引失效
  3. 建立索引時,避免有如下極端誤解:
  • 寧濫勿缺,認為一個查詢就需要建立一個索引
  • 寧缺勿濫,認為索引會消耗空間,嚴重拖慢更新和新增的速度
  • 抵制唯一索引,認為業務的唯一性一律需要在應用層“先查後插”的方式解決

SQL語句

  1. 不要使用count(列名)count(常量)來替代count(*)count(*)是SQL92定義的標準統計行數的語法,跟資料庫無關,跟NULL和非NULL無關
    count(*)會統計值為NULL的行,而count(列名)不會統計此列為NULL值的行
  2. count(distinct col)計算該列除NULL外的不重複行數,注意,count(distinct col1, col2), 如果其中一列全為NULL,那麼即使另一列有不同的值,也返回0
  3. 當某一列的值全為NULL時,count(col) 返回結果為0,但sum(col)的返回結果為NULL,因此,使用sum()時需要注意NPE問題
    可以使用如下問題避免sum的NPE問題:
    select if(isnull(sum(g)),0,sum(g)) from table
  4. 使用isnull()判斷是否為null值。
  • null與任何值比較竇唯null
  • null<>null返回結果為null,而不是false
  • null=null返回結果為null,而不是true
  • null<>1的返回結果是null,而不是true
  1. 在程式碼中寫分頁查詢邏輯時,若count為0應直接返回,避免執行後面的分頁語句
  2. 不得使用外來鍵與級聯,一切外來鍵概念必須在應用層解決。
  3. 禁止使用儲存過程,儲存過程難以除錯和維護,更沒有移植性
  4. 資料訂正(特別是刪除、修改記錄操作),要首先select,避免出現誤刪除,確認無誤後才能執行更新語句
  5. in操作能避免則避免,若實在避免不了,需仔細確認評估in後面元素數量,控制在1000以內。
  6. 如果有全球化需要,所有的字元儲存與表示,均以utf-8編碼,注意字元統計函式的區別。
    select length(“輕鬆工作”); 返回12
    select character_length(“輕鬆工作”); 返回為4。
    如果需要儲存表情,那麼選擇utf8mb4進行儲存,注意它與utf-8編碼的區別
    mysql字符集 utf8 和utf8mb4 的區別
  7. truncate table 比delete快,且使用的系統和事務日誌資源少,但truncate 無事物且不觸發trigger,有可能造成事故,故不建議在開發程式碼中使用此語句。

ORM對映

  1. 在表查詢中,一律不要使用* 作為查詢欄位裡誒嗯,需要哪些欄位明確寫出
  • 增加查詢分析器解析成本
  • 增減欄位容易與resultMap不一致
  1. POJO類的布林屬性不能加is,而資料庫欄位必須加is_,要求在resultMap中進行欄位名與屬性之間的對映
    如果使用MyBatisGenerator生成程式碼,則需要進行相應修改
  2. 不要使用resultClass作為返回引數,即使所有類屬性名與資料庫欄位一一對應,也需要定義,反過來,每一個表必然有一個屬性與之對應。
    配置對映關係,使欄位與DO類解藕,方便維護
  3. sql.xml配置引數使用#{},#param#,不要使用${},此方式容易出現sql注入
  4. iBATIS自帶的queryForList(String statementName, int start, int size) 不推薦使用
    其實現方式是在資料庫取到statementName對應的SQL語句的所有記錄,再通過subList取start,size子集合
  5. 不允許直接拿HashMap與HashTable作為查詢結果集的輸出
    resultClass=“HashTable”,會置入欄位名和屬性值,但是值的型別不可控
  6. 更新資料表記錄時,必須同時更新記錄對應的修改時間,即gmt_modified欄位值為當前事件
  7. 不要寫一個大而全的資料更新介面,傳入POJO類,如果不管是否為自己的目標更新欄位都進行update 操作,是不對的。執行SQL時,不要更新無改動的欄位,一是容易出錯,而是效率低,三是會增加binlog儲存。
  8. @Transactional 事務不要濫用,事物會影響資料庫的QPS,另外,使用事物的地方需要考慮各方面的回滾方案,包括快取回滾,搜尋引擎回滾,訊息補償,統計修正等。
  9. <isEqual> 中的compareValue是與屬性值對比的常量,表示相等時執行的SQL語句;isNotEmpty表示不為空且不為null時執行;isNotNull表示null值時執行。

工程結構

說明應用工程分層思想,二方庫約定及基本的伺服器知識

  1. 圖6-1中預設上層依賴於下層,箭頭關係標識可直接依賴,如開放介面層依賴於Web層,也可以直接依賴於Service層,以此類推 。
    圖6-1
  2. (分層異常處理規約)在DAO層,產生的異常型別有很多,無法細粒度的異常執行catch,使用catch (Exception e)方式,並throw new DAOException(e)。不需要列印日誌,因為日誌在Manager/Service層,一定需要捕獲並寫到日誌檔案中去,如果同太伺服器再寫日誌,會浪費效能和儲存。在Service層出現異常時,必須講出錯日誌記錄到磁碟,儘可能帶上引數資訊,相當於保護案發現場。如果Manager層與Service同機部署,則日誌與DAO層處理一致,如果是單獨部署,則採用與Service層一致的處理方式。Web層絕不應該繼續網上拋異常,因為已經處於頂層,如果意識到該異常將導致頁面無法正常渲染,則應該直接跳到友好的錯誤頁面,加上使用者容易理解的錯誤提示資訊。開放介面層需要講異常處理成錯誤碼和錯誤資訊返回。
  3. 分層領域模型規約:
  • DO (Data Object):與資料庫表結構一一對應,通過DAO層向上傳輸資料來源物件。
  • DTO(Data Transfer Object):資料傳輸物件,Service 或Manager向外傳輸物件
  • BO(Business Object):業務物件,有Service層輸出的封裝業務邏輯物件
  • AO(Application Object):應用物件。在Web層與Service層之間抽象的複用物件模型,極為貼近展示層,複用度不高。
  • VO(View Object):顯示層物件,通常Web向模板渲染層傳輸的物件。
  • Query:資料查詢物件,各層次接收上層的查詢請求,注意,如果是超過兩個引數的查詢封裝,則禁止使用Map類來傳輸

二方庫依賴

  1. 定義GAV遵從以下準則:
  • GroupID格式:com.{公司/BU}.業務線.[子業務線],最多四級
  • ArtifactID格式:產品線名-模組名。語義不重複不
  • Version:
  1. 二方庫版本號命名格式:主機板本號.次版本號.修訂版本號
  • 主版本號:產品方向改變,或者大規模API不相容,或者架構不相容升級
  • 次版本號:保持相對相容性,增加主要功能特性,影響範圍極小的API不相容修改
  • 修訂號:保持完全的相容性,修改Bug,新增次要功能特性等
    注意,起始版本號必須為1.0.0,而不是0.0.1。正式釋出的類庫必須先去中央倉庫進行查證,使版本號有延續性,正式版本號不允許覆蓋升級。例如當前版本號:1.3.3,那麼下一個合理版本號1.3.4或者1.4.0或者2.0.0
  1. 線上應用不要依賴SNAPSHOT版本(安全包除外)。
    不依賴SNAPSHOT版本是保證應用釋出的冪等性。另外,也可以加快編譯時的打包構建
  2. 二方庫的新增或者升級,保持除功能點外的其他jar包仲裁結果不變。如果有改變,則必須明確評估和驗證,建議進行dependency:resolve前後資訊比對。如果仲裁結果完全不一致,則通過dependency:tree命令找出差異點,進行<excludes>排除jar包。
  3. 二方庫裡可以定義列舉型別,型別可以使用列舉型別,但是介面返回值不允許使用列舉型別或者包含列舉型別的POJO物件。
  4. 依賴於一個二方庫群時,必須定義一個統一的版本變數,避免版本不一致。
  5. 禁止在子專案的pom依賴中出現相同的GroupId,相同的ArtifactId,但是不同的Version。
    在本地除錯時會使用各子專案指定的版本號,但是若合併成一個war,則只能有一個版本號出現在最後的lib目錄中,可能會出現線下除錯是正確的,釋出到線上缺出故障的問題。
  6. 所有pom檔案中的依賴宣告放在dependencies語句塊中,所有版本仲裁放在dependencyManagement語句塊中。
    dependencyManagement裡只是宣告版本,並不實現引入,因此子專案需要顯式的宣告依賴,version和scope都取自父pom。而dependencies所有宣告在主pom的dependencies裡的依賴都會自動引入,並預設被所有的子專案繼承。
  7. 二方庫不要有配置項,最低限度不要再增加配置項。
  8. 為避免應用二方庫的依賴衝突問題,二方庫釋出者應當遵循以下原則:
  • 精簡可控原則。移除一切不必要的API依賴,只包含Service API,必要領域模型物件,Utils類,常量、列舉等。如果依賴其他二方庫,儘量provided引入,讓二方庫使用者依賴具體版本號;無log具體實現
  • 穩定可追溯原則。每個版本的變化都應該被記錄,二方庫版本由誰維護,原始碼在哪裡,都需要能方便的查到。除非使用者主動升級版本,否則公共二方庫的行為不應該發生變化。

伺服器

  1. 併發伺服器建議調笑TCP協議的time_wait超時事件。
    作業系統預設240s後,才會關閉time_wait狀態的連線。在高併發訪問下,伺服器會因為處於time_wait的連線數過多,而無法建立新的連線,所以需要在伺服器上調小此等待值。
    在linux伺服器上可通過變更/etc/sysctl.conf檔案去修改預設值(s)。
    net.ipv4.tcp_fin_timeout=30
  2. 調大伺服器所支援的最大檔案控制代碼數(FileDescriptor,簡寫為fd)。
    主流作業系統的設計是將TCP/UDP連線採用與檔案一樣的方式管理,即一個連線對應於一個fd。主流的Linux伺服器預設支援最大fd數量為1024,當併發連線數很大時很容易因為fd不足而“open too many files”錯誤,導致新的連線無法建立。建議架將linux伺服器所支援的最大控制代碼調高數倍(與伺服器的記憶體數量相關)
  3. 給JVM設定 -XX:+HeapDumpOnOutOfMemoryError引數,讓JVM碰到OOM場景時輸出dump資訊。
    OOM的發生是有概率的,甚至有規律地相隔數月才出現一例,出現時現場資訊對查錯非常有價值
  4. 在線上生產環境,JVM的Xms和Xmx設定一樣大小的記憶體容量,避免在GC後調整堆大小帶來的壓力。
  5. 伺服器內部重定向使用forward;外部重定向地址使用URL拼裝工具類來生成,否則會帶來URL維護不一致的問題和潛在的安全風險

設計規約

說明軟體設計過程UML設計準則以及基本交狗理念

  1. 儲存方案和底層資料結構的設計獲得評審一致通過,並沉澱為文件。
  • 有缺陷的底層資料結構容易導致系統風險高、可擴充套件性低,重構成本因歷史資料遷移、系統平滑過度的需要也會陡然增加。所以,對儲存方案和資料結構需要進行認真的設計和評審,生產環境提交執行後,團隊成員需要進行double check。
  1. 在需求分析階段,如果與系統互動的User超過1個類,並且相關的User Case超過5個,那麼使用用例圖來表達結構化需求會更加清晰。
  2. 如果某個業務物件的狀態超過3個,那麼應使用狀態圖來表達並且明確狀態變化的各個觸發條件。
    狀態圖的核心是物件狀態,首先明確物件有多少種狀態,然後明確兩兩狀態之間是否存在直接轉換關係,再明確觸發狀態轉換的條件是什麼。
  3. 如果系統中某個功能的呼叫鏈路上設計的物件超過3個,則應該使用時序圖來表達並明確各個環節的輸入與輸出。
    時序圖反映了一系列物件間的互動與協作關係,可以清晰立體地反映系統的呼叫的縱深鏈路
  4. 如果系統中模型超過5個,並且存在複雜的依賴關係,則應該使用類圖來表達並明確類之間的關係
    類圖就像建築領域的施工圖,如果搭平房,可能不需要,但如果建造“螞蟻Z空間”大樓,則肯定需要詳細的施工圖
  5. 如果系統中超過2個物件之間存在協作關係,並且需要表示複雜的處理流程,則使用活動圖來表示。
    活動圖是流程圖的擴充套件,增加了能夠體現協作關係的物件泳道,支援並發表示等
  6. 考慮主幹功能的同時,需求分析與系統設計需要充分評估異常流程與業務邊界。
  7. 類在設計與實現要符合單一原則。
    單一原則是最易理解卻又最難實現的一條規則,隨著系統的演進,工程師很多時候會忘記類設計的初衷
  8. 謹慎使用繼承的方式進行擴充套件,優先使用聚合或組合的方式來實現。
    若一定要使用繼承,則必須符合裡式代換原則,即父類能夠出現,子類一定能夠出現。
  9. 在系統設計時,根據依賴倒置原則,儘量依賴抽象類與介面,有利擴充套件與維護
    低層次模組依賴於高層次模組的抽象,方便系統間的解藕
  10. 在設計系統時,注意對擴充套件開放,對修改閉合。
    在極端情況下,交付的程式碼是不修該的,同一業務域內的需求變化,應通過模組或類的擴充套件來實現
  11. 在系統設計階段,共性業務或公共行為抽取出來公共模組,公共配置,公共類,公共方法等,在系統中不應該出現重複程式碼的情況。
  12. 系統設計的摸底是明確需求,理順邏輯,後期維護,次要目的是用於指定編碼
    避免為了設計而設計,系統設計文件有助於後期的系統維護,所以設計結果需要進行分類歸檔儲存
  13. 設計的本質是識別和表達系統難點,並找到系統的變化點,並隔離變化點。
  14. 系統架構設計的目的:
  • 確定系統邊界,確定系統在技術層面上的做與不做
  • 確定系統內模組之間的關係。確定模組之間的依賴關係及模組的巨集觀輸入與輸出。
  • 確定指導後續設計與演化的原則。使後續的子系統或模組設計在一個規定的框架內繼續演化
  • 確定非功能性需求。非功能性需求是指安全性、可用性、可擴充套件性
  1. 避免發生如下誤解:敏捷開發 = 講故事 + 編碼 + 釋出
    敏捷開發是快速交付迭代可用的系統,省略多餘的設計方案,擯棄傳統的審批流程,但需要核心關鍵點上必要設計和文件沉澱。

專有名詞

  1. POJO(Plain Ordinary Java Object):在本手冊中,POJO專指只有setter/getter/toString的簡單類,包括DO/DTO/BO/VO
  2. GAV(GroupId、ArtifactId、Version):Maven座標,用來唯一標識jar包
  3. OOP(Object Oriented Programming):本開發手冊泛指類、物件的基本程式設計處理方式
  4. ORM(Object Relation Mapping):物件關係對映,物件領域模型與底層資料之間的轉換,本文泛指iBATIS,mybatis等框架
  5. NPE(java.lang.NullPointerException):即空指標異常
  6. SOA(Service-OrientedArchitecture):面向服務架構,它可以根據需求通過網路對鬆散耦合的粗粒度應用元件進行分散式部署、組合和使用,有利於提升元件可重用性和可維護性。
  7. 一方庫:本工程內部子專案模組依賴的庫(jar包)
  8. 二方庫:公司內部發布到中央倉庫,可供公司內部其他應用依賴的庫(jar包)
  9. 三方庫:公司之外的開源庫(jar包)
  10. IDE(Integrated Development Environment):用於提供程式開發環境的應用程式,一般包括程式碼編輯器、編譯器、偵錯程式和圖形使用者介面等工具。

來源 :

  1. 阿里巴巴-Java開發手冊