java編程思想之註解

註解 (元數據) 爲咱們在代碼中添加信息提供了一種形式化的方法,使咱們能夠在稍後的某個時刻很是方便的使用這些數據。java

註解在必定程度上是在把元數據與源代碼文件結合在一塊兒,而不是保存在外部文檔中。註解是衆多引入 javaSE5 中的重要語言變化之一。他們能夠提供用來完整地描述程序所需的信息,而這些信息是沒法用 Java 來表達的。註解能夠用來生成描述文件,甚至或是新的類定義,而且有助於編寫減輕樣板代碼的負擔。經過使用註解,咱們能夠將這些元數據保存在 Java 源代碼中,並利用 annotation API 爲本身的註解構造處理工具,同時,註解的優勢還包括:更加乾淨易讀的代碼以及編譯期類型檢查等。雖然 Java SE5 預先定義了一些元數據,但通常來講,主要仍是須要程序員本身添加新的註解,而且按照本身的方式使用它們。程序員

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

  • @Override,表示當前的方法定義將覆蓋超類中的方法。
  • @Deprecated,若是程序員使用了註解爲它的元素,那麼編譯器會發出警告信息。
  • @SuppressWarnings,關閉不當的編譯器警告信息。Java SE5 以前的版本也可使用這個註解,不過會被忽略不起做用。

每當你建立了描述性質的類或接口時,一旦其中包含了重複性的工做,那就能夠考慮使用註解來簡化與自動化該過程。註解是在實際的源代碼級別保存全部的信息,而不是某種註釋性的文字,這使得代碼更整潔,且便於維護。經過使用擴展的 annotation API,或外部的字節碼工具類庫,程序員擁有對源代碼以及字節碼強大的檢查與操做能力。數據庫

基本語法

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

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 用來定義應該用於哪個級別可用,在源代碼中、類文件中或者運行時。bash

在註解中通常都會包含某些元素用以表示某些值。當分析出來註解時,程序和工具能夠利用這些值。註解的元素看起來就像接口的方法,惟一的區別是你能夠爲他指定默認值。沒有元素的註解被稱爲標記註解。app

下面是一個簡單的註解,它能夠跟蹤一個項目中的用例。程序員能夠在該方法上添加註解,咱們就能夠計算有多少已經實現了該用例。框架

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

注意:id 和 description 相似方法的定義。description 元素有一個 default 值,若是在註解某個方法時沒有給出 description 的值,則就會使用這個默認值。ide

下面的三個方法被註解:

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 工具發現的全部的註解都有相應的處理器,是否全部控制檯輸入的參數都是你提供的可選項。

若有疑問,能夠關注我。

相關文章
相關標籤/搜索