Java編程思想 註解總結

註解(也被稱爲元數據)爲我們在代碼中添加信息提供了一種形式化的方法 使我們可以在稍後某個時刻非常方便地使用這些數據

基本語法
在下面的例子中 使用@Test對testExecute()方法進行註解 該註解本身並不做任何事情 但是編譯器要確保在其構造路徑上必須有@Test註解的定義 程序員可以創建一個通過反射機制來運行testExecute()方法的工具
在這裏插入圖片描述

定義註解
下面就是前例中用到的註解@Test的定義 可以看到 註解的定義看起來很像接口的定義 事實上 與其他任何Java接口一樣 註解也將會編譯成class文件
在這裏插入圖片描述
除了@符號以外 @Test的定義很像一個空的接口 定義註解時 會需要一些元註解(meta annotation) 如@Target和Retention @Target用來定義你的註解將應用於什麼地方(例如是一個方法或者一個域) @Rectetion用來定義該註解在哪一個級別可用 在源代碼中(SOURCE) 類文件中(CLASS)或者運行時(RUNTIME)
在註解中 一般都會包含一些元素以表示某些值 當分析處理註解時 程序或工具可以利用這些值 註解的元素看起來就像接口的方法 唯一的區別是你可以爲其指定默認值
沒有元素的註解稱爲標記註解(meta annotation) 例如上例中的@Test
下面是一個簡單的註解 我們可以用它來跟蹤一個項目中的用例 如果一個方法或一組方法實現了某個用例的需求 那麼程序員可以爲此方法加上該註解 於是 項目經理通過計算已經實現的用例 就可以很好地掌控項目的進展 而如果要更新或修改系統的業務邏輯 則維護該項目的開發人員也可以很容易地在代碼中找到對應的用例
在這裏插入圖片描述

在下面的類中 有三個方法被註解爲用例
在這裏插入圖片描述

元註解
Java目前只內置了三種標準註解 以及四種元註解 元註解專職負責註解其他的註解
在這裏插入圖片描述

編寫註解處理器
如果沒有用來讀取註解的工具 那註解也不會比註釋更有用 使用註解的過程中 很重要的一個部分就是創建與使用註解處理器 Java SE5擴展了反射機制的API 以幫助程序員構造這類工具 同時 它還提供了一個外部工具apt幫助程序員解析帶有註解的Java源代碼
下面是一個非常簡單的註解處理器 我們將用它來讀取PasswordUtils類 並使用反射機制查找@UseCase標記 我們爲其提供了一組id值 然後它會列出在PasswordUtils中找到的用例 以及缺失的用例
在這裏插入圖片描述

註解元素
標籤@UseCase由UseCase.java定義 其中包含int元素id 以及一個String元素description 註解元素可用的類型如下所示

  • 所有基本類型(int float boolean等)
  • String
  • Class
  • enum
  • Annotation
  • 以上類型的數組
    如果你使用了其他類型 那編譯器就會報錯 注意 也不允許使用任何包裝類型 不過由於自動打包的存在 這算不是什麼限制 註解也可以作爲元素的類型 也就是說註解可以嵌套 這是一個很有用的技巧

默認值限制
編譯器對元素的默認值有些過分挑剔 首先 元素不能有不確定的值 也就是說 元素必須要麼具有默認值 要麼在使用註解時提供元素的值
其次 對於非基本類型的元素 無論是在源代碼中聲明時 或是在註解接口中定義默認值時 都不能以null作爲其值 這個約束使得處理器很難表現一個元素的存在或缺失的狀態 因爲在每個註解的聲明中 所有的元素都存在 並且都具有相應的值 爲了繞開這個約束 我們只能自己定義一些特殊的值 例如空字符串或負數 以此表示某個元素不存在
在這裏插入圖片描述

生成外部文件
假設你希望提供一些基本的對象/關係映射功能 能夠自動生成數據庫表 用以存儲JavaBean對象 你可以選擇使用XML描述文件 指明類的名字 每個成員以及數據庫映射的相關信息 然而 如果使用註解的話 你可以將所有信息都保存在JavaBean源文件中 爲此 我們需要一些新的註解 用以定義與Bean關聯的數據庫表的名字 以及與Bean屬性關聯的列的名字和SQL類型
以下是一個註解的定義 它告訴註解處理器 你需要爲我生成一個數據庫表
在這裏插入圖片描述

