java程式設計思想之註解

我是傳奇哈哈發表於2017-12-23

註解 (後設資料) 為我們在程式碼中新增資訊提供了一種形式化的方法,使我們可以在稍後的某個時刻非常方便的使用這些資料。

註解在一定程度上是在把後設資料與原始碼檔案結合在一起,而不是儲存在外部文件中。註解是眾多引入 javaSE5 中的重要語言變化之一。他們可以提供用來完整地描述程式所需的資訊,而這些資訊是無法用 Java 來表達的。註解可以用來生成描述檔案,甚至或是新的類定義,並且有助於編寫減輕樣板程式碼的負擔。通過使用註解,我們可以將這些後設資料儲存在 Java 原始碼中,並利用 annotation API 為自己的註解構造處理工具,同時,註解的優點還包括:更加乾淨易讀的程式碼以及編譯期型別檢查等。雖然 Java SE5 預先定義了一些後設資料,但一般來說,主要還是需要程式設計師自己新增新的註解,並且按照自己的方式使用它們。

註解的語法比較簡單,除了 @ 符號的使用外,它基本與 Java 固有的語法一樣。Java SE5 內建了三種,定義在 java.lang 中的註解:

  • @Override,表示當前的方法定義將覆蓋超類中的方法。
  • @Deprecated,如果程式設計師使用了註解為它的元素,那麼編譯器會發出警告資訊。
  • @SuppressWarnings,關閉不當的編譯器警告資訊。Java SE5 之前的版本也可以使用這個註解,不過會被忽略不起作用。

每當你建立了描述性質的類或介面時,一旦其中包含了重複性的工作,那就可以考慮使用註解來簡化與自動化該過程。註解是在實際的原始碼級別儲存所有的資訊,而不是某種註釋性的文字,這使得程式碼更整潔,且便於維護。通過使用擴充套件的 annotation API,或外部的位元組碼工具類庫,程式設計師擁有對原始碼以及位元組碼強大的檢查與操作能力。

基本語法

下面示例中,使用 @Test 對 TestExecute() 方法進行註解。這個註解本身並不做任何事情,但是編譯器要確保在其構造路徑上必須有 @Test註解的定義。

public @interface Test {

}

public class Testble {
	public void execute() {
		System.out.println("Executing..");
	}

	@Test
	void testExecute(){
		execute();
	}
}

複製程式碼

被註解的方法與其他的方法沒有區別。@Test 可以與任何修飾符共同作用域方法。

定義註解

上面的例子註解的定義我們已經看到了。註解的定義看起來很像介面的定義。事實上與任何 Java 檔案一樣,註解也會被編譯為 class 檔案。除了 @ 符號以外,@Test 的定義很像一個空的介面。定義註解時會需要一些元註解,如 @Target@Retention@Target 用來定義你的註解將應用於什麼地方。@Deprecated 用來定義應該用於哪一個級別可用,在原始碼中、類檔案中或者執行時。

在註解中一般都會包含某些元素用以表示某些值。當分析出來註解時,程式和工具可以利用這些值。註解的元素看起來就像介面的方法,唯一的區別是你可以為他指定預設值。沒有元素的註解被稱為標記註解。

下面是一個簡單的註解,它可以跟蹤一個專案中的用例。程式設計師可以在該方法上新增註解,我們就可以計算有多少已經實現了該用例。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	public int id();
	public String description() default "沒有描述";
}
複製程式碼

注意:id 和 description 類似方法的定義。description 元素有一個 default 值,如果在註解某個方法時沒有給出 description 的值,則就會使用這個預設值。

下面的三個方法被註解:

public class PasswordUtils {
	@UseCase(id =47,description = "password 哈哈哈防止破解")
	public boolean validatePassword(String password) {
		return (password.matches("\\w*\\d\\w*"));
	}

	@UseCase(id = 48)
	public String encryptPassword(String password) {
		return new StringBuilder(password).reverse().toString();
	}

