深入理解 Java 註解

靜默虛空發表於2019-04-05

本文內容基於 JDK8。註解是 JDK5 引入的,後續 JDK 版本擴充套件了一些內容,本文中沒有明確指明版本的註解都是 JDK5 就已經支援的註解。

:notebook: 本文已歸檔到:「blog

:keyboard: 本文中的示例程式碼已歸檔到:「javacore

簡介

註解的形式

Java 中,註解是以 @ 字元開始的修飾符。如下:

@Override
void mySuperMethod() { ... }
複製程式碼

註解可以包含命名或未命名的屬性,並且這些屬性有值。

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass() { ... }
複製程式碼

如果只有一個名為 value 的屬性,那麼名稱可以省略,如:

@SuppressWarnings("unchecked")
void myMethod() { ... }
複製程式碼

如果註解沒有屬性,則稱為標記註解。如:@Override

什麼是註解

從本質上來說,註解是一種標籤,其實質上可以視為一種特殊的註釋,如果沒有解析它的程式碼,它並不比普通註釋強。

解析一個註解往往有兩種形式:

  • 編譯期直接的掃描 - 編譯器的掃描指的是編譯器在對 java 程式碼編譯位元組碼的過程中會檢測到某個類或者方法被一些註解修飾,這時它就會對於這些註解進行某些處理。這種情況只適用於 JDK 內建的註解類。
  • 執行期的反射 - 如果要自定義註解,Java 編譯器無法識別並處理這個註解,它只能根據該註解的作用範圍來選擇是否編譯進位元組碼檔案。如果要處理註解,必須利用反射技術,識別該註解以及它所攜帶的資訊,然後做相應的處理。

註解的作用

註解有許多用途:

  • 編譯器資訊 - 編譯器可以使用註解來檢測錯誤或抑制警告。
  • 編譯時和部署時的處理 - 程式可以處理註解資訊以生成程式碼,XML 檔案等。
  • 執行時處理 - 可以在執行時檢查某些註解並處理。

作為 Java 程式設計師,多多少少都曾經歷過被各種配置檔案(xml、properties)支配的恐懼。過多的配置檔案會使得專案難以維護。個人認為,使用註解以減少配置檔案或程式碼,是註解最大的用處。

註解的代價

凡事有得必有失,註解技術同樣如此。使用註解也有一定的代價:

  • 顯然,它是一種侵入式程式設計,那麼,自然就存在著增加程式耦合度的問題。
  • 自定義註解的處理需要在執行時,通過反射技術來獲取屬性。如果註解所修飾的元素是類的非 public 成員,也可以通過反射獲取。這就違背了物件導向的封裝性。
  • 註解所產生的問題,相對而言,更難以 debug 或定位。

但是,正所謂瑕不掩瑜,註解所付出的代價,相較於它提供的功能而言,還是可以接受的。

註解的應用範圍

註解可以應用於類、欄位、方法和其他程式元素的宣告。

JDK8 開始,註解的應用範圍進一步擴大,以下是新的應用範圍:

類例項初始化表示式:

new @Interned MyObject();
複製程式碼

型別轉換:

myString = (@NonNull String) str;
複製程式碼

實現介面的宣告:

class UnmodifiableList<T> implements
    @Readonly List<@Readonly T> {}
複製程式碼

丟擲異常宣告:

void monitorTemperature()
    throws @Critical TemperatureException {}
複製程式碼

內建註解

JDK 中內建了以下註解:

  • @Override
  • @Deprecated
  • @SuppressWarnnings
  • @SafeVarargs(JDK7 引入)
  • @FunctionalInterface(JDK8 引入)

@Override

@Override 用於表明被修飾方法覆寫了父類的方法。

如果試圖使用 @Override 標記一個實際上並沒有覆寫父類的方法時,java 編譯器會告警。

@Override 示例:

public class OverrideAnnotationDemo {

    static class Person {
        public String getName() {
            return "getName";
        }
    }


    static class Man extends Person {
        @Override
        public String getName() {
            return "override getName";
        }

        /**
         *  放開下面的註釋,編譯時會告警
         */
       /*
        @Override
        public String getName2() {
            return "override getName2";
        }
        */
    }

    public static void main(String[] args) {
        Person per = new Man();
        System.out.println(per.getName());
    }
}
複製程式碼

@Deprecated

@Deprecated 用於標明被修飾的類或類成員、類方法已經廢棄、過時,不建議使用。

@Deprecated 有一定的延續性:如果我們在程式碼中通過繼承或者覆蓋的方式使用了過時的類或類成員,即使子類或子方法沒有標記為 @Deprecated,但編譯器仍然會告警。

注意: @Deprecated 這個註解型別和 javadoc 中的 @deprecated 這個 tag 是有區別的:前者是 java 編譯器識別的;而後者是被 javadoc 工具所識別用來生成文件(包含程式成員為什麼已經過時、它應當如何被禁止或者替代的描述)。

@Deprecated 示例:

public class DeprecatedAnnotationDemo {
    static class DeprecatedField {
        @Deprecated
        public static final String DEPRECATED_FIELD = "DeprecatedField";
    }


    static class DeprecatedMethod {
        @Deprecated
        public String print() {
            return "DeprecatedMethod";
        }
    }


    @Deprecated
    static class DeprecatedClass {
        public String print() {
            return "DeprecatedClass";
        }
    }

    public static void main(String[] args) {
        System.out.println(DeprecatedField.DEPRECATED_FIELD);

        DeprecatedMethod dm = new DeprecatedMethod();
        System.out.println(dm.print());


        DeprecatedClass dc = new DeprecatedClass();
        System.out.println(dc.print());
    }
}
//Output:
//DeprecatedField
//DeprecatedMethod
//DeprecatedClass
複製程式碼

@SuppressWarnnings

@SuppressWarnings 用於關閉對類、方法、成員編譯時產生的特定警告。

@SuppressWarning 不是一個標記註解。它有一個型別為 String[] 的陣列成員,這個陣列中儲存的是要關閉的告警型別。對於 javac 編譯器來講,對 -Xlint 選項有效的警告名也同樣對 @SuppressWarings 有效,同時編譯器會忽略掉無法識別的警告名。

@SuppressWarning 示例:

@SuppressWarnings({"rawtypes", "unchecked"})
public class SuppressWarningsAnnotationDemo {
    static class SuppressDemo<T> {
        private T value;

        public T getValue() {
            return this.value;
        }

        public void setValue(T var) {
            this.value = var;
        }
    }

    @SuppressWarnings({"deprecation"})
    public static void main(String[] args) {
        SuppressDemo d = new SuppressDemo();
        d.setValue("南京");
        System.out.println("地名:" + d.getValue());
    }
}
複製程式碼

@SuppressWarnings 註解的常見引數值的簡單說明:

  • deprecation - 使用了不贊成使用的類或方法時的警告;
  • unchecked - 執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合儲存的型別;
  • fallthrough - 當 Switch 程式塊直接通往下一種情況而沒有 Break 時的警告;
  • path - 在類路徑、原始檔路徑等中有不存在的路徑時的警告;
  • serial - 當在可序列化的類上缺少 serialVersionUID 定義時的警告;
  • finally - 任何 finally 子句不能正常完成時的警告;
  • all - 所有的警告。
@SuppressWarnings({"uncheck", "deprecation"})
public class InternalAnnotationDemo {

    /**
     * @SuppressWarnings 標記消除當前類的告警資訊
     */
    @SuppressWarnings({"deprecation"})
    static class A {
        public void method1() {
            System.out.println("call method1");
        }

        /**
         * @Deprecated 標記當前方法為廢棄方法,不建議使用
         */
        @Deprecated
        public void method2() {
            System.out.println("call method2");
        }
    }

    /**
     * @Deprecated 標記當前類為廢棄類,不建議使用
     */
    @Deprecated
    static class B extends A {
        /**
         * @Override 標記顯示指明當前方法覆寫了父類或介面的方法
         */
        @Override
        public void method1() { }
    }

    public static void main(String[] args) {
        A obj = new B();
        obj.method1();
        obj.method2();
    }
}
複製程式碼

@SafeVarargs

@SafeVarargs 在 JDK7 中引入。

@SafeVarargs 的作用是:告訴編譯器,在可變長引數中的泛型是型別安全的。可變長引數是使用陣列儲存的,而陣列和泛型不能很好的混合使用。

簡單的說,陣列元素的資料型別在編譯和執行時都是確定的,而泛型的資料型別只有在執行時才能確定下來。因此,當把一個泛型儲存到陣列中時,編譯器在編譯階段無法確認資料型別是否匹配,因此會給出警告資訊;即如果泛型的真實資料型別無法和引數陣列的型別匹配,會導致 ClassCastException 異常。

@SafeVarargs 註解使用範圍:

  • @SafeVarargs 註解可以用於構造方法。
  • @SafeVarargs 註解可以用於 staticfinal 方法。

@SafeVarargs 示例:

public class SafeVarargsAnnotationDemo {
    /**
     * 此方法實際上並不安全,不使用此註解,編譯時會告警
     */
    @SafeVarargs
    static void wrongMethod(List<String>... stringLists) {
        Object[] array = stringLists;
        List<Integer> tmpList = Arrays.asList(42);
        array[0] = tmpList; // 語法錯誤,但是編譯不告警
        String s = stringLists[0].get(0); // 執行時報 ClassCastException
    }

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");

        List<String> list2 = new ArrayList<>();
        list.add("1");
        list.add("2");

        wrongMethod(list, list2);
    }
}
複製程式碼

以上程式碼,如果不使用 @SafeVarargs ,編譯時會告警

[WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 某些輸入檔案使用了未經檢查或不安全的操作。
[WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 有關詳細資訊, 請使用 -Xlint:unchecked 重新編譯。
複製程式碼

@FunctionalInterface

@FunctionalInterface 在 JDK8 引入。

@FunctionalInterface 用於指示被修飾的介面是函式式介面。

需要注意的是,如果一個介面符合"函式式介面"定義,不加 @FunctionalInterface 也沒關係;但如果編寫的不是函式式介面,卻使用 @FunctionInterface,那麼編譯器會報錯。

什麼是函式式介面?

函式式介面(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的介面。函式式介面可以被隱式轉換為 lambda 表示式。

函式式介面的特點:

  • 介面有且只能有個一個抽象方法(抽象方法只有方法定義,沒有方法體)。
  • 不能在介面中覆寫 Object 類中的 public 方法(寫了編譯器也會報錯)。
  • 允許有 default 實現方法。

示例:

public class FunctionalInterfaceAnnotationDemo {

    @FunctionalInterface
    public interface Func1<T> {
        void printMessage(T message);
    }

    /**
     * @FunctionalInterface 修飾的介面中定義兩個抽象方法,編譯時會報錯
     * @param <T>
     */
    /*@FunctionalInterface
    public interface Func2<T> {
        void printMessage(T message);
        void printMessage2(T message);
    }*/

    public static void main(String[] args) {
        Func1 func1 = message -> System.out.println(message);
        func1.printMessage("Hello");
        func1.printMessage(100);
    }
}
複製程式碼

元註解

JDK 中雖然內建了幾個註解,但這遠遠不能滿足開發過程中遇到的千變萬化的需求。所以我們需要自定義註解,而這就需要用到元註解。

元註解的作用就是用於定義其它的註解

Java 中提供了以下元註解型別:

  • @Retention
  • @Target
  • @Documented
  • @Inherited(JDK8 引入)
  • @Repeatable(JDK8 引入)

這些型別和它們所支援的類在 java.lang.annotation 包中可以找到。下面我們看一下每個元註解的作用和相應分引數的使用說明。

@Retention

@Retention 指明瞭註解的保留級別。

@Retention 原始碼:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}
複製程式碼

RetentionPolicy 是一個列舉型別,它定義了被 @Retention 修飾的註解所支援的保留級別:

  • RetentionPolicy.SOURCE - 標記的註解僅在原始檔中有效,編譯器會忽略。
  • RetentionPolicy.CLASS - 標記的註解在 class 檔案中有效,JVM 會忽略。
  • RetentionPolicy.RUNTIME - 標記的註解在執行時有效。

@Retention 示例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField";
    public boolean defaultDBValue() default false;
}
複製程式碼

@Documented

@Documented 表示無論何時使用指定的註解,都應使用 Javadoc(預設情況下,註釋不包含在 Javadoc 中)。更多內容可以參考:Javadoc tools page

@Documented 示例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField";
    public boolean defaultDBValue() default false;
}
複製程式碼

@Target

@Target 指定註解可以修飾的元素型別。

@Target 原始碼:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}
複製程式碼

ElementType 是一個列舉型別,它定義了被 @Target 修飾的註解可以應用的範圍:

  • ElementType.ANNOTATION_TYPE - 標記的註解可以應用於註解型別。
  • ElementType.CONSTRUCTOR - 標記的註解可以應用於建構函式。
  • ElementType.FIELD - 標記的註解可以應用於欄位或屬性。
  • ElementType.LOCAL_VARIABLE - 標記的註解可以應用於區域性變數。
  • ElementType.METHOD - 標記的註解可以應用於方法。
  • ElementType.PACKAGE - 標記的註解可以應用於包宣告。
  • ElementType.PARAMETER - 標記的註解可以應用於方法的引數。
  • ElementType.TYPE - 標記的註解可以應用於類的任何元素。

@Target 示例:

@Target(ElementType.TYPE)
public @interface Table {
    /**
     * 資料表名稱註解,預設值為類名稱
     * @return
     */
    public String tableName() default "className";
}

@Target(ElementType.FIELD)
public @interface NoDBColumn {}
複製程式碼

@Inherited

@Inherited 表示註解型別可以被繼承(預設情況下不是這樣)

表示自動繼承註解型別。 如果註解型別宣告中存在 @Inherited 元註解,則註解所修飾類的所有子類都將會繼承此註解。

注意:@Inherited 註解型別是被標註過的類的子類所繼承。類並不從它所實現的介面繼承註解,方法並不從它所覆寫的方法繼承註解。

此外,當 @Inherited 型別標註的註解的 @RetentionRetentionPolicy.RUNTIME,則反射 API 增強了這種繼承性。如果我們使用 java.lang.reflect 去查詢一個 @Inherited 型別的註解時,反射程式碼檢查將展開工作:檢查類和其父類,直到發現指定的註解型別被發現,或者到達類繼承結構的頂層。

@Inherited
public @interface Greeting {
    public enum FontColor{ BULE,RED,GREEN};
    String name();
    FontColor fontColor() default FontColor.GREEN;
}
複製程式碼

@Repeatable

@Repeatable 表示註解可以重複使用。

以 Spring @Scheduled 為例:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Schedules {
	Scheduled[] value();
}

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
  // ...
}
複製程式碼

應用示例:

public class TaskRunner {

    @Scheduled("0 0/15 * * * ?")
    @Scheduled("0 0 12 * ?")
    public void task1() {}
}
複製程式碼

自定義註解

使用 @interface 自定義註解時,自動繼承了 java.lang.annotation.Annotation 介面,由編譯程式自動完成其他細節。在定義註解時,不能繼承其他的註解或介面。@interface 用來宣告一個註解,其中的每一個方法實際上是宣告瞭一個配置引數。方法的名稱就是引數的名稱,返回值型別就是引數的型別(返回值型別只能是基本型別、Class、String、enum)。可以通過 default 來宣告引數的預設值。

這裡,我會通過實現一個名為 RegexValid 的正則校驗註解工具來展示自定義註解的全步驟。

1. 註解的定義

註解的語法格式如下:

public @interface 註解名 {定義體}
複製程式碼

我們來定義一個註解:

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValid {}
複製程式碼

說明:

通過上一節對於元註解 @Target@Retention@Documented 的說明,這裡就很容易理解了。

  • 上面的程式碼中定義了一個名為 @RegexValid 的註解。
  • @Documented 表示 @RegexValid 應該使用 javadoc。
  • @Target({ElementType.FIELD, ElementType.PARAMETER}) 表示 @RegexValid 可以在類成員或方法引數上修飾。
  • @Retention(RetentionPolicy.RUNTIME) 表示 @RegexValid 在執行時有效。

此時,我們已經定義了一個沒有任何屬性的註解,如果到此為止,它僅僅是一個標記註解。作為正則工具,沒有屬性可什麼也做不了。接下來,我們將為它新增註解屬性。

2. 註解屬性

註解屬性的語法形式如下:

[訪問級別修飾符] [資料型別] 名稱() default 預設值;
複製程式碼

例如,我們要定義在註解中定義一個名為 value 的字串屬性,其預設值為空字串,訪問級別為預設級別,那麼應該定義如下:

String value() default "";
複製程式碼

注意:在註解中,我們定義屬性時,屬性名後面需要加 ()

定義註解屬性有以下要點:

  • 註解屬性只能使用 public 或預設訪問級別(即不指定訪問級別修飾符)修飾

  • 註解屬性的資料型別有限制要求。支援的資料型別如下:

    • 所有基本資料型別(byte、char、short、int、long、float、double、boolean)
    • String 型別
    • Class 類
    • enum 型別
    • Annotation 型別
    • 以上所有型別的陣列
  • 註解屬性必須有確定的值,建議指定預設值。註解屬性只能通過指定預設值或使用註解時指定屬性值,相較之下,指定預設值的方式更為可靠。註解屬性如果是引用型別,不可以為 null。這個約束使得註解處理器很難判斷註解屬性是預設值,或是使用註解時所指定的屬性值。為此,我們設定預設值時,一般會定義一些特殊的值,例如空字串或者負數。

  • 如果註解中只有一個屬性值,最好將其命名為 value。因為,指定屬性名為 value,在使用註解時,指定 value 的值可以不指定屬性名稱。

// 這兩種方式效果相同
@RegexValid("^((\\+)?86\\s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))\\d{8}$")
@RegexValid(value = "^((\\+)?86\\s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))\\d{8}$")
複製程式碼

示例:

瞭解了註解屬性的定義要點,讓我們來為 @RegexValid 註解定義幾個屬性。

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValid {
    enum Policy {
        // @formatter:off
        EMPTY(null),
        DATE("^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\\1"
            + "(?:29|30)|(?:0?[13578]|1[02])\\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|"
            + "(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\\2(?:29))$"),
        MAIL("^[A-Za-z0-9](([_\\.\\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([\\.\\-]?[a-zA-Z0-9]+)*)\\.([A-Za-z]{2,})$");
        // @formatter:on

        private String policy;

        Policy(String policy) {
            this.policy = policy;
        }

        public String getPolicy() {
            return policy;
        }
    }

    String value() default "";
    Policy policy() default Policy.EMPTY;
}
複製程式碼

說明:

在上面的示例程式碼中,我們定義了兩個註解屬性:String 型別的 value 屬性和 Policy 列舉型別的 policy 屬性。Policy 列舉中定義了幾個預設的正規表示式,這是為了直接使用這幾個常用表示式去正則校驗。考慮到,我們可能需要自己傳入一些自定義正規表示式去校驗其他場景,所以定義了 value 屬性,允許使用者傳入正規表示式。

至此,@RegexValid 的宣告已經結束。但是,程式仍不知道如何處理 @RegexValid 這個註解。我們還需要定義註解處理器。

3. 註解處理器

如果沒有用來讀取註解的方法和工作,那麼註解也就不會比註釋更有用處了。使用註解的過程中,很重要的一部分就是建立於使用註解處理器。JDK5 擴充套件了反射機制的 API,以幫助程式設計師快速的構造自定義註解處理器。

java.lang.annotation.Annotation 是一個介面,程式可以通過反射來獲取指定程式元素的註解物件,然後通過註解物件來獲取註解裡面的後設資料

Annotation 介面原始碼如下:

public interface Annotation {
    boolean equals(Object obj);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}
複製程式碼

除此之外,Java 中支援註解處理器介面 java.lang.reflect.AnnotatedElement ,該介面代表程式中可以接受註解的程式元素,該介面主要有如下幾個實現類:

  • Class - 類定義
  • Constructor - 構造器定義
  • Field - 累的成員變數定義
  • Method - 類的方法定義
  • Package - 類的包定義

java.lang.reflect 包下主要包含一些實現反射功能的工具類。實際上,java.lang.reflect 包所有提供的反射 API 擴充了讀取執行時註解資訊的能力。當一個註解型別被定義為執行時的註解後,該註解才能是執行時可見,當 class 檔案被裝載時被儲存在 class 檔案中的註解才會被虛擬機器讀取。 AnnotatedElement 介面是所有程式元素(Class、Method 和 Constructor)的父介面,所以程式通過反射獲取了某個類的AnnotatedElement 物件之後,程式就可以呼叫該物件的如下四個個方法來訪問註解資訊:

  • getAnnotation - 返回該程式元素上存在的、指定型別的註解,如果該型別註解不存在,則返回 null。
  • getAnnotations - 返回該程式元素上存在的所有註解。
  • isAnnotationPresent - 判斷該程式元素上是否包含指定型別的註解,存在則返回 true,否則返回 false。
  • getDeclaredAnnotations - 返回直接存在於此元素上的所有註釋。與此介面中的其他方法不同,該方法將忽略繼承的註釋。(如果沒有註釋直接存在於此元素上,則返回長度為零的一個陣列。)該方法的呼叫者可以隨意修改返回的陣列;這不會對其他呼叫者返回的陣列產生任何影響。

瞭解了以上內容,讓我們來實現 @RegexValid 的註解處理器:

import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexValidUtil {
    public static boolean check(Object obj) throws Exception {
        boolean result = true;
        StringBuilder sb = new StringBuilder();
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 判斷成員是否被 @RegexValid 註解所修飾
            if (field.isAnnotationPresent(RegexValid.class)) {
                RegexValid valid = field.getAnnotation(RegexValid.class);

                // 如果 value 為空字串,說明沒有注入自定義正規表示式,改用 policy 屬性
                String value = valid.value();
                if ("".equals(value)) {
                    RegexValid.Policy policy = valid.policy();
                    value = policy.getPolicy();
                }

                // 通過設定 setAccessible(true) 來訪問私有成員
                field.setAccessible(true);
                Object fieldObj = null;
                try {
                    fieldObj = field.get(obj);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                if (fieldObj == null) {
                    sb.append("\n")
                        .append(String.format("%s 類中的 %s 欄位不能為空!", obj.getClass().getName(), field.getName()));
                    result = false;
                } else {
                    if (fieldObj instanceof String) {
                        String text = (String) fieldObj;
                        Pattern p = Pattern.compile(value);
                        Matcher m = p.matcher(text);
                        result = m.matches();
                        if (!result) {
                            sb.append("\n").append(String.format("%s 不是合法的 %s !", text, field.getName()));
                        }
                    } else {
                        sb.append("\n").append(
                            String.format("%s 類中的 %s 欄位不是字串型別,不能使用此註解校驗!", obj.getClass().getName(), field.getName()));
                        result = false;
                    }
                }
            }
        }

        if (sb.length() > 0) {
            throw new Exception(sb.toString());
        }
        return result;
    }
}
複製程式碼

說明:

以上示例中的註解處理器,執行步驟如下:

  1. 通過 getDeclaredFields 反射方法獲取傳入物件的所有成員。
  2. 遍歷成員,使用 isAnnotationPresent 判斷成員是否被指定註解所修飾,如果不是,直接跳過。
  3. 如果成員被註解所修飾,通過 RegexValid valid = field.getAnnotation(RegexValid.class); 這樣的形式獲取,註解例項化物件,然後,就可以使用 valid.value()valid.policy() 這樣的形式獲取註解中設定的屬性值。
  4. 根據屬性值,進行邏輯處理。

4. 使用註解

完成了以上工作,我們就可以使用自定義註解了,示例如下:

public class RegexValidDemo {
    static class User {
        private String name;
        @RegexValid(policy = RegexValid.Policy.DATE)
        private String date;
        @RegexValid(policy = RegexValid.Policy.MAIL)
        private String mail;
        @RegexValid("^((\\+)?86\\s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))\\d{8}$")
        private String phone;

        public User(String name, String date, String mail, String phone) {
            this.name = name;
            this.date = date;
            this.mail = mail;
            this.phone = phone;
        }

        @Override
        public String toString() {
            return "User{" + "name='" + name + '\'' + ", date='" + date + '\'' + ", mail='" + mail + '\'' + ", phone='"
                + phone + '\'' + '}';
        }
    }

    static void printDate(@RegexValid(policy = RegexValid.Policy.DATE) String date){
        System.out.println(date);
    }

    public static void main(String[] args) throws Exception {
        User user = new User("Tom", "1990-01-31", "xxx@163.com", "18612341234");
        User user2 = new User("Jack", "2019-02-29", "sadhgs", "183xxxxxxxx");
        if (RegexValidUtil.check(user)) {
            System.out.println(user + "正則校驗通過");
        }
        if (RegexValidUtil.check(user2)) {
            System.out.println(user2 + "正則校驗通過");
        }
    }
}
複製程式碼

深入理解 Java 註解

參考資料

相關文章