注意 @DBTable有一個name()元素 該註解通過這個元素爲處理器創建數據庫表提供表的名字 接下來是爲修飾JavaBean域準備的註解
在這裏插入圖片描述
註解處理器通過@Constraints註解提取出數據庫表的元數據 雖然對於數據庫所能提供的所有約束而言 @Constraints註解只表示了它的一個很小的子集 不過它所要表達的思想已經很清楚了 primaryKey() allowNull()和unique()元素明智地提供了默認值 從而在大多數情況下 使用該註解的程序員無需輸入太多東西
另外兩個@interface定義的是SQL類型 如果希望這個framework更有價值的話 我們就應該爲每種SQL類型都定義相應的註解 不過作爲示例 兩個類型足夠了
這些SQL類型具有name()元素和constraints()元素 後者利用了嵌套註解的功能 將column類型的數據庫約束信息嵌入其中 注意constraints()元素的默認值是@Constraints 由於在@Constraints註解類型之後 沒有在括號中指明@Constraints中的元素的值 因此 constraints()元素的默認值實際上就是一個所有元素都爲默認值的@Constraints 註解 如果要令嵌入的@Constraints註解中的unique()元素爲true 並以此作爲constraints()元素的默認值 則需要如下定義該元素
在這裏插入圖片描述
下面是一個簡單的Bean定義 我們在其中應用了以上這些註解
在這裏插入圖片描述

默認值的語法雖然很靈巧 但它很快就變得複雜起來 以handle域的註解爲例 這是一個@SQLString註解 同時該域將成爲表的主鍵 因此在嵌入的@Constraints註解中 必須對primaryKey元素進行設定 這時事情就變得麻煩了 現在 你不得不使用很長的名 值對形式 重新寫出元素名和@interface的名字 與此同時 由於有特殊命名的value元素已經不再是唯一需要賦值的元素了 所以你也不能再使用快捷方式爲其賦值了 如你所見 最終的結果算不上清晰易懂

變通之道
可以使用多種不同的方式來定義自己的註解 以實現上例中的功能 例如 你可以使用一個單一的註解類@TableColumn 它帶有一個enum元素 該枚舉類定義了STRING INTEGER以及FLOAT等枚舉實例 這就消除了每個SQL類型都需要一個@interface定義的負擔 不過也使得以額外的信息修飾SQL類型的需求變得不可能 而這些額外的信息 例如長度或精度等 可能是非常有必要的需求
我們也可以使用String元素來描述實際的SQL類型 比如VARCHAR(30)或INTEGER 這使得程序員可以修飾SQL類型 但是 它同時也將Java類型到SQL類型的映射綁在了一起 這可不是一個好的設計 我們可不希望更換數據庫導致代碼必須修改並重新編譯 如果我們只需告訴註解處理器 我們正在使用的是什麼 口味 的SQL 然後由處理器爲我們處理SQL類型的細節 那將是一個優雅的設計
第三種可行的方案是同時使用兩個註解類型來註解一個域 @Constraints和相應的SQL類型(例如@SQLIntege) 這種方式可能會使代碼有點亂 不過編譯器允許程序員對一個目標同時使用多個註解 注意 使用多個註解的時候 同一個註解不能重複使用

註解不支持繼承
不能使用關鍵字extends來繼承某個@interface 這真是一個遺憾 如果可以定義一個@TableColumn註解(參考前面的建議) 同時在其中嵌套一個@SQLType類型的註解 那麼這將成爲一個優雅的設計 按照這種方式 程序員可以繼承@SQLType 從而創建出各種SQL類型 例如@SQLInteger和@SQLString等 如果註解允許繼承的話 這將大大減少打字的工作量 並且使語法更整潔 在Java未來的版本中 似乎沒有任何關於讓註解支持繼承的提案 所以 在當前狀況下 上例中的解決方案可能已經是最佳方法了

實現處理器
下面是一個註解處理器的例子 它將讀取一個類文件 檢查其上的數據庫註解 並生成用來創建數據庫的SQL命令
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

使用apt處理註解
與javac一樣 apt被設計爲操作Java源文件 而不是編譯後的類 默認情況下 apt會在處理完源文件後編譯它們 如果在系統構建的過程中會自動創建一些新的源文件 那麼這個特性非常有用 事實上 apt會檢查新生成的源文件中註解 然後將所有文件一同編譯
當註解處理器生成一個新的源文件時 該文件會在新一輪(round Sun文檔中這樣稱呼它)的註解處理中接受檢查 該工具會一輪一輪地處理 直到不再有新的源文件產生爲止 然後再編譯所有的源文件
程序員自定義的每一個註解都需要自己的處理器 而apt工具能夠很容易地將多個註解處理器組合在一起 有了它 程序員就可以指定多個要處理的類 這比程序員自己遍歷所有的類文件簡單多了 此外還可以添加監聽器 並在一輪註解處理過程結束的時候收到通知信息

