程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 8的類型注解:工具和機會

Java 8的類型注解:工具和機會

編輯:關於JAVA

在以前的Java版本中,開發者只能將注解(Annotation)寫在聲明中。對於Java 8,注解可以寫在使用類型的任何地方,例如聲明、泛型和強制類型轉換等語句:

@Encrypted String data;
List<@NonNull String> strings;
myGraph = (@Immutable Graph) tmpGraph;

乍一看,類型注解並不是Java新版本最炫的特性。事實上,注解只是語法!工具決定了注解的的語義(即,它們的含義和行為)。本文介紹新的注解語法和實用工具,以提高生產力和構建更高質量的軟件。

在金融行業,我們的市場波動和監管環境決定了上市時間比以往任何時候都更加重要。但犧牲安全性或質量絕對不是一個可選項:簡單的百分點和基點混亂就可能造成嚴重後果。這種情況同樣存在於所有其它行業。

作為一名Java程序員,也許你已經采用注解來提高軟件質量。想想早在Java 1.5中引入的@Override注解。在具有復雜繼承層次結構的大型項目中,要跟蹤系統運行時會執行方法的哪一種實現是很困難的。如果你不小心修改了某個方法的聲明,可能會導致子類方法沒有被調用。這種方式取消了一個方法調用,將會引入缺陷或者安全漏洞。為此,Java引入了@Override注解,開發者可以用它來說明該方法覆蓋了父類方法。如果程序沒有匹配這種意圖,Java編譯器將使用這些注解來警告開發者。如此,注解扮演了機器檢查文檔的形式。

開發者可以通過元編程(Metaprogramming)等技術提高生產率,注解在其中扮演了核心角色。其思想是通過注解夠告訴工具如何生成新代碼、轉換代碼或者決定運行期的行為。以Java Persistence API(JPA)為例,這也是Java 1.5引入的功能。它允許開發者以聲明的方式如@Entity,指定Java對象與數據庫實體之間的關系。然後Hibernate這類工具就可以使用這些注解,在運行期生成映射文件和SQL查詢。

