天天看點

标記接口,注解和注解處理器的前世今生

目錄

  • ​​簡介​​
  • ​​注解的起源和marker interfaces​​
  • ​​注解的定義​​
  • ​​Retention​​
  • ​​Target​​
  • ​​自定義參數​​
  • ​​在運作時使用注解​​
  • ​​在編譯時使用注解​​
  • ​​總結​​

标記接口,注解和注解處理器的前世今生

簡介

相信大部分的開發者都用過注解,尤其是對使用過Spring的開發者來說,注解是現代Spring中不可擷取的一部分。Spring從最開始的xml配置到後面的注解配置,不論是從程式設計習慣還是項目的建構,都對我們程式員産生了非常重要的影響。

除了使用Spring自帶的注解之外,我們還可以自定義注解。然後通過AOP來對注解進行攔截進而處理相應的業務邏輯。

除了Spring之外,其實JDK本身自帶注解,本文将會深入探讨注解的起源和兩種不同的使用方式。

注解的起源和marker interfaces

先看一個最簡單的注解:

@CustUserAnnotation
public class CustUser {
}      

上面我們将CustUser标記為一個自定義的注解@CustUserAnnotation。

注解其實是在JDK 5中引入的。那麼在JDK 5之前,注解是用什麼方式來表示的呢?答案就是marker interfaces。

marker interfaces中文翻譯叫做标記接口,标記接口就是說這個接口使用來做标記用的,内部并沒有提供任何方法或者字段。

在java中有很多标記接口,最常見的就是Cloneable,Serializable,還有java.util包中的EventListener和RandomAccess。

以Cloneable為例:

/*
 * @since   1.0
 */
public interface Cloneable {
}      

該接口從java1.0就開始有了。實作該接口的類才能夠調用Object中的clone方法。

我們在代碼中如何判斷類是否實作了Cloneable接口呢?

public Object clone() throws CloneNotSupportedException {
        if (this instanceof Cloneable) {
            return super.clone();
        } else {
            throw new CloneNotSupportedException();
        }
    }      

很簡單,通過instanceof來判斷是否是Cloneable即可。

marker interfaces好用是好用,但是有一些缺點,比如沒有額外的中繼資料資訊,功能太過單一,并且會和正常的interface混淆。實作起來也比一般的interface複雜。

正式由于這些原因,在JDK5中,引入了注解Annotation。

注解的定義

注解是由@interface來定義的。建立一個annotation需要指定其target和retention,并可以自定義參數。

我們舉個例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustUserAnnotation {
    int value();
    String name();
    String[] addresses();
}      

上面是我自定義的一個注解。

Retention

Retention表示注解将會在什麼階段可見。它有三個可選值:

SOURCE 表示隻在源代碼可見,編譯的時候就會被丢棄。

CLASS 表示在class可見,也就是說編譯的時候可見,但是運作時候不可見。

RUNTIME 表示運作時候可見。什麼時候才需要運作時可見呢?那就是使用到反射的時候。我們會在後面的例子中具體的描述這種情況。

Retention本身也是一個注解:

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

Target

Target表示這個注解将會用到什麼地方。它有12個值。

TYPE 表示用在Class,interface,enum或者record上。

FIELD 表示用在class的字段上。

METHOD 表示用在方法上。

PARAMETER 表示用在方法上面。

CONSTRUCTOR 用在構造函數上。

LOCAL_VARIABLE 用在本地變量上。

ANNOTATION_TYPE 用在注解上。

PACKAGE 用在package上。

TYPE_PARAMETER 用在類型參數上。

TYPE_USE 用在任何TYPE使用上。

TYPE_PARAMETER和TYPE_USE有什麼差別呢?

TYPE_USE用在任何類型的使用上面,比如申明,泛型,轉換:

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

而TYPE_PARAMETER用在類型參數上:

class MyClass<T> {...}      

MODULE 用在module上。

RECORD_COMPONENT 預覽功能,和records相關。

Target和Retention一樣也是一個注解。

@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();
}      

自定義參數