下面是一個自定義的註解 使用它可以把一個類中的public方法提取出來 構造一個新的接口
在這裏插入圖片描述
RetentionPolicy是SOURCE 因爲當我們從一個使用了該註解的類中抽取出接口之後 沒有必要再保留這些註解信息 下面的類有一個公共方法 我們將會把它抽取到一個有用接口中
在這裏插入圖片描述
在Multiplier類中(它只對正整數起作用) 有一個multiply()方法 該方法多次調用一個私有的add()方法以實現乘法操作 add()方法不是公共的 因此不將其作爲接口的一部分 註解給出了值IMultiplier 這就是將要生成的接口的名字
在這裏插入圖片描述
在這裏插入圖片描述

apt工具需要一個工廠類來爲其指明正確的處理器 然後它才能調用處理器上的process()方法
在這裏插入圖片描述

以上例子中的處理器與工廠類都在annotations包中 在InterfaceExtractorProcessor.java開頭的註釋文字中 根據anotations的目錄結構 在Exec標記處給出了需要從命令行輸入的命令 它將告訴apt工具 使用上面的工廠類來處理Multiplier.java文件 參數-s說明任何新產生的文件都必須放在annotations目錄中 通過處理器中的println()語句 估計你已經能猜到最終生成的IMultiplier.java會是什麼樣子了
在這裏插入圖片描述
apt也會編譯這個新產生的文件 因此你將在相同的目錄中看到IMultiplier.class文件

將觀察者模式用於apt
上面的例子是一個相當簡單的註解處理器 只需對一個註解進行分析 但我們仍然要做大量複雜的工作 因此 處理註解的真實過程可能會非常複雜 當我們有更多的註解和更多的處理器時 爲了防止這種複雜性迅速攀升 mirror API提供了對訪問者設計模式的支持
一個訪問者會遍歷某個數據結構或一個對象的集合 對其中的每一個對象執行一個操作 該數據結構無需有序 而你對每個對象執行的操作 都是特定於此對象的類型 這就將操作與對象解耦 也就是說 你可以添加新的操作 而無需向類的定義中添加方法
這個技巧在處理註解時非常有用 因爲一個Java類可以看作是一系列對象的集合 例如TypeDeclaration對象 FieldDeclaration對象以及MethodDeclaration對象等 當你配合訪問者模式使用apt工具時 需要提供一個Visitor類 它具有一個能夠處理你要訪問的各種聲明的方法 然後 你就可以爲方法 類以及域上的註解實現相應的處理行爲
下面仍然是SQL表生成器的例子 不過這次我們使用訪問者模式來創建工廠和註解處理器
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

看起來這個例子使用的方式似乎更復雜 但是它確實是一種具備擴展能力的解決方案 當你的註解處理器的複雜性越來越高的時候 如果還按前面例子中的方式編寫自己獨立的處理器 那麼很快你的處理器就將變得非常複雜

基於註解的單元測試
單元測試是對類中的每個方法提供一個或多個測試的一種實踐 其目的是爲了有規律地測試一個類的各個部分是否具備正確的行爲 在Java中 最著名的單元測試工具就是JUnit 對於註解出現之前的JUnit而言 有一個主要的問題 即爲了設置並運行JUnit測試需要做大量的形式上的工作 隨着其漸漸的發展 這種負擔已經減輕了一些 但註解的出現能夠使其更貼近 最簡單的單元測試系統
使用註解出現之前的JUnit 程序員必須創建一個獨立的類來保存其單元測試 有了註解 我們可以直接在要驗證的類裏面編寫測試 這將大大減少單元測試所需的時間和麻煩之外 採用這種方式還有一個額外的好處 就是能夠像測試public方法一樣很容易地測試private方法
這個基於註解的測試框架叫做@Unit 其最基本的測試形式 可能也是你用的最多的一個註解是@Test 我們用@Test來標記測試方法 測試方法不帶參數 並返回boolean結果來說明測試成功或失敗 程序員可以任意命名他的測試方法 同時 @Unit測試方法可以是任意你喜歡的訪問修飾方式 包括private
要使用@Unit 程序員必須引入net.mindview.atunit 用@Unit的測試標記爲合適的方法和域打上標記 然後讓你的構建系統對編譯後的類運行@Unit 下面是一個簡單的例子
在這裏插入圖片描述

程序員並非必須將測試方法嵌入到原本的類中 因爲有時候這根本做不到 要生成一個非嵌入式的測試 最簡單的辦法就是繼承
在這裏插入圖片描述

或者你還可以使用組合的方式創建非嵌入式的測試
在這裏插入圖片描述