	@UseCase(id = 49,description = "是否包含在這個密碼庫中")
	public boolean checkForNewPassword(List<String> prevPassword,String password) {
		return !prevPassword.contains(password);
	}
}
複製程式碼

註解的元素在使用時是名值對的形式放入註解的括號內。

元註解

Java 目前只內建了三種標準註解,以及四種元註解。元註解就是註解的註解:

@Target 表示註解可以用在什麼地方。ElementType 的引數包括:
CONSTRUCTOR:構造器的宣告
FIELD:域宣告
LOCAL_VARIABLE:區域性變數宣告
METHOD:方法宣告
PACKAGE:包宣告
PARAMETER:引數宣告
TYPE:類、介面或enum宣告
@Retention 表示需要在什麼級別儲存註解資訊。可選的RententionPolicy引數:
SOURCE:註解將被編譯器丟失
CLASS:註解在class檔案中可用,但會被 vm 丟失
RUNTIME:vm 在執行期也保留註解,因此可以通過反射機制讀取註解的資訊。
@Documented 將此註解包含在 javaDoc 中
@Inherited 允許子類繼承父類中的註解

大多數時候我麼都是編寫位元組的註解,並編寫自己的處理器處理他們。

編寫註解處理器

如果沒有用來讀取註解的工具,那麼註解就不會這有用。使用註解很重要的就是建立和使用註解處理器。Java SE5 擴充套件了反射機制的 API,方便我們構造這種工具。同時還提供了一個外部工具 apt 幫助我們解析帶有註解的 Java 原始碼。

下面我們就用反射來做一個簡單的註解處理器。我們用它來讀取上面的 PasswordUtils 類。

 public class UseCaseTracker {

	public static void trackUseCase(List<Integer> useCase,Class<?> cl) {
		for (Method method : cl.getDeclaredMethods()) {
			UseCase uCase = method.getAnnotation(UseCase.class);
			if (uCase != null) {
				System.out.println("方法上的註解資訊:"+uCase.id()+"  "+uCase.description());
			}
		}

		for (Integer integer : useCase) {
			System.out.println("引數:"+integer);
		}
	}

	public static void main(String[] args) {
		List<Integer> uList = new ArrayList<>();
		Collections.addAll(uList, 47,48,49,50);
		trackUseCase(uList, PasswordUtils.class);
	}

}

複製程式碼

測試結果:

方法上的註解資訊:49  是否包含在這個密碼庫中
方法上的註解資訊:48  沒有描述
方法上的註解資訊:47  password 哈哈哈防止破解
引數:47
引數:48
引數:49
引數:50
複製程式碼

上面用到了兩個反射的方法:getDeclaredMethods() 和 getAnnotation(),getAnnotation() 方法返回指定型別的註解物件,在這裡使用 UseCse。如果被註解的方法上沒有改型別的註解,則返回 null 值。然後我們從返回的 UseCase 物件中提取元素的值。

註解元素

標籤 @UseCase 由 UseCase,java 定義,包含 int 型別的元素 id,以及一個 String 型別的元素 description。註解元素可以使用的型別包括:

  • 所有的基本型別
  • String
  • Class
  • enum
  • Annotation
  • 以上型別的陣列

如果你使用了其他的型別,那麼編譯器會報錯。注意也不允許使用任何包裝型別,但是自動打包存在這也不是什麼限制。註解也可以作為元素的型別,也就是說註解可以巢狀。

預設值限制

編譯器對元素的預設值具有嚴格的限制。首先,元素不能有不確定的值。也就是說元素必須要具有預設的值,要嘛在使用註解時提供元素的值。其次,對於非基本型別的元素,無論在原始碼中宣告時,或是在註解介面中定義時,都不能以 null 作為值。為了繞開這個限制,我們只能定義一些特殊的值,例如;空字串或者是負數表示某個元素不存在:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	public int id() default -1;
	public String description() default " ";
}
複製程式碼

生成外部檔案

