天天看點

Springboot以友善聞名,那麼你知道其簡便性的核心-java注解的原理嘛?

java注解

用法

一個簡單的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test1 {
    String name() default "123";
    String[] name2() default {"123","342432","321321321"};
}

// 
public class Test2{
    @Test1(name = "2313122313",name2 = {"312231321","4342","65456543"})
    public static void sayHello(){
        System.out.println("123");
    }

    public static void main(String[] args){
        sayHello();
    }
}      

有幾個地方需要注意一下,首先@interface是編譯器識别的,标明該類型是一個繼承了java.lang.annotation.Annotation接口的子接口,稱之為注解。@Target和@Retention都是jdk中定義好的注解,前者主要标明該注解可以用于修飾什麼,而後者主要确定該注解保留的環境,即可以在哪些環境中運作,是編譯期,運作期還是編寫代碼的時候。

public enum ElementType {
    TYPE, // Class, interface (including annotation type), or enum declaration
    FIELD, // Field declaration (includes enum constants)
    METHOD, // Method declaration
    PARAMETER, // Formal parameter declaration
    CONSTRUCTOR, // Constructor declaration
    LOCAL_VARIABLE, // Local variable declaration 
    ANNOTATION_TYPE,  //Annotation type declaration
    PACKAGE, // Package declaration
    TYPE_PARAMETER, // type parameter declaration
    TYPE_USE // Use of a type
}

public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}      

上面兩個枚舉類型分别是注解保留時機(編碼期,編譯期,運作期)和作用範圍,也是注解最為核心的屬性。

不過這樣的接口是沒有意義的。

讓注解變得有意義

注意,剛剛我們說到了,@interface标明該對象是一個繼承自Annotation的子接口,那麼我們首先得看看該接口的源碼

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}      

前三個方法比較基礎,第四個函數究竟表達的是啥呢?我剛才說,@interface标明該對象是繼承了Annotation的子接口,而沒有說是實作了Annotation接口的類,從這裡就可以看出來,如果實作類,而@interface無法自動辨識怎麼去實作第四個方法,是以隻能是接口,從extends關鍵字也可以看出來,由@interface修飾的對象就是Annotation的子接口。

隻不過與一般的接口不同的是,注解類型,我們可以在定義函數的時候可以設定預設值,通過default關鍵字實作,而且,注解這樣的接口是沒有實作類的。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test1 {
    String name() default "123";
    String[] name2() default {"123","342432","321321321"};
}      

我看了幾篇部落格,都說注解就是中繼資料,可以了解為程式正常運作的配置檔案,可以定義程式運作的先後關系,配置等。那麼我們看看其餘幾個元注解的實作

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}      

可以看見@Document注解的源碼和@Inherited注解的源碼一模一樣,但是我們知道這兩個注解的語義是不一樣的,編譯器是怎麼知道兩個不同的注解定義的予以的呢?這是因為jdk内置的注解,編譯器是有一套對應的方法的,也就是編譯器内部自身在掃描注解的時候,對于内置注解,會根據名字去識别和做行為判斷。

是以對于自定義注解,編譯器是無法識别的,是以自定義注解一般都是作用于運作期,也就是RetentionPolicy.RUNTIME,在編譯期編譯進位元組碼。在運作期,需要我們通過反射技術,識别該注解以及它所攜帶的資訊,然後做相應的處理。

這就帶來了一個問題,由于使用的是反射技術,是以,注解帶來的問題,隻能在運作期才能發現,同時,這樣也給debug帶來了難度。

運作期如何找到并解析

來看下面這個簡單的例子

// Test1.java 定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test1 {
    String name() default "123";
    String[] name2() default {"123","342432","321321321"};
}

// Test2.java 定義被注解修飾的方法,一個修改了注解的預設值,一個沒有修改注解的預設值
public class Test2{
    @Test1(name = "2313122313",name2 = {"312231321","4342","65456543"})
    public static void change(){
    }

    @Test1
    public static void notChange(){
    }
}

// Test3.java 用反射尋找被注解修飾的方法
public class Test3 {

    public static void parsing(Object obj) {
        if (Objects.isNull(obj)) return;
        Method[] methods = obj.getClass().getDeclaredMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(Test1.class)) {
                Test1 test1 = method.getAnnotation(Test1.class);
                System.out.println("methodName = " + method.getName());
                System.out.println("name = " + test1.name());
                for (String s : test1.name2()) {
                    System.out.println("name2 = " + s);
                }
            }
        }
    }
}

// Test4.java 調用

public class Test4{

    public static void main(String[] args){
        Test2 test2 = new Test2();
        Test3.parsing(test2);
    }
}      

運作結果如下

methodName = notChange
name = 123
name2 = 123
name2 = 342432
name2 = 321321321
methodName = change
name = 2313122313
name2 = 312231321
name2 = 4342
name2 = 65456543      

從上面這個例子,可以發現,jdk實作的反射方法中提供了跟注解有關的方法,這樣就友善了開發者找到注解修飾的對象并利用目前注解的值情況做一些處理,進而讓代碼可以自動化運作(即通過簡單的配置使得代碼能夠正常運作)。

本質上來講,我個人覺得注解是可以有替換的操作,隻不過注解是從設計思想上的提升,使得實作更為簡單。

帶來的好處