關於JAVA中原始碼級註解的編寫及使用
一、註解簡介:
1.1.什麼是“註解”:
在我們編寫程式碼時,一定看到過這樣的程式碼:
class Student {
private String name;
@Override
public String toString(String str) {//編譯錯誤!
return "Student name = " + name;
}
}
其中的@Override,就是一個“註解”,@Override一般出現在重寫equals()或者toString()方法的上邊,意思是告訴編譯器:下邊的程式碼是重寫父類方法的。這時編譯器會按照“重寫”的語法嚴格檢查下面的方法,如果不符合重寫語法,將會編譯錯誤。
"註解"作為一種“標記”,被寫在原始碼中,不會改變程式的執行流程。它通常由“註解解析工具”來解析,而“註解解析器”可以隨Java編譯器啟動,也可以獨立啟動,來解析註解,並以此可以做一些事情。
1.2.註解的分類:
原始碼註解:
註解只在原始碼中,編譯成class檔案後就不存在了。
編譯時註解:
註解在原始碼和.class檔案中都存在(如:JDK內建系統註解)
執行時註解:
在執行階段還起作用,甚至會影響執行邏輯的註解(如:JUnit的@Test)
1.3.註解的作用
註解的作用非常廣泛,註解可以被用在類、屬性、構造方法、成員方法、區域性變數等位置,用於對這些元素進行說明。由“註解解析工具”解析後,可以生成文件、進行程式碼分析、編譯檢查等。
二、自定義註解:
2.1.定義註解的基本語法
“註解”本質上是一個“類”,我們可以根據自己的需要定義自己的註解。
定義註解的語法很簡單:
public @interface CheckWord{
...
}
"註解”編譯後會生成.class檔案。但這是一個非常簡單的註解,它可以被用在任何位置,而且編譯器遇到這種註解也不做任何事情。例如:
@CheckWord
public class Student {
@CheckWord
public Student() {
}
@CheckWord
private String name;
@CheckWord
public void study() {
}
}
下面我們先使用“元註解”來規定這個註解可以被用在哪裡。
2.2.元註解
“元註解”也是一種“註解”,它是已經實現好的。必須用在“註解”的定義上,它可以規定註解可以用在哪裡,以及可以存在於原始碼中,或者class中,或者執行時。
常用的“元註解”有兩個:
1).@Target : 規定註解可以用在哪裡。常用的取值被定義在列舉java.lang.annotation.ElementType中:
ElementType.TYPE:類和介面上
ElementType.FIELD: 用在成員變數上
ElementType.METHOD: 用在方法上
ElementType.PARAMETER: 用在引數上
ElementType.CONSTRUCTOR: 用在構造方法上
ElementType.LOCAL_VARIABLE: 用在區域性變數上
2).@Retention : 規定註解可以存在於哪裡。常用的取值被定義在列舉java.lang.annotation.RetentionPolicy中:
RetentionPolicy.SOURCE: 規定註解只存在於Java原始碼中, 編譯生成的位元組碼檔案中就不存在了。
RetentionPolicy.CLASS: 規定註解存在於Java原始碼、 編譯以後的位元組碼檔案中, 但JVM執行時,不會被載入到記憶體。
RetentionPolicy.RUNTIME: 規定註解存在於Java原始碼中、 編譯以後的位元組碼檔案中、 執行時記憶體中, 程式可以通過反射獲取該註解。
例如:修改我們的註解,規定它只能用在"類","欄位",“方法”上,並且可以存在於“原始碼中”:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface CheckWord {
}
如果再編譯之前的Student類,會發現用在"構造方法"上的@CheckWord會編譯錯誤,因為我們規定了它只能用在"類","欄位","方法"上。
2.3.定義註解的屬性:
1.“註解”中可以定義一些屬性,“註解解析器”可以根據“屬性”的不同,分別做不同的事情。
例如@Target註解中的ElementType.TYPE就是此註解的一個屬性,它是一個"列舉"型別。
下面讓我們來看看怎樣定義屬性,然後再解析這些屬性。
註解中定義屬性的語法:資料型別 屬性名() [deafult 值];
1.其中“資料型別”可以是:
1).所有基本型別;
2).String;
3).Class;
4).列舉;
5).註解;
6).以上任一型別的陣列
2.屬性名():屬性名可以自由設定,要遵循Java識別符號的命名規則;其中的一對()是必須的。
3.[default 值]:為此屬性設定的預設值。
2.本例中由於只檢查大小寫,為了規範取值,所以定義一個"列舉"型別的屬性。
1).先定義列舉:
public enum StartsWith {
UPPERCASE, LOWERCASE
}
此列舉定義了兩個值:UPPERCASE表示:大寫;LOWERCASE表示:小寫。
2).修改"CheckWord"註解的程式碼:
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface CheckWord {
StartsWith value();
}
說明:
a.StartsWith表示"資料型別",是一個"列舉"型別。
b.value表示"屬性名",在使用此註解時,此屬性的可取值只有StartsWith.UPPERCASE和StartsWith.LOWERCASE兩個。
c.此屬性沒有設定"預設值",在使用此註解時必須要設定此屬性的值。如下面的程式碼:
3).修改"Student"類的程式碼:
@CheckWord(StartsWith.UPPERCASE)
public class Student {
@CheckWord(StartsWith.LOWERCASE)
private String stuName;
@CheckWord(StartsWith.LOWERCASE)
public void show() {
}
}
2.4註解解析器:
1."註解解析器"通常是隨著註解一起定義的,用於解析"註解",並做一些事情。本例的"註解解析器"用於與javac編譯器一起啟動,編譯Student類時,檢查各元素的名稱是否按要求以指定的大寫、小寫字母開頭。
2.自定義"註解解析器"需要繼承AbstractProcessor類,並重寫process()方法,完整的"註解解析器"程式碼如下:
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;
@SupportedAnnotationTypes("CheckWord")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//獲取所有使用了@CheckWord註解的元素
Set<? extends Element> annoEle = roundEnv.getElementsAnnotatedWith(CheckWord.class);
// 遍歷這些元素
for (Element e : annoEle) {
//獲取元素名稱,可能是:類名、屬性名、方法名
String name = e.getSimpleName().toString();
//獲取這個名字的第一個字母
char c = name.charAt(0);
//獲取這個元素上的@CheckWord註解物件
CheckWord anno = e.getAnnotation(CheckWord.class);
//獲取這個註解的value屬性的值,它是一個StartsWith列舉型別
StartsWith sw = anno.value();
//判斷屬性值是否設定為:StartsWith.UPPERCASE,但名字的首字母是小寫
if (sw == StartsWith.UPPERCASE && Character.isLowerCase(c)) {
//向控制檯列印異常資訊
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "名稱:" + name + " 首字母應該大寫!");
return false;
}
//判斷屬性值是否設定為:StartsWith.LOWERCASE,但名字的首字母是大寫
if (sw == StartsWith.LOWERCASE && Character.isUpperCase(c)) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "名稱:" + name + " 首字母應該小寫!");
return false;
}
}
return true;
}
}
此程式碼的細節大家可以根據註釋一點一點研究。一些類:TypeElement,RoundEnvironment,Element等的一些方法大家可以在API手冊中查詢。
其它說明:
@SupportedAnnotationTypes("CheckWord") : 表示只處理CheckWord註解。
@SupportedSourceVersion(SourceVersion.RELEASE_8) : 表示支援JDK1.8。
2.5.編譯和測試:
1.在編譯前,我們看一下完整的程式碼清單:請確保以下的四個類在同一個目錄下
1).列舉類:
public enum StartsWith {
UPPERCASE, LOWERCASE
}
2).自定義註解類:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface CheckWord {
StartsWith value();
}
3).使用了CheckWord註解的Student類:
@CheckWord(StartsWith.UPPERCASE)
public class Student {
@CheckWord(StartsWith.LOWERCASE)
private String StuName;
@CheckWord(StartsWith.LOWERCASE)
public void show() {
}
}
4).註解解析器類:
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;
@SupportedAnnotationTypes("CheckWord")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> annoEle = roundEnv.getElementsAnnotatedWith(CheckWord.class);
for (Element e : annoEle) {
String name = e.getSimpleName().toString();
char c = name.charAt(0);
CheckWord anno = e.getAnnotation(CheckWord.class);
StartsWith sw = anno.value();
if (sw == StartsWith.UPPERCASE) {
if (Character.isLowerCase(c)) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "名稱:" + name + " 首字母應該大寫!");
return false;
}
}
if (sw == StartsWith.LOWERCASE) {
if (Character.isUpperCase(c)) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "名稱:" + name + " 首字母應該小寫!");
return false;
}
}
}
return true;
}
}
2.啟動命令列,使用javac依次進行編譯:
javac StartsWith.java
javac CheckWord.java
javac MyProcessor.java(如果報錯: 編碼GBK的不可對映字元,是因為程式碼中的中文,可以使用javac -encoding UTF-8 MyProcessor.java進行編譯)
接下來使用MyProcessor解析器編譯Student:
javac -processor MyProcessor Student.java
執行命令後,會有錯誤提示:
錯誤: 名稱:StuName 首字母應該小寫!
1 個錯誤
三、總結:
原始碼級註解的應用非常廣泛,例如:進行程式碼檢查、生成新類、生成檔案。本文實現了基本的程式碼檢查,用於檢查類中的元素是否按照要求進行首字母大寫或者小寫。也可以根據需要,驗證是否全部大寫,或者全部小寫。希望大家通過本案例能夠了解原始碼級註解的編寫及使用