假如我們希望提供一些基本的物件關係對映功能,能夠自動生成資料庫表,用以儲存 Javabean 物件。可以選擇使用 XML 描述檔案。然而,如果使用註解的話,可以將所有的資訊儲存在 JavaBean 原始檔中。為此我們需要一個新的註解,用以定義與 Bean 關聯的資料庫表的名字,以及屬性關聯的列明和 SQL 型別。

下面是一個註解的示例,告訴註解處理器,我們需要生成一個資料庫表:

@Retention(RUNTIME)
@Target(TYPE)
public @interface DBtable {
	public String name() default "";
}

複製程式碼

注意:@Target 標籤內可以有多個值用逗號分開,也可以沒有值表示應用所有型別。其中的 name() 元素我們用來為處理器建立資料庫表提供名字。

接下來是修飾 javaBean 物件準備的註解:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Constraints {
	boolean primaryKey() default false;
	boolean allowNull() default true;
	boolean unique() default false;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface SqlString {
	int value() default 0;
	String name() default "";
	Constraints constraints() default @Constraints;
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints;
}
複製程式碼

註解處理器通過 @Constraints 註解提取出資料庫表的後設資料。雖然對於資料庫所能提供的所有約束而言只是一小部分,但足以表達我們的思想。並且我們也為三個元素提供了預設值。另外兩個註解定義的是 SQL 型別。這些 sql 型別具有 name() 元素和 constraints() 元素。後者利用註解巢狀的功能將列的約束資訊嵌入其中。我們看到 @Constraints 註解型別之後沒有指明元素的值而是用一個註解作為預設值。如果要讓嵌入的 @Constraints 註解中的 unique() 元素為 true,並以此作為 constraints() 元素的預設值,則需要如下定義:

@Retention(RUNTIME)
@Target(FIELD)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints(unique = true);
}
複製程式碼

下面使我們的 Bean 的示例:

@DBtable(name = "Member")
public class Member {
	@SqlString(30)
	String firstname;

	@SqlString(50)
	String lasttname;

	@SQLInteger
	Integer age;

	@SqlString(value = 30,constraints = @Constraints(primaryKey=true))
	String handle;

	static int menberCount;

	public String getFirstname() {
		return firstname;
	}

	public String getLasttname() {
		return lasttname;
	}

	public Integer getAge() {
		return age;
	}

	public String getHandle() {
		return handle;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return handle;
	}
}

複製程式碼

類的註解 @DBTable 給定了值 MEMBER,他將會用來作為表的名字。Bean 的屬性 firstname 和 lasttname ,都被註解為 @SqlString 型別,並且為其元素賦值為 30。我們看到這其中使用了快捷方式。如果你的註解中定義了名為 value() 的元素,並且該元素在應用的時候是唯一需要賦值的元素。那麼此時無需使用名值對的語法,只需要在括號內給出 value 的值即可。

註解不支援繼承

不能使用關鍵字 extends 來繼承某個 @interface。很遺憾如果註解支援繼承的話可以大大減少我們打字的工作量,並且使得語法更加整潔。

實現處理器

下面是一個註解處理器的示例,它將讀取一個類檔案,檢查其上的資料庫註解,並生成用來建立資料庫的 SQL 命令:

public class TableCreator {

