Java深度歷險:Java註解

    在開發Java程序,尤爲是Java EE應用的時候,老是免不了與各類配置文件打交道。以Java EE中典型的S(pring)S(truts)H(ibernate)架構來講,Spring、Struts和Hibernate這三個框架都有本身的 XML格式的配置文件。java

annotation使用三步曲,
    1.define annotation interface
    2.define parser annotation class
    3.use annotation and it's parser
數組

        在開發Java程序,尤爲是Java EE應用的時候,老是免不了與各類配置文件打交道。以Java EE中典型的S(pring)S(truts)H(ibernate)架構來講,Spring、Struts和Hibernate這三個框架都有本身的 XML格式的配置文件。這些配置文件須要與Java源代碼保存同步,不然的話就可能出現錯誤。並且這些錯誤有可能到了運行時刻才被發現。把同一份信息保存在兩個地方,老是個壞的主意。理想的狀況是在一個地方維護這些信息就行了。其它部分所需的信息則經過自動的方式來生成。JDK 5中引入了源代碼中的註解(annotation)這一機制。註解使得Java源代碼中不但能夠包含功能性的實現代碼,還能夠添加元數據。註解的功能相似於代碼中的註釋,所不一樣的是註解不是提供代碼功能的說明,而是實現程序功能的重要組成部分。Java註解已經在不少框架中獲得了普遍的使用,用來簡化程序中的配置。架構

使用註解框架

在通常的Java開發中,最常接觸到的可能就是@Override和@SupressWarnings這兩個註解了。使用@Override的時候只須要一個簡單的聲明便可。這種稱爲標記註解(marker annotation ),它的出現就表明了某種配置語義。而其它的註解是能夠有本身的配置參數的。配置參數以名值對的方式出現。使用 @SupressWarnings的時候須要相似@SupressWarnings({"uncheck", "unused"})這樣的語法。在括號裏面的是該註解可供配置的值。因爲這個註解只有一個配置參數,該參數的名稱默認爲value,而且能夠省略。而花括號則表示是數組類型。在JPA中的@Table註解使用相似@Table(name = "Customer", schema = "APP")這樣的語法。從這裏能夠看到名值對的用法。在使用註解時候的配置參數的值必須是編譯時刻的常量。ide

從某種角度來講,能夠把註解當作是一個XML元素,該元素能夠有不一樣的預約義的屬性。而屬性的值是能夠在聲明該元素的時候自行指定的。在代碼中使用註解,就至關於把一部分元數據從XML文件移到了代碼自己之中,在一個地方管理和維護。工具

開發註解ui

在通常的開發中,只須要經過閱讀相關的API文檔來了解每一個註解的配置參數的含義,並在代碼中正確使用便可。在有些狀況下,可能會須要開發本身的註解。這在庫的開發中比較常見。註解的定義有點相似接口。下面的代碼給出了一個簡單的描述代碼分工安排的註解。經過該註解能夠在源代碼中記錄每一個類或接口的分工和進度狀況。this

如下是代碼片斷:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Assignment {
String assignee();
int effort();
double finished() default 0;
}

@interface用來聲明一個註解,其中的每個方法其實是聲明瞭一個配置參數。方法的名稱就是參數的名稱,返回值類型就是參數的類型。能夠經過default來聲明參數的默認值。在這裏能夠看到@Retention和@Target這樣的元註解,用來聲明註解自己的行爲。 @Retention用來聲明註解的保留策略,有CLASS、RUNTIME和SOURCE這三種,分別表示註解保存在類文件、JVM運行時刻和源代碼中。只有當聲明爲RUNTIME的時候,纔可以在運行時刻經過反射API來獲取到註解的信息。@Target用來聲明註解能夠被添加在哪些類型的元素上,如類型、方法和域等。spa

處理註解命令行

在程序中添加的註解,能夠在編譯時刻或是運行時刻來進行處理。在編譯時刻處理的時候,是分紅多趟來進行的。若是在某趟處理中產生了新的Java 源文件,那麼就須要另一趟處理來處理新生成的源文件。如此往復,直到沒有新文件被生成爲止。在完成處理以後,再對Java代碼進行編譯。JDK 5中提供了apt工具用來對註解進行處理。apt是一個命令行工具,與之配套的還有一套用來描述程序語義結構的Mirror API。Mirror API(com.sun.mirror.*)描述的是程序在編譯時刻的靜態結構。經過Mirror API能夠獲取到被註解的Java類型元素的信息,從而提供相應的處理邏輯。具體的處理工做交給apt工具來完成。編寫註解處理器的核心是 AnnotationProcessorFactory和AnnotationProcessor兩個接口。後者表示的是註解處理器,而前者則是爲某些註解類型建立註解處理器的工廠。

以上面的註解Assignment爲例,當每一個開發人員都在源代碼中更新進度的話,就能夠經過一個註解處理器來生成一個項目總體進度的報告。 首先是註解處理器工廠的實現。