@Unit中並沒有JUnit裏的特殊的assert方法 不過@Test方法仍然允許程序員返回void(如果你還是想用true或false的話 你仍然可以用boolean作爲方法返回值類型) 這是@Test方法的第二種形式 在這種情況下 要表示測試成功 可以使用Java的assert語句 Java的斷言機制一般要求程序員在java命令行中加上-ea標誌 不過@Unit已經自動打開了該功能 而要表示測試失敗的話 你甚至可以使用異常 @Unit的設計目標之一就是儘可能少地添加額外的語法 而Java的assert和異常對於報告錯誤而言 已經足夠了 一個失敗的assert或從測試方法中拋出異常 都將被看作一個失敗的測試 但是@Unit並不會就在這個失敗的測試上打住 它會繼續運行 直到所有的測試都運行完畢 下面是一個示例程序
在這裏插入圖片描述

下面的例子使用非嵌入式的測試 並且用到了斷言 它將對java.util.HashSet執行一些簡單的測試
在這裏插入圖片描述

對每一個單元測試而言 @Unit都會用默認的構造器 爲該測試所屬的類創建出一個新的實例 並在此新創建的對象上運行測試 然後丟棄該對象 以避免對其他測試產生副作用 如此創建對象導致我們依賴於類的默認構造器 如果你的類沒有默認構造器 或者新對象需要複雜的構造過程 那麼你可以創建一個static方法專門負責構造對象 然後用@TestObjectCreaet註解將該方法標記出來 就像這樣
在這裏插入圖片描述
在這裏插入圖片描述

有的時候 我們需要向單元測試中添加一些額外的域 這時可以使用@TestProperty註解 由它註解的域表示只在單元測試中使用(因此 在我們將產品發佈給客戶之前 他們應該被刪除掉) 在下面的例子中 一個String通過String.split()方法被拆散了 從其中讀取一個值 這個值將被用來生成測試對象
在這裏插入圖片描述
在這裏插入圖片描述
如果你的測試對象需要執行某些初始化工作 並且使用完畢後還需要進行某些清理工作 那麼可以選擇使用static @TestObjectCleanup方法 當測試對象使用結束後 該方法會爲你執行清理工作 在下面的例子中 @TestObjectCreate爲每個測試對象打開了一個文件 因此必須在丟棄測試對象的時候關閉該文件
在這裏插入圖片描述
在這裏插入圖片描述

將@Unit用於泛型
泛型爲@Unit出了一個難題 因爲我們不可能 泛泛地測試 我們必須針對某個特定類型的參數或參數集才能進行測試 解決的辦法很簡單 讓測試類繼承自泛型類的一個特定版本即可
下面是一個堆棧的例子
在這裏插入圖片描述
要測試String版的堆棧 就讓測試類繼承自StackL
在這裏插入圖片描述
在這裏插入圖片描述

不需要任何 套件
與JUnit相比 @Unit有一個比較大的優點 就是@Unit不需要 套件(suites) 在JUnit中 程序員必須告訴測試工具你打算測試什麼 這就要求用套件來組織測試 以便JUnit能夠找到它們 並運行其中包含的測試

實現@Unit
首先 我們需要定義所有的註解類型 這些都是簡單的標籤 並且沒有屬性 @Test標籤在本節開頭已經定義過了 這裏是其他所需的註解
在這裏插入圖片描述
在這裏插入圖片描述
所有測試的保留屬性必須是RUNTIME 因爲@Unit系統必須在編譯後的代碼中查詢這些註解
要實現該系統 並運行測試 我們還需使用反射機制來抽取註解 下面這個程序通過註解中的信息 決定如何構造測試對象 並在測試對象上運行測試 正是由於註解的幫助 這個程序才如此短小而直接
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

AtUnit.java必須要解決一個問題 就是當它找到類文件時 實際引用的類名(含有包)並非一定就是類文件的名字 爲了從中解讀信息 我們必須分析該類文件 這很重要 因爲這種名字不一致的情況確實可能出現 所以 當找到一個.class文件時 第一件事情就是打開該文件 讀取其二進制數據 然後將其交給ClassNameFinder.thisClass() 從這裏開始 我們將進入 字節碼工程 的領域 因爲我們實際上是在分析一個類文件的內容
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

移除測試代碼
對許多項目而言 在發佈的代碼中是否保留測試代碼並沒什麼區別(特別是在如果你將所有的測試方法都聲明爲private的情況下 如果你喜歡就可以這麼做) 但是在有的情況下 我們確實希望將測試代碼清除掉 精簡發佈的程序 或者就是不希望測試代碼暴露給客戶
與自己動手刪除測試代碼相比 這需要更復雜的字節碼工程 不過開源的Javassist工具類庫將字節碼工程帶入了一個可行的領域 下面的程序接受一個-r標誌作爲其第一個參數 如果你提供了該標誌 那麼它就會刪除所有的@Test註解 如果你沒有提供該標記 那它則只會打印出@Test註解 這裏同樣使用ProcessFiles來遍歷你選擇的文件和目錄
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章