	public static void main(String[] args) throws Exception{
		 if(args.length < 1) {
		      System.out.println("arguments: annotated classes");
		      System.exit(0);
		  }
		for (String className : args) {
			Class<?> cl = Class.forName(className);
			DBtable dBtable = cl.getAnnotation(DBtable.class);
			if (dBtable != null) {
				System.out.println("這個類沒喲建立資料庫:"+className);
				continue;
			}

			//資料庫的名字
			String tableName = dBtable.name();
			if (tableName.length()<1) {
				//如果名字沒有賦值就用類名並且大寫
				tableName = cl.getName().toUpperCase();
			}
			//查詢出所有的列
			List<String> columnName = new ArrayList<>();
			for (Field field : cl.getDeclaredFields()) {
				String colum = null;
				//獲取物件上的註解
				Annotation[] anns = field.getDeclaredAnnotations();
				if (anns.length <1) {
					continue;
				}

				if (anns[0] instanceof SQLInteger) {
					SQLInteger sInteger = (SQLInteger) anns[0];
					if (sInteger.name().length()<1) {
						colum = field.getName().toUpperCase();
					}else {
						colum = sInteger.name();
					}
					columnName.add(columnName + " INT" +getConstraints(sInteger.constraints()));
				}

				if(anns[0] instanceof SqlString) {
					SqlString sString = (SqlString) anns[0];
			          // Use field name if name not specified.
			          if(sString.name().length() < 1)
			        	  colum = field.getName().toUpperCase();
			          else
			        	  colum = sString.name();
			          columnName.add(columnName + " VARCHAR(" +
			            sString.value() + ")" +
			            getConstraints(sString.constraints()));
			      }

				 StringBuilder createCommand = new StringBuilder(
				          "CREATE TABLE " + tableName + "(");
				        for(String columnDef : columnName)
				          createCommand.append("\n    " + columnDef + ",");
				        // Remove trailing comma
				        String tableCreate = createCommand.substring(
				          0, createCommand.length() - 1) + ");";
				        System.out.println("Table Creation SQL for " +
				          className + " is :\n" + tableCreate);
			}
		}

	}

	private static String getConstraints(Constraints con) {
	    String constraints = "";
	    if(!con.allowNull())
	      constraints += " NOT NULL";
	    if(con.primaryKey())
	      constraints += " PRIMARY KEY";
	    if(con.unique())
	      constraints += " UNIQUE";
	    return constraints;
	  }

}

複製程式碼

我們使用註解來解析構造 sql 語句。上面的示例是非常簡潔的一個例子。對於真正的物件影射資料庫是非常複雜的。現在有很多這樣的框架,可以將物件影射到關聯式資料庫。比如:大名鼎鼎的 greenDAO。

使用 apt 處理註解

註解處理工具 apt,這是 sun 為了幫助註解處理的過程提供的工具。與 Javac 一樣,apt 被設計為操作 Java 的原始檔,而不是編譯後的類。預設情況下 apt 會在處理完原始檔後編譯他們。當註解處理器生成一個新的原始檔時,改檔案會在新一輪的註解處理中接受檢查。該工具會一輪一輪的處理,直到不再有新的原始檔產生。

我們定義的每一個註解都需要自己的處理器,而 apt 工具可以很容易的將多個處理器組合在一起。這樣我們就可以指定多個要處理的類。通過使用 AnnotationProcessorFactory,apt 能夠為每一個它發現的註解生成一個正確的註解處理器。使用 apt 生成註解處理器時,我們無法利用 Java 的反射機制,因為我們操作的是原始碼,而不是編譯後的類。使用 mirror API 能夠解決這個問題,他使得我們能夠在未經編譯的原始碼中檢視方法、物件以及型別。

下面是一個自定義的註解,使用它可以把一個類的 public 方法提取出來,構造成一個新的介面:

@Retention(SOURCE)
@Target(TYPE)
public @interface ExtractInterface {
	public String value();
}
複製程式碼

我們看到 @Retention(SOURCE) 是 SOURCE。因為我們從使用了該註解的類抽取介面之後沒必要在保留這些註解資訊。下面的類有一個公共的方法,我們將把他抽取到一個介面中:

@ExtractInterface("Multiplier")
public class Multiplier {

	public int multiply(int x,int y) {
		int total = 0;
		for (int i = 0; i < x; i++) {
			total = add(total, y);
		}
		return total;
	}

	public int  add(int x,int y) {
		return x+y;
	}

	public static void main(String[] args) {
		Multiplier multiplier = new Multiplier();
		System.out.println("11*16=" + multiplier.multiply(11,16));

	}

}

複製程式碼

測試結果:

11*16=176
複製程式碼

在 Multiplier 類中有一個 multiply() 方法,該方法經過迴圈呼叫私有的 add() 方法實現乘法操作。add() 方法不是公共的,因此不將其作為介面的一部分。註解給了類名作為值,這就是將要生成的介面的名字:

import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.Modifier;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;

import genericity.New;

