【JDK8】Annotation 功能增強


在 JDK8 出現之前,ElementType 的列舉成員 TYPEFIELDMETHODPARAMETERCONSTRUCTORLOCAL_VARIABLEANNOTATION_TYPEPACKAGE 等,是用來限定哪個宣告位置可以進行標註。

JDK8 的 ElementType 多了兩個列舉成員 TYPE_PARAMETERTYPE_USE,它們是用來限定哪個型態可以進行標註。舉例來說,如果想要對泛型的型態參數(Type parameter)進行標註:

public class MailBox<@Email T> {
    ...
}

那麼,你在定義 @Email 時,必須在 @Target 設定 ElementType.TYPE_PARAMETER,表示這個標註可用來標註型態參數。例如:

package cc.openhome;

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Target(ElementType.TYPE_PARAMETER)
public @interface Email {}

ElementType.TYPE_USE 可用於標註在各式型態,因此上面的範例也可以將 ElementType.TYPE_PARAMETER 改為 ElementType.TYPE_USE,一個標註如果被設定為 ElementType.TYPE_USE,只要是型態名稱,都可以進行標註。例如若有個標註定義如下:

package cc.openhome;

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Target(ElementType.TYPE_USE)
public @interface Test {}

那以下幾個標註範例都是可以的:

List<@Test Comparable> list1 = new ArrayList<>();   
List<? extends Comparable> list2 = new ArrayList<@Test Comparable>();
@Test String text;
text = (@Test String) new Object();
java.util. @Test Scanner console;
console = new java. util. @Test11 Scanner(System.in);

注意,這幾個範例都僅對 @Test 右邊的型態名稱進行標註,你得與 JDK8 出現前就存在的列舉成員 TYPEFIELDMETHODPARAMETERCONSTRUCTORLOCAL_VARIABLEANNOTATION_TYPEPACKAGE 等區別。舉例來說,以下的標註就不合法:

@Test java.lang.String text;

上面這個例子中,java.lang.String text 顯然是在進行 text 變數的宣告,如果是在宣告一個區域變數,想要讓以上合法,@Test 得在 @Target 加註 ElementType.LOCAL_VARIABLE

可以在更多地方標註,一些靜態分析工具或框架是最主要受到影響的對象,舉例來說,The Checker Framework 中有個 @NonNull 標註,@Target 就設定為 TYPE_USETYPE_PARAMETER

...
@Retention(value=RUNTIME)
@Target(value={TYPE_USE,TYPE_PARAMETER})
public @interface NonNull
...

你可以 下載 The Checker Framework,撰寫本文的時間點它是 1.8.1 版,下載完成後解開 zip 檔案,並設置環境變數:

SET CHECKERFRAMEWORK=%YOUR_WORKSPACE%\checker-framework-1.8.1
SET PATH=%CHECKERFRAMEWORK%\checker\bin;%PATH%

最主要的是,你的 PATH 必須包括解開後的 zip 中 checker\bin 目錄。你可以撰寫一個簡單的程式:

package cc.openhome;

import org.checkerframework.checker.nullness.qual.*;

public class GetStarted {
    public static void main() {
        java.util.@NonNull List<String> elems = 
             new java.util.ArrayList<>();
    }
}

這個簡單的程式使用了 @NonNull 標註,表明 elems 不能是 null,如果你使用以下指令進行編譯:

javac -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java

程式中的 elems 因為不為 null,所以不會發生編譯錯誤,如果你將之改為:

java.util.@NonNull List<String> elems = null;

使用相同指令編譯時,就會發生以下編譯錯誤:

error: [assignment.type.incompatible] incompatible types in assignment.
        java.util.@NonNull List<String> elems = null;
                                            ^
  found   : null
  required: @UnknownInitialization @NonNull List<@Initialized @NonNull String>
1 error

可以看到,對於 List,Checker 框架會預設不能收集 null,因此,如果你撰寫以下程式:

package cc.openhome;

import org.checkerframework.checker.nullness.qual.*;
import java.util.*;

public class GetStarted {
    public static void main(String[] args) {
        List<String> elems = new ArrayList<>();
        elems.add(null);
    }
}

使用相同指令編譯,預設會檢查出被加入 null 元素而發生編譯錯誤:

error: [argument.type.incompatible] incompatible types in argument.
        elems.add(null);
              ^
  found   : null
  required: @Initialized @NonNull String
1 error

如果你真的想允許 List 可以收集 null,那麼可以加以標註,那麼使用相同指令編譯時,就不會發生編譯錯誤:

List<@Nullable String> elems = new ArrayList<>();
elems.add(null);

想要知道更多 Checker 的使用,可以參考The Checker Framework Manual

JDK8 除了 ElementType 多了兩個列舉成員 TYPE_PARAMETERTYPE_USE 之外,還新增了個 @Repeatable,可以讓你在同一個位置重複相同標註。舉例來說,你也許本來定義了以下的 @Filter 標註:

public @interface Filter {
    String[] value();
}

這可以讓你如下進行標註:

@Filter({"/admin", "/manager"})
public interface SecurityFilter {
    ...
}

如果你想要另一種如下的標註風格:

package cc.openhome;

@Filter("/admin")
@Filter("/manager")
public interface SecurityFilter {}

在 JDK8 還沒出現之前,沒有辦法達到這點需求,如果使用 JDK8,可以如下定義 @Filter 來解決這類問題:

package cc.openhome;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Filters.class)
public @interface Filter {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@interface Filters {
    Filter[] value(); 
}

實際上這是編譯器的把戲,在這邊你使用 @Repeatable 時告訴編譯器,使用 @Filters 來作為收集重複標註資訊的容器,而每個 @Filter 儲存各自指定的字串值。

JDK8 在 java.lang.reflect.AnnotatedElement 新增了 getDeclaredAnnotationsByTypegetAnnotationsByType,在指定 @Repeatable 的標註時,會找尋收集重複標註的容器中,相對來說,getDeclaredAnnotationgetAnnotation 就不會處理 @Repeatable 的標記。舉例來說,可以使用以下範例,來讀取之前看過的 SecurityFilter 上的重複的 @Filter 標記資訊:

package cc.openhome;

import static java.lang.System.out;

public class SecurityTool {
    public static void main(String[] args) {
        Filter[] filters = SecurityFilter.class.
                getAnnotationsByType(Filter.class);
        for(Filter filter : filters) {
            out.println(filter.value());
        }

        out.println(SecurityFilter.class.getAnnotation(Filter.class));
    }
}

執行結果如下,可以觀察到,對於被標註為 @Repeatable@FiltergetAnnotation 傳回值會是 null

/admin
/manager
null