Java注解使用與自定義

零、背景

曾幾何時,XML 一直是 Java 各大框架配置元數(shù)據(jù)(meta data) 的主要途徑。但作為一種集中式的元數(shù)據(jù)管理工具,配置項與作用代碼距離太過 “遙遠”,非常不利于代碼的維護和調(diào)試。再加上 XML 本身復(fù)雜的語法結(jié)構(gòu),往往令碼農(nóng)們大感頭疼。一種與作用代碼耦合在一起的元數(shù)據(jù)配置方式呼之欲出。于是 注解 (Annotations)就在 JDK 5 中正式出現(xiàn)在開發(fā)者的視線之中了。

一、什么是注解

注解是元數(shù)據(jù)的一種形式,它提供有關(guān)程序的數(shù)據(jù),該數(shù)據(jù)不屬于程序本身。注解對其注釋的代碼操作沒有直接影響。換句話說,注解攜帶元數(shù)據(jù),并且會引入一些和元數(shù)據(jù)相關(guān)的操作,但不會影響被注解的代碼的邏輯。

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    ...
}

Java中所有的注解都擴展自 Annotation這個接口,注解本質(zhì)就是一個接口。比如我們最常見的重寫@Override,它沒有參數(shù),所以是一個標記注解。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

1. 元注解

@Override為例,我們發(fā)現(xiàn)上面有兩個注解@Target@Retention,像這種注解的注解被稱為元注解。Java中的元注解有以下幾個,

@Target

這個注解標識了被修飾注解的作用對象,看源碼,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

value是一個數(shù)組,說明該注解的作用對象可以是多個,取值對象在ElementType中,

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE
}

不同的值代表被注解可修飾的范圍,例如TYPE只能修飾類、接口和枚舉定義,METHOD只能修飾方法,比如@Override只能注解方法。這其中有個很特殊的值叫做 ANNOTATION_TYPE, 是專門表示元注解的。

@Retention

該注解指定了被修飾的注解的生命周期。定義如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

value返回了一個RetentionPolicy枚舉類型,

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
  • SOURCE:表示注解編譯時可見(在原文件有效),編譯完后就被丟棄。這種注解一般用于在編譯期做一些事情,比如可以讓注解處理器生成一些代碼,或者是讓注解處理器做一些額外的類型檢查,等等;
  • CLASS:表示在編譯完后寫入 class 文件(在class文件有效),但在類加載后被丟棄。這種注解一般用于在類加載階段做一些事情,比如Android的資源類型檢查(@ColorRes、@DrawableRes、@Px);
  • RUNTIME:表示注解會一直起作用。
    @Override就是編譯時可見,編譯成class之后丟失。
@Documented

編譯器在生成Java文檔時將被注解的元素包含進去。這意味著使用了@Documented注解的注解,其注解的信息會被包含在生成的文檔中,方便開發(fā)者查閱。@Documented是一個標記注解,沒有成員。

@Inherited

用于指示子類是否繼承父類的注解。當(dāng)一個注解被標注為 @Inherited 時,如果一個類使用了被 @Inherited 標注的注解,那么其子類也會自動繼承這個注解。這在一些框架或庫中很有用,可以自動繼承某些特性或行為。需要注意的是,@Inherited annotation類型是被標注過的class的子類所繼承。類并不從它所實現(xiàn)的接口繼承annotation,方法并不從它所重載的方法繼承annotation


一個注解可以有多個target(ElementType),但只能有一個retention(RetentionPolicy)。

2. Java內(nèi)置注解

以下基于JDK18

@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

這個注解沒有任何取值,只能修飾方法,而且RetentionPolicy 為 SOURCE,說明這是一個僅在編譯階段起作用的注解。在編譯階段,如果一個類的方法被 @Override 修飾,編譯器會在其父類中查找是否有同簽名函數(shù),如果沒有則編譯報錯。可見這確實是一個除了在編譯階段就沒什么用的注解。

@Deprecated
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    /**
     * Returns the version in which the annotated element became deprecated.
     * The version string is in the same format and namespace as the value of
     * the {@code @since} javadoc tag. The default value is the empty
     * string.
     *
     * @return the version string
     * @since 9
     */
    String since() default "";

    /**
     * Indicates whether the annotated element is subject to removal in a
     * future version. The default value is {@code false}.
     *
     * @return whether the element is subject to removal
     * @since 9
     */
    boolean forRemoval() default false;
}

這個注解能修飾所有的類型,永久存在,有兩個參數(shù):since用來表示從哪個版本開始廢棄;forRemoval用來表示被注解的元素將來是否可能被移除。這個注解的作用是,告訴使用者被修飾的代碼不推薦使用了,可能會在下一個軟件版本中移除。這個注解僅僅起到一個通知機制,如果代碼調(diào)用了被@Deprecated修飾的代碼,編譯器在編譯時輸出一個編譯告警。

