1. 程式人生 > >Java註解(入門級)

Java註解(入門級)

### Java註解 #### 前言 近日在閱讀開源專案,發現專案裡好多奇奇怪怪的註解(`@DataScope`、`@Log`...)看得我一臉懵,不知道大家是否也有過這樣的經歷,回想了一下,發現自己對於註解的知識,好像只停留在`@Override`。。。異常尷尬,所以今天就補補**註解**這個知識,並把自己的收穫記錄在此,與大家一同交流,如有不對的地方,敬請指正! > 希望本文能給讀者帶來以下收穫: > > - 明白註解是什麼,大概有什麼用 > - 能理解別人程式碼裡面註解的作用 > - 能使用自定義註解 #### 什麼是註解 想要了解某個知識點,我首先推薦的都是去官網檢視,下面看看Java官方對註解的解釋: > *Annotations*, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. > > 註解是元資料的一種形式,它提供有關程式的資料,但這些資料不是程式本身的一部分。註解對它們註釋的程式碼的操作沒有直接影響。 一堆英文讀完,一陣雲裡霧裡。沒關係,這是正常操作,不過我們從翻譯中還是可以瞭解到註解可以提供資料,並且資料是獨立於程式的,那麼我們大致可以推斷出,註解其實是介於程式和資料之間的一種媒介,程式和資料通過註解達成了某種聯絡,即**註解**類似一根紅線,把資料和程式**關聯**在一起。 #### 從`@Override`開始 通過對Java官方提供的註解解釋的翻譯,我們篩選推斷出了一個關鍵資訊——**關聯**。那到底如何理解這個詞呢?別急,我們從最熟悉的陌生人`@Override`開始,最熟悉是因為我們知道這是方法重寫,子類覆蓋父類方法用到的註解,陌生是因為我們從來沒有點進去了解過這個註解,那接下來就進去看看吧! ```java import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } ``` 短短的5行,好像除了第一行,其他啥都不知道。。。不急,我們一行一行來解讀! - 註解匯入了一個`annotation`包 - 註解的“套娃”行為`@Target(ElementType.METHOD)`、`@Retention(RetentionPolicy.SOURCE)` - 不同於介面和類的宣告`public @interface Override { }` 除了對新註解不認識,我們大致可以瞭解到註解的定義格式,`修飾符 @interface 註解名{}`。(有點介面的感覺) #### 禁止套娃——元註解 通過對`@Override`的剖析,我們瞭解了註解的定義格式,不過我們發現註解裡面又有新的註解,本著刨根問底的好奇心,我們繼續進入`@Target`註解一探究竟! ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); } ``` 一直點選,發現始終在`@Documented`、`@Retention`、`@Target`這幾個註解之間套娃,通過Java文件我們瞭解到原來這些修飾註解的註解叫做[元註解](https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html)。元註解(meta-annotation)在`java.lang.annotation`包下: ##### **@Retention** 表示如何儲存被標記的註解(指定儲存級別),有以下三個級別 - RetentionPolicy.SOURCE:只保留到原始碼級別,在編譯階段會被忽略,所以他們不會被寫入位元組碼。 - RetentionPolicy.CLASS:(預設)編譯級別,在編譯時由編譯器保留,但被Java虛擬機器(JVM)忽略。 - RetentionPolicy.RUNTIME:由JVM保留,可以在執行時環境使用。 ##### **@Target** 表示被標記的註解可以用於哪種java元素(類、介面、屬性、方法......),有以下八種 | 作用域 | 解釋 | | --------------------------- | -------------------- | | ElementType.ANNOTATION_TYPE | 可用於註解型別 | | ElementType.CONSTRUCTOR | 可以用於建構函式 | | ElementType.FIELD | 可以用於欄位或者屬性 | | ElementType.LOCAL_VARIABLE | 可以用於區域性變數 | | ElementType.METHOD | 可以用於方法級註解 | | ElementType.PACKAGE | 可以用於包宣告 | | ElementType.PARAMETER | 可以用於方法的引數 | | ElementType.TYPE | 可以用於類的任何元素 | ##### **@Documented** 無論何時使用指定的註解,都應使用Javadoc工具記錄這些元素。(即會在生成的javadoc中加入註解說明) ##### **@Inherited** 可以從超類繼承註釋型別,僅用於類的宣告(介面不會繼承) ##### **@Repeatable** 在Java SE 8中引入的,表示標記的註釋可以多次應用於相同的宣告或型別使用。 #### 註解的分類 通過對元註解的瞭解,我明白了一個註解都是由這些元註解修飾而來,而且我們也收穫了一個重要資訊——**註解可以修飾註解** 這樣無限的套娃,就會有各種各樣的註解,那麼到底有哪些註解呢?常見的註解大致分為以下四類: ##### 元註解 即上文提及的5個元註解 ##### jdk註解 常見的如[`@Override`](https://docs.oracle.com/javase/8/docs/api/java/lang/Override.html) [`@Deprecated`](https://docs.oracle.com/javase/8/docs/api/java/lang/Deprecated.html) [`@SuppressWarnings`](https://docs.oracle.com/javase/8/docs/api/java/lang/SuppressWarnings.html) [`@SafeVarargs`](https://docs.oracle.com/javase/8/docs/api/java/lang/SafeVarargs.html) [`@FunctionalInterface`](https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html) ##### 第三方註解 即第三方框架提供的註解,例如自動注入依賴`@Autowired`、`@Controller`等 ##### 自定義註解 即開發人員根據專案需求自定義的註解,用於一些工具在編譯、執行時進行解析和使用,起到說明、配置的功能。 #### 實戰——定義自己的註解 看過了Java提供的註解,相信你已經對註解有個大致的瞭解了。那你有沒有想過,註解是如何化腐朽為神奇,加了一個簡單的`@Autowired`就能實現依賴注入、`@Setter`就能實現set方法的生成,下面通過簡單的實戰來體會一下註解的神奇之處吧! > 實戰目標: > > 使用自定義註解,通過在實體類及其屬性上加註解,實現對實體類查詢sql語句的構造 > > ps:類似`select * from t_user where t_name='kingwan'`的形式 ##### 自定義註解的編寫規則 在開始實戰之前,我們先了解一下編寫自定義註解的**規則**: - 註解的定義為`@interface`,所有的註解會自動繼承`java.lang.Annotation`這個介面,並且不能再去繼承別的類或者介面 - 引數成員只能用`public`或`default(預設)`訪問許可權符修飾 - 引數成員只能用八大基本資料型別、`String`、`Enum`、`Class`、`annotations`等資料型別,以及這些型別的陣列 - 要獲取類方法和欄位的註解資訊,必須通過java反射機制來獲取 - 註解也可以沒有定義成員(只起到標識作用) 瞭解了註解的定義規範,接下來我們開始進入正式的實戰環節。 ##### **1.自定義註解`@KingwanTable`、`KingwanColumn`** 對於實體類查詢的sql語句,我們需要知道兩個資訊:①查詢的表名②欄位名。並且我們通常習慣將使用者表`t_user`對應於實體類`User`,那麼我們如何和把`t_user`和`User`進行**關聯**呢?一想到關聯,回顧我們最開始從官方文件中提取出來的資訊,沒錯,就是使用註解關聯。接下來定義兩個自定義註解: - `@KingwanTable`:註解實體類對應的表名 ```java @Target(ElementType.TYPE)//作用在類/介面上 @Retention(RetentionPolicy.RUNTIME)//保留作用域:保留到執行時 public @interface KingwanTable { String value();//引數:表名 } ``` - `@KingwanColumn`:註解實體類屬性對應的表字段名 ```java @Target(ElementType.FIELD)//表示作用在欄位上 @Retention(RetentionPolicy.RUNTIME)//保留到執行時 public @interface KingwanColumn { String value();//引數:欄位名 } ``` ##### **2.實體類新增上自定義註解** 有了自定義的兩個註解,那麼我們現在就可以把它們加在實體類上。 - 以下程式碼定義了一個`Student`實體類,加上了`@KingwanTable("t_student")`對映表名,`@KingwanColumn("stu_birth")`對映欄位名。 ```java @Data//簡化實體類的set、get方法 @KingwanTable("t_student") public class Student { @KingwanColumn("stu_name") private String stuName; @KingwanColumn("stu_age") private Integer stuAge; @KingwanColumn("stu_birth") private Date stuBirth; } ``` - 以下程式碼建立了一個student物件,並初始化資訊 ```java public static void main(String[] args) { Student student = new Student(); //初始化資訊 init(student); } private static void init(Student student) { student.setStuName("kingwan"); student.setStuAge(18); student.setStuBirth(new Date()); } ``` ##### **3.反射獲取註解資訊** 有了一個加了自定義註解的Student實體類,那麼我們想要構造SQL,就有以下思路: **獲取到註解的資訊(獲取表名、欄位名)=>獲取屬性的值(欄位值)=>構造SQL** 如何獲取呢?規則裡說了,使用**反射**。 以下程式碼通過獲取`student`的`class`物件,獲取**類上**的註解`@KingwanTable`資訊。 ` aClass.isAnnotationPresent`:判斷指定的註解是否存在 ```java public static void main(String[] args) throws Exception { StringBuffer sql = new StringBuffer("");//即將拼接的SQL語句 Student student = new Student(); //初始化資訊 init(student); //反射獲取class類 Class aClass = student.getClass(); //1. 判斷實體類上是否存在註解@KingwanTable boolean exist = aClass.isAnnotationPresent(KingwanTable.class);//傳入我們自定義的註解類 String tableName = null; if(exist){ //1.1 存在註解即獲取註解值---(表名) KingwanTable annotation = aClass.getAnnotation(KingwanTable.class); tableName = annotation.value(); sql.append("select * from ").append(tableName).append(" where 1=1");//拼接SQL } System.out.println(sql); } ``` 此時SQL列印的結果:![](https://cdn.jsdelivr.net/gh/qinwant/Figurebed/img/20201107%E6%B3%A8%E8%A7%A301.png) 獲取到了類上的註解資訊,接下來我們來看看如何獲取**屬性上**的註解資訊 ```java //2. 獲取屬性上的註解 Field[] fields = aClass.getDeclaredFields(); for (Field field : fields) { //2.1 遍歷每個屬性上是否有KingwanColumn註解 KingwanColumn column = field.getAnnotation(KingwanColumn.class); if( column != null){ //2.1.1 獲取該屬性的值 String fieldName = field.getName();//屬性名 String methodName = "get"+fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);//構造getter方法 Method method = aClass.getMethod(methodName); //通過反射代理呼叫get方法,獲取屬性的值(name='kingwan',age=18....) Object invoke = method.invoke(student); if(invoke instanceof String){ String value = (String) invoke; //sql拼接 sql.append(" and ").append(column.value()).append("=").append("'") .append(value).append("'"); }else{ //想想還有哪些情況... } } System.out.println(sql); } ``` 此時SQL的結果:![](https://cdn.jsdelivr.net/gh/qinwant/Figurebed/img/20201108162827.png) 當然,如果有小夥伴跟著本文敲,可能在這一步就走不下去了,這是因為我們的get方法返回的欄位型別多種多樣,所以僅僅`invoke instanceof String`是不夠的,我們還需要考慮其他情況(`Integer`、`Date`),限於篇幅原因,這裡不做過多介紹,大家完全可以自行補充,如果想了解我的實現思路,移步:[案例原始碼地址](https://github.com/qinwant/note-annotation) 這樣,是不是就達到了我們要的效果了,**對於任意簡單實體類,我們都可以通過加上該註解實現一個簡單的查詢SQL的生成** 你學廢了嗎!:smirk::smirk::smirk: #### 總結 ##### 註解有什麼用 相信大家看我之後可能會有疑問,註解好複雜,費一大堆功夫,還不如直接點呢!的確,我最開始也覺得註解有點雞肋,不過用久了之後,發現真香!而且註解的作用不僅僅這些,本文的目的是讓大家對註解有一個簡單的瞭解,當你看到別人寫的註解是多麼巧妙時,你也許就會發現,原來註解這麼好用! - 在編譯時進行格式檢查。如`@Override ` - 跟蹤程式碼依賴性,實現替代配置檔案功能。通過處理註解資訊生成程式碼、XML檔案。 - 一些註釋可以在執行時進行檢查 ##### 結尾一張圖 一張思維導圖總結一下內容,儲存下來,時常複習! ![](https://cdn.jsdelivr.net/gh/qinwant/Figurebed/img/20201108%E6%B3%A8%E8%A7%A3%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%