在開發(fā)Java程序,尤其是Java EE應(yīng)用的時候,總是免不了與各種配置文件打交道。以Java EE中典型的S(pring)S(truts)H(ibernate)架構(gòu)來說,Spring、Struts和Hibernate這三個框架都有自己的XML格式的配置文件。這些配置文件需要與Java源代碼保存同步,否則的話就可能出現(xiàn)錯誤。而且這些錯誤有可能到了運行時刻才被發(fā)現(xiàn)。把同一份信息保存在兩個地方,總是個壞的主意。理想的情況是在一個地方維護這些信息就好了。其它部分所需的信息則通過自動的方式來生成。JDK 5中引入了源代碼中的注解(annotation)這一機制。注解使得Java源代碼中不但可以包含功能性的實現(xiàn)代碼,還可以添加元數(shù)據(jù)。注解的功能類似于代碼中的注釋,所不同的是注解不是提供代碼功能的說明,而是實現(xiàn)程序功能的重要組成部分。Java注解已經(jīng)在很多框架中得到了廣泛的使用,用來簡化程序中的配置。
使用注解
在一般的Java開發(fā)中,最常接觸到的可能就是@Override和@SupressWarnings這兩個注解了。使用@Override的時候只需要一個簡單的聲明即可。這種稱為標(biāo)記注解(marker annotation ),它的出現(xiàn)就代表了某種配置語義。而其它的注解是可以有自己的配置參數(shù)的。配置參數(shù)以名值對的方式出現(xiàn)。使用 @SupressWarnings的時候需要類似@SupressWarnings({"uncheck", "unused"})這樣的語法。在括號里面的是該注解可供配置的值。由于這個注解只有一個配置參數(shù),該參數(shù)的名稱默認為value,并且可以省略。而花括號則表示是數(shù)組類型。在JPA中的@Table注解使用類似@Table(name = "Customer", schema = "APP")這樣的語法。從這里可以看到名值對的用法。在使用注解時候的配置參數(shù)的值必須是編譯時刻的常量。
從某種角度來說,可以把注解看成是一個XML元素,該元素可以有不同的預(yù)定義的屬性。而屬性的值是可以在聲明該元素的時候自行指定的。在代碼中使用注解,就相當(dāng)于把一部分元數(shù)據(jù)從XML文件移到了代碼本身之中,在一個地方管理和維護。
開發(fā)注解
在一般的開發(fā)中,只需要通過閱讀相關(guān)的API文檔來了解每個注解的配置參數(shù)的含義,并在代碼中正確使用即可。在有些情況下,可能會需要開發(fā)自己的注解。這在庫的開發(fā)中比較常見。注解的定義有點類似接口。下面的代碼給出了一個簡單的描述代碼分工安排的注解。通過該注解可以在源代碼中記錄每個類或接口的分工和進度情況。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Assignment {
String assignee();
int effort();
double finished() default 0;
}
@interface用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數(shù)。方法的名稱就是參數(shù)的名稱,返回值類型就是參數(shù)的類型。可以通過default來聲明參數(shù)的默認值。在這里可以看到@Retention和@Target這樣的元注解,用來聲明注解本身的行為。@Retention用來聲明注解的保留策略,有CLASS、RUNTIME和SOURCE這三種,分別表示注解保存在類文件、JVM運行時刻和源代碼中。只有當(dāng)聲明為RUNTIME的時候,才能夠在運行時刻通過反射API來獲取到注解的信息。@Target用來聲明注解可以被添加在哪些類型的元素上,如類型、方法和域等。
在程序中添加的注解,可以在編譯時刻或是運行時刻來進行處理。在編譯時刻處理的時候,是分成多趟來進行的。如果在某趟處理中產(chǎn)生了新的Java源文件,那么就需要另外一趟處理來處理新生成的源文件。如此往復(fù),直到?jīng)]有新文件被生成為止。在完成處理之后,再對Java代碼進行編譯。JDK 5中提供了apt工具用來對注解進行處理。apt是一個命令行工具,與之配套的還有一套用來描述程序語義結(jié)構(gòu)的Mirror API。Mirror API(com.sun.mirror.*)描述的是程序在編譯時刻的靜態(tài)結(jié)構(gòu)。通過Mirror API可以獲取到被注解的Java類型元素的信息,從而提供相應(yīng)的處理邏輯。具體的處理工作交給apt工具來完成。編寫注解處理器的核心是AnnotationProcessorFactory和AnnotationProcessor兩個接口。后者表示的是注解處理器,而前者則是為某些注解類型創(chuàng)建注解處理器的工廠。
以上面的注解Assignment為例,當(dāng)每個開發(fā)人員都在源代碼中更新進度的話,就可以通過一個注解處理器來生成一個項目整體進度的報告。 首先是注解處理器工廠的實現(xiàn)。
public class AssignmentApf implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds,? AnnotationProcessorEnvironment env) {
if (atds.isEmpty()) {
return AnnotationProcessors.NO_OP;
}
return new AssignmentAp(env); //返回注解處理器
}
public Collection<String> supportedAnnotationTypes() {
return Collections.unmodifiableList(Arrays.asList("annotation.Assignment"));
}
public Collection<String> supportedOptions() {
return Collections.emptySet();
}
}
AnnotationProcessorFactory接口有三個方法:getProcessorFor是根據(jù)注解的類型來返回特定的注解處理器;supportedAnnotationTypes是返回該工廠生成的注解處理器所能支持的注解類型;supportedOptions用來表示所支持的附加選項。在運行apt命令行工具的時候,可以通過-A來傳遞額外的參數(shù)給注解處理器,如-Averbose=true。當(dāng)工廠通過 supportedOptions方法聲明了所能識別的附加選項之后,注解處理器就可以在運行時刻通過AnnotationProcessorEnvironment的getOptions方法獲取到選項的實際值。注解處理器本身的基本實現(xiàn)如下所示。
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<Declaration> declarations = env.getDeclarationsAnnotatedWith(assignmentDeclaration);
for (Declaration declaration : declarations) {
processAssignmentAnnotations(declaration);
}
}
private void processAssignmentAnnotations(Declaration declaration) {
Collection<AnnotationMirror> annotations = declaration.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().getDeclaration().equals(assignmentDeclaration)) {
Map<AnnotationTypeElementDeclaration, AnnotationValue> values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); //獲取注解的值
}
}
}
}
注解處理器的處理邏輯都在process方法中完成。通過一個聲明(Declaration)的getAnnotationMirrors方法就可以獲取到該聲明上所添加的注解的實際值。得到這些值之后,處理起來就不難了。
在創(chuàng)建好注解處理器之后,就可以通過apt命令行工具來對源代碼中的注解進行處理。 命令的運行格式是apt -classpath bin -factory annotation.apt.AssignmentApf src/annotation/work/*.java,即通過-factory來指定注解處理器工廠類的名稱。實際上,apt工具在完成處理之后,會自動調(diào)用javac來編譯處理完成后的源代碼。
JDK 5中的apt工具的不足之處在于它是Oracle提供的私有實現(xiàn)。在JDK 6中,通過JSR 269把自定義注解處理器這一功能進行了規(guī)范化,有了新的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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(assignmentElement);
for (Element element : elements) {
processAssignment(element);
}
}
private void processAssignment(Element element) {
List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().asElement().equals(assignmentElement)) {
Map<? extends ExecutableElement, ? extends AnnotationValue> values = mirror.getElementValues();
String assignee = (String) getAnnotationValue(values, "assignee"); //獲取注解的值
}
}
}
}
仔細比較上面兩段代碼,可以發(fā)現(xiàn)它們的基本結(jié)構(gòu)是類似的。不同之處在于JDK 6中通過元注解@SupportedAnnotationTypes來聲明所支持的注解類型。另外描述程序靜態(tài)結(jié)構(gòu)的javax.lang.model包使用了不同的類型名稱。使用的時候也更加簡單,只需要通過javac -processor annotation.pap.AssignmentProcess Demo1.java這樣的方式即可。
上面介紹的這兩種做法都是在編譯時刻進行處理的。而有些時候則需要在運行時刻來完成對注解的處理。這個時候就需要用到Java的反射API。反射API提供了在運行時刻讀取注解信息的支持。不過前提是注解的保留策略聲明的是運行時。Java反射API的AnnotatedElement接口提供了獲取類、方法和域上的注解的實用方法。比如獲取到一個Class類對象之后,通過getAnnotation方法就可以獲取到該類上添加的指定注解類型的注解。
實例分析
下面通過一個具體的實例來分析說明在實踐中如何來使用和處理注解。假定有一個公司的雇員信息系統(tǒng),從訪問控制的角度出發(fā),對雇員的工資的更新只能由具有特定角色的用戶才能完成。考慮到訪問控制需求的普遍性,可以定義一個注解來讓開發(fā)人員方便的在代碼中聲明訪問控制權(quán)限。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredRoles
{ String[] value(); }
下一步則是如何對注解進行處理,這里使用的Java的反射API并結(jié)合動態(tài)代理。下面是動態(tài)代理中的InvocationHandler接口的實現(xiàn)。
public class AccessInvocationHandler<T> 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方法創(chuàng)建一個EmployeeGateway的接口的代理類,使用該代理類來完成實際的操作。