@SuppressWarnings
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The set of warnings that are to be suppressed by the compiler in the
     * annotated element.  Duplicate names are permitted.  The second and
     * successive occurrences of a name are ignored.  The presence of
     * unrecognized warning names is <i>not</i> an error: Compilers must
     * ignore any warning names they do not recognize.  They are, however,
     * free to emit a warning if an annotation contains an unrecognized
     * warning name.
     *
     * <p> The string {@code "unchecked"} is used to suppress
     * unchecked warnings. Compiler vendors should document the
     * additional warning names they support in conjunction with this
     * annotation type. They are encouraged to cooperate to ensure
     * that the same names work across multiple compilers.
     * @return the set of warnings to be suppressed
     */
    String[] value();
}

這個注解的主要作用是壓制編譯告警的,有一個字符串?dāng)?shù)組的參數(shù)。可以在類型、屬性、方法、參數(shù)、構(gòu)造函數(shù)和局部變量前使用,聲明周期是編譯期。
比如下面這個方法使用了magic number,使用@SuppressWarnings("MagicNumber")進行壓制,在編譯期就不會有告警。


二、自定義注解與注解處理器

1. 自定義注解

使用@interface標識CustomeAnnotation是一個注解,表示擴展自java.lang.annotation.Annotation

public @interface CustomeAnnotation {}

然后需要指定注解的作用域與生命周期,使用元注解@Target@Retention來注解CustomeAnnotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomeAnnotation {}

上面表示CustomeAnnotation注解只能作用于方法上,并且其所攜帶的元數(shù)據(jù)會保留到運行時。
CustomeAnnotation沒有攜帶元數(shù)據(jù),意義不大,下面給注解添加參數(shù)(元數(shù)據(jù))

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomeAnnotation {
    String value() default "";
}

注解中每一個方法就是聲明了一個配置參數(shù),方法的名稱就是參數(shù)的名稱,返回值類型就是參數(shù)的類型。使用default定義參數(shù)默認值。
注解參數(shù)的可支持數(shù)據(jù)類型

  1. 所有基本數(shù)據(jù)類型(int, float, boolean, byte, double, char, long, short)
  2. String類型
  3. Class類型
  4. enum類型
  5. Annotation類型
  6. 以上所有類型的數(shù)組

Annotation類型里面的參數(shù)該怎么設(shè)定:
第一,只能用public或默認(default)這兩個訪問權(quán)修飾。上面例子把方法設(shè)為defaul默認類型;
第二,參數(shù)成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數(shù)據(jù)類型和String,Enum,Class,Annotations等數(shù)據(jù)類型,以及這一些類型的數(shù)組。例如,上面例子中參數(shù)名就是value,參數(shù)類型就是String。
第三,如果只有一個參數(shù)成員,最好把參數(shù)名稱設(shè)為 value,后加小括號。
使用的時候通過value = <你的元數(shù)據(jù)>傳遞參數(shù),如果只有一個參數(shù),可以不寫參數(shù)名,如果有多個,則需要顯示聲明參數(shù)名。

public class AnnotationTest{