如下是代碼片斷:
public class AssignmentApf implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(Set atds,? AnnotationProcessorEnvironment env) {
if (atds.isEmpty()) {
return AnnotationProcessors.NO_OP;
}
return new AssignmentAp(env); //返回註解處理器
}
public Collection supportedAnnotationTypes() {
return Collections.unmodifiableList(Arrays.asList("annotation.Assignment"));
}
public Collection supportedOptions() {
return Collections.emptySet();
}
}
AnnotationProcessorFactory接口有三個方法:getProcessorFor是根據註解的類型來返回特定的註解處理器;supportedAnnotationTypes是返回該工廠生成的註解處理器所能支持的註解類型;supportedOptions用來表示所支持的附加選項。在運行apt命令行工具的時候,能夠經過-A來傳遞額外的參數給註解處理器,如-Averbose=true。當工廠經過 supportedOptions方法聲明瞭所能識別的附加選項以後,註解處理器就能夠在運行時刻經過 AnnotationProcessorEnvironment的getOptions方法獲取到選項的實際值。註解處理器自己的基本實現以下所示。

如下是代碼片斷:
public class AssignmentAp implements AnnotationProcessor {
private AnnotationProcessorEnvironment env;
private AnnotationTypeDeclaration assignmentDeclaration;
public AssignmentAp(AnnotationProcessorEnvironment env) {
this.env = env;
assignmentDeclaration = (AnnotationTypeDeclaration) env.getTypeDeclaration("annotation.Assignment");
}
public void process() {
Collection declarations = env.getDeclarationsAnnotatedWith(assignmentDeclaration);
for (Declaration declaration : declarations) {
processAssignmentAnnotations(declaration);
}
}
private void processAssignmentAnnotations(Declaration declaration) {
Collection annotations = declaration.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().getDeclaration().equals(assignmentDeclaration)) {
Map values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); //獲取註解的值
}
}
}
}

註解處理器的處理邏輯都在process方法中完成。經過一個聲明(Declaration)的getAnnotationMirrors方法就能夠獲取到該聲明上所添加的註解的實際值。獲得這些值以後,處理起來就不難了。

在建立好註解處理器以後,就能夠經過apt命令行工具來對源代碼中的註解進行處理。 命令的運行格式是apt -classpath bin -factory annotation.apt.AssignmentApf src/annotation/work/*.java,即經過-factory來指定註解處理器工廠類的名稱。實際上,apt工具在完成處理以後,會自動調用javac來編譯處理完成後的源代碼。

JDK 5中的apt工具的不足之處在於它是Oracle提供的私有實現。在JDK 6中,經過JSR 269把自定義註解處理器這一功能進行了規範化,有了新的javax.annotation.processing這個新的API。對Mirror API也進行了更新,造成了新的javax.lang.model包。註解處理器的使用也進行了簡化,不須要再單獨運行apt這樣的命令行工具,Java 編譯器自己就能夠完成對註解的處理。對於一樣的功能,若是用JSR 269的作法,只須要一個類就能夠了。

如下是代碼片斷:
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("annotation.Assignment")
public class AssignmentProcess extends AbstractProcessor {
private TypeElement assignmentElement;
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Elements elementUtils = processingEnv.getElementUtils();
assignmentElement = elementUtils.getTypeElement("annotation.Assignment");
}
public boolean process(Set annotations, RoundEnvironment roundEnv) {
Set elements = roundEnv.getElementsAnnotatedWith(assignmentElement);
for (Element element : elements) {
processAssignment(element);
}
}
private void processAssignment(Element element) {
List annotations = element.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().asElement().equals(assignmentElement)) {
Map values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); //獲取註解的值
}
}
}
}
仔細比較上面兩段代碼,能夠發現它們的基本結構是相似的。不一樣之處在於JDK 6中經過元註解@SupportedAnnotationTypes來聲明所支持的註解類型。另外描述程序靜態結構的javax.lang.model包使用了不一樣的類型名稱。使用的時候也更加簡單,只須要經過javac -processor annotation.pap.AssignmentProcess Demo1.java這樣的方式便可。

上面介紹的這兩種作法都是在編譯時刻進行處理的。而有些時候則須要在運行時刻來完成對註解的處理。這個時候就須要用到Java的反射API。反射API提供了在運行時刻讀取註解信息的支持。不過前提是註解的保留策略聲明的是運行時。Java反射API的AnnotatedElement接口提供了獲取類、方法和域上的註解的實用方法。好比獲取到一個Class類對象以後,經過getAnnotation方法就能夠獲取到該類上添加的指定註解類型的註解。

實例分析

下面經過一個具體的實例來分析說明在實踐中如何來使用和處理註解。假定有一個公司的僱員信息系統,從訪問控制的角度出發,對僱員的工資的更新只能由具備特定角色的用戶才能完成。考慮到訪問控制需求的廣泛性,能夠定義一個註解來讓開發人員方便的在代碼中聲明訪問控制權限。

如下是代碼片斷:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredRoles {
String[] value();
}

下一步則是如何對註解進行處理,這裏使用的Java的反射API並結合動態代理。下面是動態代理中的InvocationHandler接口的實現。

如下是代碼片斷: public class AccessInvocationHandler implements InvocationHandler { final T accessObj; public AccessInvocationHandler(T accessObj) { this.accessObj = accessObj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RequiredRoles annotation = method.getAnnotation(RequiredRoles.class); //經過反射API獲取註解 if (annotation != null) { String[] roles = annotation.value(); String role = AccessControl.getCurrentRole(); if (!Arrays.asList(roles).contains(role)) { throw new AccessControlException("The user is not allowed to invoke this method."); } } return method.invoke(accessObj, args); } }
在具體使用的時候,首先要經過Proxy.newProxyInstance方法建立一個EmployeeGateway的接口的代理類,使用該代理類來完成實際的操做。
相關文章
相關標籤/搜索