注解也可以自定參數,參數可以是下的類型:

  1. 基礎類型:int,long,double等
  2. String
  3. Class
  4. 枚舉類型
  5. 其他的注解類型
  6. 上面5中的數組

上面我們的自定義類型定義了三個參數:

int value();
    String name();
    String[] addresses();      

我們看下怎麼使用:

@CustUserAnnotation(value = 100, name="jack ma",addresses = {"人民路","江西路"})
public class CustUser {
}      

在使用中,我們需要傳入自定義的參數,當然你也可以使用default在注解中提供預設值,這樣就不需要從外部傳入。

在運作時使用注解

在運作時,我們可以使用反射的API來獲得注解,并擷取注解中的自定義變量,進而進行相應的業務邏輯處理。

CustUser custUser= new CustUser();
        Annotation[] annotations= custUser.getClass().getAnnotations();
        Stream.of(annotations).filter(annotation -> annotation instanceof CustUserAnnotation)
                .forEach(annotation -> log.info(((CustUserAnnotation) annotation).name()));      

還是剛才的例子,我們通過getAnnotations方法擷取到注解的值。

在運作時是用注解當然是個不錯的主意,但是反射用的太多的話其實會影響程式的性能。

那麼我們可以不可以将運作時的注解提前到編譯時呢?答案是肯定的。

在編譯時使用注解

要想在編譯時使用注解,就要介紹今天我們的最後一部分内容annotation processors。

自定義processors需要實作javax.annotation.processing.Processor接口。

接下來我們自定義一個Processor:

@SupportedAnnotationTypes("com.flydean.*")
@SupportedSourceVersion(SourceVersion.RELEASE_14)
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("process annotation!");
        annotations.forEach(annotation -> {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
            elements.stream()
                    .filter(TypeElement.class::isInstance)
                    .map(TypeElement.class::cast)
                    .map(TypeElement::getQualifiedName)
                    .map(name -> "Class " + name + " is annotated with " + annotation.getQualifiedName())
                    .forEach(System.out::println);
        });
        return true;
    }
}      

SupportedAnnotationTypes表示支援的注解類型。

SupportedSourceVersion表示支援的源代碼版本。

最後我們在process方法中,擷取了注解類的一些資訊。

有了processor我們怎麼在maven環境中使用呢?

最簡單的辦法就是在maven的maven-compiler-plugin插件中添加annotationProcessors,如下所示:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>14</source>
                    <target>14</target>
                    <annotationProcessors>
                       <annotationProcessor>com.flydean.MyProcessor</annotationProcessor>
                    </annotationProcessors>
                </configuration>
            </plugin>
        </plugins>
    </build>      

如果不添加,預設情況下編譯器會從classpath中去尋找META-INF/services/javax.annotation.processing.Processor檔案,這個檔案裡面列出了對外提供的注解處理器。編譯器會加載這些注解處理器去處理目前項目的注解。

lombok應該大家都用過吧,它實際上為我們提供了兩個注解處理器:

标記接口,注解和注解處理器的前世今生

很不幸的是,因為我在CustUser中使用了lombok中的log,如果像上面一樣顯示指定annotationProcessor則會将覆寫預設的查找路徑,最後會導緻lombok失效。

那應該怎麼處理才能相容lombok和自定義的processor呢?

我們可以把自定義processor單獨成一個子產品,也做成lombok這樣的形式:

标記接口,注解和注解處理器的前世今生

這個processor的子產品編譯參數需要加上一個proc none的參數:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>14</source>
                    <target>14</target>
                    <proc>none</proc>
                </configuration>
            </plugin>
        </plugins>
    </build>      

proc是設定是否需要在本項目中啟用processor。對于processor項目來說,它本身還沒有編譯,如果啟用就會出現找不到類的錯誤。是以這裡我們需要将proc設定為none。

最後我們的annotation-usage項目可以不需要annotationProcessors的配置就可以自動從classpath中讀取到自定義的processor了。

總結

本文介紹了marker interface,annotation和annotation processor,并詳細講解了如何在maven程式中使用他們。

本文的例子​​https://github.com/ddean2009/

learn-java-base-9-to-20​​

本文作者:flydean程式那些事