    @CustomeAnnotation("This is main function")
    public static void main(String[] args) {
        try {
            Class cls = AnnotationTest.class;
            Method method = cls.getMethod("main", String[].class);
            CustomeAnnotation anno = method.getAnnotation(CustomeAnnotation.class);
            System.out.println(anno.value());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

打印:
This is main function

上面的demo,CustomeAnnotation的生命周期必須是RUNTIME,否則獲取到的anno為null。

2. 自定義注解處理器

先創(chuàng)建一個Java library,必須為java庫,不然會找不到j(luò)avax包下的相關(guān)資源。



定義一個注解處理器 CustomProcessor ,每一個處理器都是繼承于AbstractProcessor,并要求必須復(fù)寫 process() 方法,通常我們使用復(fù)寫以下4個方法。

/**
 * 每一個注解處理器類都必須有一個空的構(gòu)造函數(shù),默認不寫就行
 */
public class CustomProcessor extends AbstractProcessor {
    /**
     * init()方法會被注解處理器工具調(diào)用,并輸入ProcessingEnvironment參數(shù)。
     * ProcessingEnvironment 提供很多有用的工具類Elements,Types和Filter
     * @param processingEnvironment 提供給process用來訪問工具框架的環(huán)境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        System.out.print("init");
    }

    /**
     * 相當(dāng)于每個處理器的主函數(shù)main(),在這里寫掃描、評估和處理注解的代碼,以及自定義處理邏輯。
     * 輸入?yún)?shù)RoundEnvironment可以查詢出包含特定注解的被注解元素
     * @param set 請求處理注解類型
     * @param roundEnvironment 有關(guān)當(dāng)前和以前的信息環(huán)境
     * @return 返回true,則這些注解已聲明并且不要求后續(xù)Processor處理它們;
     *          返回false,則這些注解未聲明并且可能要求后續(xù)Processor處理它們;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.print("process");
        return false;
    }

    /**
     * 這里必須指定,這個注解處理器是注冊給那個注解的。
     * 注意:它的返回值是一個字符串的集合,包含本處理器想要處理注解的注解類型的合法全程。
     * @return 注解器所支持的注解類型集合,如果沒有這樣的類型,則返回一個空集合。
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotations = new LinkedHashSet<>();
        supportedAnnotations.add(ClassAnnotation.class.getCanonicalName());
        supportedAnnotations.add(RuntimeAnnotation.class.getCanonicalName());
        supportedAnnotations.add(SourceAnnotation.class.getCanonicalName());
        return supportedAnnotations;
    }

    /**
     * 指定Java版本,通常這里使用SourceVersion.latestSupported(),
     * 默認返回SourceVersion.RELEASE_6
     * @return 使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

也可以使用注解的方式來指定Java版本和注解類型,

@SupportedAnnotationTypes({
        "com.example.annotation.ClassAnnotation",
        "com.example.annotation.RuntimeAnnotation",
        "com.example.annotation.SourceAnnotation"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CustomProcessor extends AbstractProcessor {...}

接下來注冊注解處理器。

  1. 在main 目錄下新建 resources 資源文件夾;
  2. 在 resources文件夾下建立 META-INF/services 目錄文件夾;
  3. 在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件寫入注解處理器的全稱,包括包路徑;



    javax.annotation.processing.Processor文件如下配置,

com.example.processor.CustomProcessor

這個注冊配置看起來比較麻煩,所以google提供了插件快速配置。注解處理器module的build.gradle中配置

android {
    ...
}

dependencies {
    annotationProcessor('com.google.auto.service:auto-service:1.0')
    implementation('com.google.auto.service:auto-service-annotations:1.0')
}

然后自定義注解處理器使用@AutoService注解

// 注意Processor包名
@AutoService(javax.annotation.processing.Processor.class)
@SupportedAnnotationTypes({
        "com.example.annotation.ClassAnnotation",
        "com.example.annotation.RuntimeAnnotation",
        "com.example.annotation.SourceAnnotation"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class CustomProcessor extends AbstractProcessor {...}

這樣就不用上面哪些注冊配置了。

3. 自定義注解處理器的使用

在需要的module的build.gradle中加上自定義處理器module的依賴,注意使用的是annotationProcessor

annotationProcessor(project(":lib_annotation_processor"))

sync之后編譯module,會在build窗口中看見打印,如果沒有打印,執(zhí)行clean -> make project


4. init方法詳解

/**
 * init()方法會被注解處理器工具調(diào)用,并輸入ProcessingEnvironment參數(shù)。
 * ProcessingEnvironment 提供很多有用的工具類Elements,Types和Filter
 * @param processingEnvironment 提供給process用來訪問工具框架的環(huán)境
 */
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    System.out.print("init");
}

當(dāng)我們編譯程序時注解處理器工具會調(diào)用此方法并且提供實現(xiàn)ProcessingEnvironment接口的對象作為參數(shù),

方法 說明
Elements getElementUtils() 返回實現(xiàn)Elements接口的對象,用于操作元素的工具類
Filer getFiler() 返回實現(xiàn)Filer接口的對象,用于創(chuàng)建文件、類和輔助文件
Messager getMessager() 返回實現(xiàn)Messager接口的對象,用于報告錯誤信息、警告提醒
Map getOptions() 返回指定的參數(shù)選項
Types getTypeUtils() 返回實現(xiàn)Types接口的對象,用于操作類型的工具類

Element是一個接口,表示一個程序元素,比如包、類或者方法。

5. process方法詳解

注解處理過程是一個有序的循環(huán)過程。在每次循環(huán)中,一個處理器可能被要求去處理那些在上一次循環(huán)中產(chǎn)生的源文件和類文件中的注解。第一次循環(huán)的輸入是運行此工具的初始輸入。這些初始輸入,可以看成是虛擬的第0次的循環(huán)的輸出。這也就是說我們實現(xiàn)的process方法有可能會被調(diào)用多次,因為我們生成的文件也有可能會包含相應(yīng)的注解。例如,我們的源文件為SourceActivity.class,生成的文件為Generated.class,這樣就會有三次循環(huán),第一次輸入為SourceActivity.class,輸出為Generated.class;第二次輸入為Generated.class,輸出并沒有產(chǎn)生新文件;第三次輸入為空,輸出為空。

/**
 * 相當(dāng)于每個處理器的主函數(shù)main(),在這里寫掃描、評估和處理注解的代碼,以及自定義處理邏輯。
 * 輸入?yún)?shù)RoundEnvironment可以查詢出包含特定注解的被注解元素
 * @param set 請求處理注解類型
 * @param roundEnvironment 有關(guān)當(dāng)前和以前的信息環(huán)境
 * @return 返回true,則這些注解已聲明并且不要求后續(xù)Processor處理它們;
 *          返回false,則這些注解未聲明并且可能要求后續(xù)Processor處理它們;
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    System.out.print("process");
    return false;
}

我們可以通過RoundEnvironment接口獲取注解元素。process方法會提供一個實現(xiàn)RoundEnvironment接口的對象。

方法 說明
boolean processingOver() 如果循環(huán)處理完成返回true,否則返回false
Set<? extends Element> getRootElements() 獲取每個處理循環(huán)需要處理的元素集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 返回被指定注解類型注解的元素集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 返回被指定注解類型注解的元素集合
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容