public class InterfaceExtractorProcessor implements AnnotationProcessor{

	private final AnnotationProcessorEnvironment aenv;

	private ArrayList<MethodDeclaration> interfaceMethods = new ArrayList<>();



	protected InterfaceExtractorProcessor(AnnotationProcessorEnvironment aenv) {
		super();
		this.aenv = aenv;
	}

	@Override
	public void process() {
		for (TypeDeclaration typeeclaration : aenv.getSpecifiedTypeDeclarations()) {
			ExtractInterface annot = typeeclaration.getAnnotation(ExtractInterface.class);
			if (annot == null) {
				break;
			}

			for (MethodDeclaration methodDeclaration : typeeclaration.getMethods()) {
				if (methodDeclaration.getModifiers().contains(Modifier.PUBLIC) && !(methodDeclaration.getModifiers().contains(Modifier.STATIC))) {
					interfaceMethods.add(methodDeclaration);
				}
			}

			if (interfaceMethods.size() >0) {
				try {
					PrintWriter writer = aenv.getFiler().createSourceFile(annot.value());
					 writer.println("package " +
					 typeeclaration.getPackage().getQualifiedName() +";");
					 writer.println("public interface " +
					 annot.value() + " {");
					 for(MethodDeclaration m : interfaceMethods) {
				            writer.print("  public ");
				            writer.print(m.getReturnType() + " ");
				            writer.print(m.getSimpleName() + " (");
				            int i = 0;
				            for(ParameterDeclaration parm :
				              m.getParameters()) {
				              writer.print(parm.getType() + " " +
				                parm.getSimpleName());
				              if(++i < m.getParameters().size())
				                writer.print(", ");
				            }
				            writer.println(");");
				          }
				          writer.println("}");
				          writer.close();

				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
		}
	}

}

複製程式碼

程式中用到的 mirror 的 jar 包可以到下面的地址下載:

http://www.java2s.com/Code/Jar/a/Downloadaptmirrorapi01jar.htm

所有的工作都在 process() 中完成。在分析一個類的時候,我們用 MethodDeclaration 類以及其上的 getModifiers() 方法找到 public 方法。一旦找到就將其儲存到一個 ArrayList 中。然後在一個 .java 檔案中建立新的介面中的方法定義。

注意在構造器中以 AnnotationProcessorEnvironment 物件為引數。通過該物件我們知道 apt 正在處理的所有型別,並且可以通過他獲取 Messager 物件和 Filer 物件。Filer 物件是一種 PrintWriter,我們可以通過他建立新的檔案。不使用普通的 PrintWriter 而是使用 Filer 物件主要原因是:只有這樣 apt 才知道我們建立的新檔案,從而對新檔案進行註解處理,並且在需要的時候編譯他們。

createSourceFile() 方法以將要新建的類或介面名字,開啟了一個普通的輸出流。

apt 工具需要一個工廠類來為其指明正確的處理器,然後它才能呼叫處理器上的 process() 方法:

public class InterfaceExtractorProcessorFactor implements AnnotationProcessorFactory{

	@Override
	public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> arg0,
			AnnotationProcessorEnvironment arg1) {

		return new InterfaceExtractorProcessor(arg1);
	}

	@Override
	public Collection<String> supportedAnnotationTypes() {
		// TODO Auto-generated method stub
		return Collections.singleton("annotations.ExtractInterface");
	}

	@Override
	public Collection<String> supportedOptions() {
		// TODO Auto-generated method stub
		return Collections.emptySet();
	}

}

複製程式碼

AnnotationProcessorFactory 介面只有三個方法。其中 getProcessorFor() 方法註解處理器,該方法包含型別宣告的 Set 以及 AnnotationProcessorEnvironment 物件作為引數。另外兩個方法是 supportedAnnotationTypes() 和 supportedOptions(),可以通過他們檢查一下是否 apt 工具發現的所有的註解都有相應的處理器,是否所有控制檯輸入的引數都是你提供的可選項。

如有疑問,可以關注我。

java程式設計思想之註解

相關文章