在JPA和Hibernate的場景中,注解用於支持DRY(Don't Repeat Yourself)原則。有趣的是,無論你在哪尋找支持最佳實踐的開發工具,都不難發現注解的存在。一些著名的例子包括使用依賴注入(Dependency Injection)降低耦合,使用面向切面編程(Aspect Oriented Programming)分離關注點。

問題來了:如果注解已經被用於提升質量和提高生產率,為什麼我們還需要類型注解?

這個問題的簡單回答是:類型注解提供更多的功能。它們幫助自動檢測更多的缺陷,為你提供生產力工具的更多控制。

類型注解的語法

在Java 8中,類型注解可以寫在使用類型的任何地方,以下是一些例子:

@Encrypted String data
List<@NonNull String> strings
MyGraph = (@Immutable Graph) tmpGraph;

引入一個新的類型注解非常簡單,只要定義一個注解,並且其target為ElementType.TYPE_PARAMETERElementType.TYPE_USE,或者兩個都包含:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
public @interface Encrypted { }

ElementType.TYPE_PARAMETER表示注解能寫在類型變量的聲明語句中(如:class MyClass {...})。而ElementType.TYPE_USE表示注解能寫在使用類型的任何語句中(例如聲明語句、泛型和強制轉換語句中的類型)。

一旦源碼中的類型有了注解,就像聲明中的注解一樣,它可以同時存在於類文件中並在運行時可以通過反射獲取(定義注解時使用RetentionPolicy.CLASSRetentionPolicy.RUNTIME策略)。類型注解與以前的注解有兩個主要區別:首先,局域變量聲明中的類型注解也可以保留在類文件中;其次,完整泛型被保留,並且在運行期可以訪問。

盡管注解可以保存在類文件中,但它不影響程序的常規運行。例如,開發人員可能在方法體中聲明了兩個File變量和一個Connection變量:

File file = ...;
@Encrypted File encryptedFile = ...;
@Open Connection connection = ...;

當程序運行時,傳遞其中任何一個文件給connection的send(...)方法,都會調用同一個方法實現。

// 以下代碼將調用同一個方法
connection.send(file);
connection.send(encryptedFile);

正如你預期的那樣,運行期沒有區別,也就是說,盡管參數的類型是有注解的,但方法不會基於注解的類型進行重載:

public class Connection{
     void send(@Encrypted File file) { ... } 
     // Impossible:
     // void send( File file) { ... }
     . . .
}

這個限制的背後,直覺告訴我們,編譯器完全無法知道有注解的類型和無注解的類型之間的關系,也不知道有不同注解的類型之間的關系。

但是,別急!變量encryptedFile的注解@Encrypted和方法聲明中file參數的注解是相對應的;那麼變量connection的注解@Open又與哪個方法聲明中的注解對應呢?在調用connection.send(...)中,變量connection是方法的“接收者”。(術語“接收者, Receiver”來源於對象間傳遞消息的面向對象的經典比喻。)Java 8為方法聲明引入了新的語法,因此類型注解可以寫在方法接收者上:

void send(@Open Connection this, @Encrypted File file)

同樣,由於注解對程序執行沒有影響,因此以新的接收者參數語法聲明的方法與使用傳統語法聲明的方法具有同樣的行為。實際上,當前新語法的唯一用處是類型注解可以寫在接收者的類型上。

類型注解語法,包括多維數組語法的完整說明可以查看JSR (Java Specification Request) 308網站。

使用注解檢測缺陷

在代碼中寫注解可用來強調有缺陷代碼中的錯誤:

@Closed Connection connection = ...;
File file = ...;
…
connection.send(file); // 錯誤!關閉的連接並且未加密!

然而,上面的代碼仍然能夠編譯、運行,然後崩潰,Java編譯器並沒有檢查用戶定義的注解。相反,Java平台公開了兩個API,Java Compiler Plug-in和Pluggable Annotations Processing API,第三方開發商可以開發自己的分析器。

在前面的例子中,實際上注解用於限制變量的值。我們可以用其它方式來限制File類型:@Open File, @Localized File, @NonNull File。我們也可以用這些注解來限制其它類型,例如@Encrypted String。因為類型注解獨立於Java類型系統,注解的概念可重用於多種類型。

但是這些注解如何能夠自動檢查呢?直觀地說,有些注解是另一些注解的子類,使用它們將能夠進行類型檢查。考慮一下SQL注入攻擊的問題,如何防止數據庫執行用戶提供的(污染的)輸入。我們也許會把數據分為@Untainted@MaybeTainted,對應於數據是否保證沒有用戶輸入:

@MaybeTainted String userInput; 
@Untainted String dbQuery;

注解@MaybeTainted可認為是注解@Untainted的父類。有兩種方式可以用來思考這種關系。首先,可能污染的數據集一定是確定未污染數據的超集(確定未污染的數據可以是可能污染數據集的元素)。相反地,注解@Untainted提供了比@MaybeTainted更嚴格有力的保證。讓我們看看在實際應用中子類是否有效:

userInput = dbQuery; // OK 
dbQuery = "SELECT FROM * WHERE " + userInput; // 類型錯誤!

第一行檢測通過,如果我們假定未污染的值也屬於污染值,這應該沒問題。在第二行,我們的子類規則發現了一個bug:我們嘗試將父類賦值給更嚴格的子類。

Checker框架

Checker框架是檢查Java注解的框架。該框架首次發布於2007年,是一個活躍的開源項目,由JSR308標准的副主管Michael Ernst教授領導。Checker框架包含了大量的注解和檢查器,能夠檢測空指針間接引用、計量單位不匹配和安全漏洞缺陷,以及線程/並發錯誤等等。因為檢查器在引擎蓋下使用類型檢查,因此其結果都是聲音。檢查器不會漏掉任何潛在錯誤,而工具使用只是摸索威力。在編譯期間,框架使用編譯器API執行檢查。作為一個框架,你能快速創建自己的注解檢查器去檢測特定應用的缺陷。

該框架的目標是不需要寫大量的注解就能檢測缺陷。這主要依賴於兩個特性:智能默認(smart default)和控制流敏感(control-flow sensitivity)。舉例來說,檢測空指針缺陷時,檢查器默認假定參數不能為空。檢查器也能使用條件語句決定間接引用表達式是安全的。

void nullSafe(Object nonNullByDefault, @Nullable Object mightBeNull){
     nonNullByDefault.hashCode(); // OK due to default
     mightBeNull.hashCode(); // Warning!
     if (mightBeNull != null){
        mightBeBull.hashCode(); // OK due to check
     }
}

實際上,默認和控制流敏感意味著你在方法體中幾乎不用寫注解,檢查器能自動推斷和檢查注解。通過保持注解的語義與官方Java編譯器分離,Java團隊確保了第三方工具設計者與用戶能夠決定自己的設計。這樣就可以定制錯誤檢查來滿足項目的個性化需求。

自定義注解的這種能力,讓你也許會考慮領域特定(Domain-specific)類型檢查。例如在金融行業,利率使用百分比描述,而利率差額通常使用基點(1%的百分之一)描述。使用Checker框架的單位檢查器(Unit Checker),你可以定義兩個注解@Percent@BasisPoints,確保你沒有混淆兩者:

BigDecimal pct = returnsPct(...); // annotated to return @Percent 
requiresBps(pct); // error: @BasisPoints is required

這兒,因為Checker框架是控制流敏感的,當調用requiresBps(pct)時,它知道pct@Percent BigDecimal,原因是:第一,returnsPct(...)的注解表明返回@Percent BigDecimal;其次,調用requiresBps(pct)前,pct沒有被重新賦值。通常開發者使用命名規范來盡量避免這類缺陷。Checker框架為你確保不存在這些缺陷,即使代碼不斷增長和發生變化。

Checker框架已經檢查了數百萬行代碼,即使在經過良好測試的軟件中,也暴露了數百個缺陷。也許這是我最喜歡的例子:當框架檢查流行的Google Collections類庫(現在叫Google Guava)時,它發現了一些空指針缺陷,這些甚至是大量測試和啟發式的靜態分析工具所沒有發現的。

要獲得這類結果,並不需要打亂代碼。實際上,使用Checker框架校驗屬性,每千行代碼只需要2-3個注解!

如果你在使用Java 6或者7,同樣可以使用Checker框架來提高代碼質量。框架支持類型注解寫成注釋(例如:/*@NotNull*/ String)。其歷史原因是,從2006年開始,Checker框架與JSR 308(類型注解規范)一起共同開發。

盡管Checker框架是能夠利用錯誤檢查新語法優勢的最佳框架,但現在它不是唯一的一個。Eclipse與IntelliJ都已經支持類型注解。

注解是聲明式的規范:(1)工具如何生成代碼或輔助文件;(2)工具該如何影響程序的運行時行為。這種使用注解的方式被稱為元編程。一些框架,如Lombok,使用注解進行元編程到了極致,導致代碼都不再像Java了。

讓我們先看看面向切面編程(AOP)。AOP旨在分離關注,例如將日志和身份驗證與程序的主業務邏輯分離。通過AOP,編譯時工具基於規則集將額外代碼加到你的程序中。例如,我們定義一個規則,基於類型注解自動加上身份驗證:

void showSecrets(@Authenticated User user){
     // 使用AOP自動插入:
     if (!AuthHelper.EnsureAuth(user)) throw . . .;
}

如前所述,注解限定了類型。然而,AOP框架不是用來在編譯期間檢查注解,而是用來在運行期自動執行校驗。這個例子展示了類型注解如何為你提供更多的控制,決定AOP框架何時以及如何修改程序。

Java 8還支持局域聲明中使用類型注解,並且保存在類文件中。這開啟了細粒度AOP的新機會。例如有規律地添加跟蹤代碼:

// 跟蹤ar對象的所有調用 
@Trace AuthorizationRequest ar = . . .;

同樣,類型注解為使用AOP進行元編程提供了更多的控制。依賴注入也是同樣的情形。使用Spring 4,你終於可以使用泛型作為限定詞形式:

@Autowired private Store<Product> s1; 
@Autowired private Store<Service> s2;

使用泛型消除了引入類,如ProductStore和ServiceStore,或者使用脆弱的命名為基礎的注入規則的必要性。

使用類型注解,不難想像(Spring中還未實現)使用注解進一步控制注入:

@Autowired private Store<@Grocery Product> s3;

這個例子演示了類型注解作為一個工具分離關注,使項目的類型層級保持整潔。這種分離是可行的,因為類型注解獨立於Java類型系統。

前方的路

我們已經看到新的類型注解如何用於檢測/防止程序錯誤和提高生產力。然而,類型注解的真正潛能是結合錯誤檢查和元編程,開辟新的開發模式。

其基本思想是構建運行時和類庫,利用注解自動使程序更高效、並行或者更安全,並且自動強制開發者正確地使用那些注解。

這種方法的一個很好的例子是Adrian Sampson的EnerJ框架,該框架通過近似計算進行高能效計算。EnerJ基於監視,有時候,例如在移動設備上處理圖像時,為了節約能源,權衡圖像精度是有意義的。開發者使用EnerJ,對於非關鍵數據使用@Approx類型注解。基於這些注解,EnerJ運行時處理這些數據時會考慮各種捷徑。例如,它可能使用低能耗的近似計算硬件來保存數據或執行計算。但是,通過程序移動近似數據是危險的,作為開發者,你不會希望控制流受到近似數據的影響。因此,EnerJ使用Checker框架強制近似數據不能用於控制流(例如,在if語句中)。

這種方法的應用並不局限於移動設備。在金融領域,我們常常面對精度與速度之間的權衡。在這些情況下,可以留給運行時來控制蒙特卡洛路徑或收斂標准的數目,或者基於當前的需求和可用的資源,甚至可能在專用硬件上運行計算。

這種方法的巧妙之處在於,它將如何執行與核心業務邏輯描述的執行什麼計算進行了分離。

總結

在Java 8中,除了在聲明中寫注解,你還能在使用類型的任何地方寫注解。注解本身對程序行為沒有任何影響。然而,通過使用Checker框架這樣的工具,你可以使用類型注解來自動檢查和確認不存在軟件缺陷,並使用元編程提高生產效率。盡管現有工具要完全利用類型注解的優勢還需要一定的時間,但現在是時候開始探索類型注解如何能夠提升你的軟件質量和生產效率了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved