高級工程師-Java註解
高級工程師-Java註解
前言
代碼,就是我們身為程序員的名片。
簡潔,優雅,統一,是我們的追求。
優秀的代碼,會給瀏覽者一種藝術的美感。如DL大神的JUC包,感興趣的小夥伴,可以研究一下。
那麼日常中,各位看到的優秀代碼,有着哪些特點呢?充分利用的工具類(lang3,lombok,Validation等等),完善的註解,統一的代碼規範等等。還有的,就是Java語言的諸多高級特性(lambda,stream,io等)。
Java語言中,有三個特性,是高級工程師不可或缺的:
- 註解
- 反射
- 泛型
如果代碼中,存在這些東西,那麼即使應用得還不夠合理,也能夠從側面證明這位程序員的技術追求。
這三點是初級工程師很難掌握的,因為缺乏了解與需求(或者說想不到對應的需求)。而高級工程師為了給出更加具有通用性,業務無侵入的代碼,就常常需要與這些特性打交道。
在不斷積累後的今天,我覺得我可以嘗試寫一寫自己對這些特性的認識了。
今天就從註解開始,闡述我對高級工程師的一些編碼認識。
簡介
我發現很多小夥伴總是在喜歡記憶一些註解的功能,比如表示非空的@NotNull等。
這裡,我要從功能與原理角度說明兩點:
- 功能:註解是一種「增強型」的注釋。只不過相對於只能給人看的注釋,註解可以給電腦(JVM,程序等)看。
- 原理:註解的底層是Annotation接口的繼承者。只不過相對於日常使用的接口,註解需要使用@interface,但是編譯的結果依舊是接口繼承(如TestAnnotation extend Annotation)。
請大家牢記上面兩點,這是有關註解認識的絕對核心。
只要大家抓住這兩個角度去認識註解,那麼很快就可以成為註解達人。後續很多闡述都會從這兩個角度,去為大家解釋。如為什麼人們常說註解是無法繼承的,為什麼需要元註解等等。
註解的目錄結構
其實可以看到,JDK中有關註解的內容很少,非常適合作為三大特性的入門啊。因為註解的實現基礎是存在於JVM中的,JDK只是提供了對應的工具。
Annotation接口
上面提到註解的底層是接口,這裡以圖為證。
注意,仔細看這個接口的注釋。注釋中明確提出,雖然註解的本質是接口。但是直接引用Annotation接口,是無法實現註解功能的。
元註解
簡介
通俗來說,元註解就是註解的註解。
首先元註解,是Java自帶的預置註解。從這個角度,需要與@XXX修飾的自定義註解進行區分。
站在功能上來說,元註解就是專門修飾註解的「注釋」,用來告訴編譯器,虛擬機,相關的信息(如運行時間,目標對象)。
站在原理上來說,元註解也是註解,也是使用了@interface(底層依舊是繼承Annotation接口)。
不過註解,在底層實現已經繼承Annotation接口,那麼就無法通過繼承接口的方式(Java不支持多重繼承),來保存元註解的信息(尤其這個信息往往不止一類)。那麼註解的元註解信息是如何保存,並交給計算機的呢?答案就是通過RuntimeVisibleAnnotations進行相關信息的保存的。以下就是對DynamicPropertyVerification註解反編譯的結果,重點在於反編譯結果的最後一段。
Classfile /D:/IDEA_Project/IdeaProjects/learning/demo/target/classes/tech/jarry/learning/demo/common/anno/DynamicPropertyVerification.class
Last modified Apr 12, 2020; size 899 bytes
MD5 checksum 72657e8b89f0de070bf7085b0dd975da
Compiled from "DynamicPropertyVerification.java"
public interface tech.jarry.learning.demo.common.anno.DynamicPropertyVerification extends java.lang.annotation.Annotation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
#1 = Class #28 // tech/jarry/learning/demo/common/anno/DynamicPropertyVerification
#2 = Class #29 // java/lang/Object
#3 = Class #30 // java/lang/annotation/Annotation
#4 = Utf8 message
#5 = Utf8 ()Ljava/lang/String;
#6 = Utf8 AnnotationDefault
#7 = Utf8 property verification fail
#8 = Utf8 groups
#9 = Utf8 ()[Ljava/lang/Class;
#10 = Utf8 Signature
#11 = Utf8 ()[Ljava/lang/Class<*>;
#12 = Utf8 payload
#13 = Utf8 ()[Ljava/lang/Class<+Ljavax/validation/Payload;>;
#14 = Utf8 SourceFile
#15 = Utf8 DynamicPropertyVerification.java
#16 = Utf8 RuntimeVisibleAnnotations
#17 = Utf8 Ljava/lang/annotation/Documented;
#18 = Utf8 Ljava/lang/annotation/Target;
#19 = Utf8 value
#20 = Utf8 Ljava/lang/annotation/ElementType;
#21 = Utf8 FIELD
#22 = Utf8 Ljava/lang/annotation/Retention;
#23 = Utf8 Ljava/lang/annotation/RetentionPolicy;
#24 = Utf8 SOURCE
#25 = Utf8 Ljavax/validation/Constraint;
#26 = Utf8 validatedBy
#27 = Utf8 Ltech/jarry/learning/demo/common/anno/DynamicPropertyVerificationValidator;
#28 = Utf8 tech/jarry/learning/demo/common/anno/DynamicPropertyVerification
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/annotation/Annotation
{
public abstract java.lang.String message();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: s#7
public abstract java.lang.Class<?>[] groups();
descriptor: ()[Ljava/lang/Class;
flags: ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: []Signature: #11 // ()[Ljava/lang/Class<*>;
public abstract java.lang.Class<? extends javax.validation.Payload>[] payload();
descriptor: ()[Ljava/lang/Class;
flags: ACC_PUBLIC, ACC_ABSTRACT
AnnotationDefault:
default_value: []Signature: #13 // ()[Ljava/lang/Class<+Ljavax/validation/Payload;>;
}
SourceFile: "DynamicPropertyVerification.java"
RuntimeVisibleAnnotations:
0: #17()
1: #18(#19=[e#20.#21])
2: #22(#19=e#23.#24)
3: #25(#26=[c#27])
最後一段,通過RuntimeVisibleAnnotations,保存了所需要的元註解信息。
如果對JVM底層原理有了解的小夥伴,應該對RuntimeVisibleAnnotations不陌生。不了解的小夥伴,可以查看Class RuntimeVisibleAnnotations
預置註解
元註解
元註解是Java自帶的,主要分為:
- @Rentention:表示目標註解的保持策略。其value為RetentionPolicy。如果目標註解沒有使用該註解,則默認使用RetentionPolicy.CLASS
- @Target:表示目標註解的應用目標類型。其value為ElementType。如果目標註解沒有使用該註解,則目標註解可以用於除了TYPE_PARAMETER和TYPE_USE以外的任何地方(這兩個類型都是Java8新添加的)。
- @Documented:表示目標註解可以出現在JavaDoc中。
- @Repeatable:表示目標註解可以在同一位置,重複使用。
- @Inherited:表示目標註解可以隨着所修飾的類的繼承關係,被子類繼承。
@Retention
源碼:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
通過RetentionPolicy枚舉表示目標註解的保持策略。
public enum RetentionPolicy {
/**
* 目標註解會在編譯期丟失
*/
SOURCE,
/**
* 默認行為。雖然目標註解會通過編譯,保存至.class文件中,但是JVM不會在運行時識別該註解。
*/
CLASS,
/**
* 常用行為。目標註解會保存至.class文件中,JVM會在運行時識別,並記錄該註解。所以可以通過反射獲取對應的信息。
* 詳見 java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
為了便於大家理解,這裡再舉一些例子。這裡挑選一些Java自帶的,不用大家再去自己寫demo,增加認知負荷:
- @Retention(RetentionPolicy.SOURCE):如@Override註解,由於該註解只是用於進行代碼檢測,所以只要存在於源碼中即可,故選擇RetentionPolicy.SOURCE。類似的還有@SuppressWarnings註解等。
- @Retention(RetentionPolicy.CLASS):涉及註解處理器,所以實例很少。可以查看自定義註解之編譯時註解(RetentionPolicy.CLASS)(一)。
- @Retention(RetentionPolicy.RUNTIME):如@Deprecated,由於該註解需要在運行時提示用戶註解修飾的方法,類等已經過時,所以需要JVM中有對應「注釋」信息,故採用RetentionPolicy.RUNTIME。類似的還有@Repeatable等。
@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();
}
通過ElementType枚舉表示目標註解的應用目標類型。
public enum ElementType {
/** 類,接口(包括註解,即Annotation接口),或者枚舉類型 */
TYPE,
/** 屬性 (包括枚舉常量,枚舉常量示例:Retention.SOURCE) */
FIELD,
/** 方法 */
METHOD,
/** 形參(形式參數) */
PARAMETER,
/** 構造器 */
CONSTRUCTOR,
/** 本地變量 */
LOCAL_VARIABLE,
/** 註解類型 */
ANNOTATION_TYPE,
/** 包 */
PACKAGE,
/**
* 類型參數(針對數據類型)
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 類型(功能域包含PARAMETER與TYPE_PARAMETER)
* @since 1.8
*/
TYPE_USE
}
這裡不會一一舉例,只會點出重點:
- TYPE_PARAMETER與TYPE_USE是Java8新增加的。所以使用Java7的小夥伴要注意。
- ElementType.TYPE涵蓋範圍很廣泛,在不知用哪個時,可以先用這個。
@Documented
默認情況下,註解是不出現在 javadoc 中的。通過給目標註解加上 @Documented 元註解,能使目標註解出現在 javadoc 中。
從源碼可以看出,@Documented是一個沒有任何成員的標記註解。
@Repeatable
@Repeatable註解的使用,引用一個不錯的demo。
package com.zejian.annotationdemo;
import java.lang.annotation.*;/**
* Created by zejian on 2017/5/20.
*/
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(FilterPaths.class)
public @interface FilterPath {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface FilterPaths {
FilterPath[] value();
}
@FilterPath("/web/update")
@FilterPath("/web/add")
@FilterPath("/web/delete")
上述代碼,其實分為兩個部分:
- 使用@Repeatable註解,使得其修飾的@FilterPath,可以在目標上重複標記(便於設置不同的成員變量)。
- 通過@FilterPaths註解(包含成員變量-FilterPath[] value ()),將@FilterPath集中到@FilterPaths中,便於後續邏輯處理。
@Inherited
@Inherited同樣是只能修飾註解的元註解,它所標註的目標註解具有繼承性。
這裡解釋一下這個繼承性,這並不是註解間的繼承。而是指目標註解可以隨着類的繼承,而被子類繼承。簡單說,就是目標註解修飾的類,其後代類也會被該註解標註(可以通過getAnnotation方法獲取)。
這裡不再贅述,感興趣的小夥伴,可以查看Java 註解(Annotation)中的相關示例。
功能註解
Java預置的功能註解,主要分為:
- @Override:該註解修飾的目標方法,必須是重寫基類方法,或實現對應接口方法,否則編譯器會報錯。
- @Deprecated:該註解修飾的目標,表示已經過時,不推薦使用。編碼時,使用該註解的目標,會有劃線提示。
- @SuppressWarnings:該註解修飾的目標,將會忽略某些異常(由註解的value指定),從而通過編譯器編譯。
- @SafeVarargs:該註解修飾的構造函數(只能修飾構造函數),將會忽略可變參數帶來的警告。該註解於Java7引入。
- @FunctionalInterface:該註解修飾的接口,為函數式接口。如java.util.function下的Consumer
接口,作為一個函數式接口,被該註解修飾(函數式接口不一定有該註解修飾,但被該註解修飾的接口,一定是函數式接口)。
自定義註解
到了這裡,大家應該對註解不再陌生了。
而在日常開發中,我們常常需要自定義開發一些註解。
自定義註解分為以下步驟:
- [必選] 使用@interface來構建自定義註解。一般在創建自定義註解的同時,就達成了該要求。
- [可選] 使用@Target元註解。通過該註解,確定自定義註解的作用目標類型。注意:如果目標註解沒有使用該註解,則目標註解可以用於除了TYPE_PARAMETER和TYPE_USE以外的任何地方(這兩個類型都是Java8新添加的)。
- [可選] 使用@Retention元註解。通過該註解,明確自定義註解的生命周期,或者說自定義註解作用域。如果目標註解沒有使用該註解,則默認使用RetentionPolicy.CLASS
- [可選] 添加成員變量。格式為「long value() default 1000L;」,與Java8的接口成員變量非常類似。注意:註解的成員變量只能採用無參方法表示。並且註解的成員變量,只能採用基本數據類型(char,boolean,byte、short、int、long、float、double)和String、Enum、Class、annotations數據類型,以及這一些類型的數組。
- [可選] 使用自定義註解。自定義註解的使用領域很多,主要分為兩個方向:
- 利用已有框架,不需要自己實現相關邏輯,自定義註解多作為標記註解。如配合SpringBoot的註解,形成自己的註解(相關的邏輯由SpringBoot自己處理)
- 利用已有框架,需要自己實現部分邏輯(不涉及反射),但需要關聯已有框架,並實現對應接口。如Validation框架的自定義校驗註解,感興趣的小夥伴,可以查看我之前寫的Validation框架的應用。
- 可選擇已有框架,需要自己實現諸多邏輯。如在AOP中,我們常常需要通過反射,獲取自定義註解的信息(如參數等),或者自定義註解修飾的目標的信息(如參數,方法名等)。這部分,我會在後續的反射部分詳細說明。
總結
簡單總結一下,本文主要描述了:
- 註解是什麼:增強型的注釋,本質是接口
- 元註解是什麼:註解的註解,作用是為了標識目標註解。包括@Target,@Retention,@Documented,@Repeatable,@Inherited.
- 預置註解是什麼:JDK自帶的經典功能註解,如@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface。
- 自定義註解如何實現:主要分為五步,但是其中必要的步驟,就一步:使用@interface來構建自定義註解。
至此,Java註解的內容就基本展現了。
最後,還是強調兩個方面:
- 註解就是增強型的注釋(可被計算機識別的注釋),本質是接口。把握住這兩點,就非常好理解註解與它的各種規則,行為。
- 註解本身並沒有任何功能(因為它只是注釋,本質也只是接口),需要其他代碼支撐,它才能體現價值。
希望對大家有所幫助,還有不清楚的地方,可以查看下列參考目錄。
願與諸君共進步。
附錄
參考
- Class RuntimeVisibleAnnotations
- 註解之註解的屬性
- Validation框架的應用
- Java 註解(Annotation)
- Android 註解系列之Annotation(二)
- 自定義註解之編譯時註解(RetentionPolicy.CLASS)(一)
- 註解