8.4.4 处理注解

当注解被添加到Java源代码中之后,并不会自动产生作用。某些注解甚至不会在字节代码中出现。创建和使用注解只完成了第一步,更重要的是如何对注解进行处理。在一般情况下,创建和处理注解是Java标准库和第三方库应该做的事情,开发人员只需要使用注解即可。以前面提到的Override注解为例,Override注解类型是Java标准库提供的,对它的处理由编译器负责。开发人员只需要在方法声明上添加@Override的声明来使用它即可。编译器会负责在编译时检查该方法是否的确覆写了父类型中的方法。如果程序中创建了特有的注解,就需要对它进行处理。了解处理注解的方式,对于框架和类库的开发人员来说尤其重要。在一般应用程序开发中也可能需要处理注解。

处理注解通常有两种方式:一种是在编译时,另外一种是在运行时。对于保留策略为SOURCE的注解类型来说,编译时是唯一的处理方式。如果注解声明在运行时仍然保留,那么可以通过反射API动态地处理。

在编译时处理注解的工作由编译器来完成。对于Java标准库中的Override、SuppressWarnings和Deprecated等注解,编译器知道如何进行处理。而对于程序中自定义的注解类型,则需要程序提供相应的处理器,由编译器在编译时调用。从注解类型被引入的J2SE 5.0到Java 7,自定义注解类型的处理方式发生了很多变化。J2SE 5.0使用的是apt工具和Oracle私有的Mirror API来进行处理。Java SE 6引入了可插拔式注解处理机制及相应的javax.annotation.processing和javax.lang.model包,另外javac工具也提供了对注解处理的支持,不再需要使用apt工具。Java SE 7则把J2SE 5.0中的apt工具和对应的Mirror API声明为被废弃的,不推荐使用。本节主要介绍的是Java SE 6中引入的新的注解处理机制,也是推荐使用的注解处理方式。

1.可插拔式注解处理机制

Java SE 6中通过JSR 269(Pluggable Annotation Processing API)对注解处理机制进行了标准化。JSR 269主要包括两个部分,一部分是进行注解处理的javax.annotation. processing包,另一部分是对程序的静态结构进行建模的javax.lang.model包。这两部分API是相辅相成的:javax.annotation.processing包用来完成实际的注解处理,在处理过程中需要了解程序中被注解的元素的信息。这部分信息由javax.lang.model包来表示。对javax.lang.model包的介绍会在说明javax.annotation.processing包时穿插进行。

JSR 269的注解处理机制的重要特征是采用可插拔的设计方式。由底层的工具提供基本的框架和运行环境,开发人员编写的注解处理功能作为插件嵌入到此框架之中,并利用框架提供的功能完成所需的处理。对于注解的处理分多轮来进行。在每轮处理中,注解处理器会对源代码和字节代码文件中发现的部分注解进行处理,并可能产生新的源代码和字节代码文件。上一轮处理的输出作为下一轮处理的输入,按照顺序来完成。第一轮处理的输入由运行编译器时的参数来确定。

所有的注解处理器都需要实现javax.annotation.processing.Processor接口。注解处理器一般选择继承自javax.annotation.processing.AbstractProcessor类。所有实现Processor接口的类需要提供一个公开的不带参数的构造方法。注解处理框架会使用此构造方法来创建处理器的对象实例。实例创建完成后,Processor接口的init方法会被调用,以完成处理器的初始化。init方法声明中包含类型为javax.annotation.processing.ProcessingEnvironment接口的参数。ProcessingEnvironment接口表示注解处理时的运行环境,提供了一些方法来创建新文件、报告错误以及获取其他实用工具类。注解处理器的实现对象通过ProcessingEnvironment接口与框架进行交互。在调用init方法时,ProcessingEnvironment接口的实现对象由框架提供。一般把此对象保留下来,供后续操作使用。接着框架调用Processor接口中的方法来获取一些与注解处理器相关的信息,这些方法包括获取处理器支持的注解类型的getSupportedAnnotationTypes方法、处理器支持的最高源代码版本号的getSupportedSourceVersion方法和处理器所能识别的选项名称的getSupportedOptions方法。这些方法对一个处理器实例只会调用一次。在每轮注解处理过程中,框架会根据当前源代码中具有的注解声明和每个处理器所能处理的注解类型来确定由哪些处理器来处理。被选中的处理器的process方法会被调用。在调用时,process方法的第一个参数表示的是待处理的注解类型,第二个参数表示的是可以用来获取本轮处理的相关环境信息的javax.annotation.processing.RoundEnvironment接口的实现对象。当没有可供处理的注解声明或是其他匹配的处理器时,本轮处理结束。当所有轮处理都完成时,整个注解处理过程结束。

下面通过一个简单的示例开始对注解处理器的介绍。需要处理的注解是代码清单8-23中的Author。Author注解被添加在Java源代码中的类或接口上,提供类或接口的作者的相关信息。对Author注解进行处理的目的统计每个作者所创建的类或接口的数量。代码清单8-29中给出了完整的处理器实现。AuthorProcessor类继承自AbstractProcessor类。AbstractProcessor类对Processor接口的大部分方法都提供了默认实现,继承者只需要实现process方法即可。在AbstractProcessor类的init方法中,把之后的处理中需要用到的ProcessingEnvironment接口的实现对象保存在受保护的域processingEnv中,AbstractProcessor类的子类可以直接使用该域来引用这个对象。在AuthorProcessor类上添加了注解javax.annotation.processing.SupportedSourceVersion和javax.annotation.processing.SupportedAnnotationTypes。AbstractProcessor类可以处理这些注解,并作为Processor接口中getSupportedSourceVersion和getSupportedAnnotationTypes方法的实现。同样AbstractProcessor类还支持javax.annotation.processing.SupportedOptions注解作为getSupportedOptions方法的返回值。AbstractProcessor类的这个能力简化了对注解处理器所支持的最高源代码版本号、所支持的注解类型和所能识别的额外选项这三个配置的处理。对AuthorProcessor类来说,支持的最高源代码版本是7,而支持的注解类型只有一种,即com.java7book.chapter8.annotation.Author。在声明所支持的注解类型时,可以使用通配符“”。单个“”字符表示可以处理所有的注解类型。

在process方法的实现中,通过RoundEnvironment接口可以获取与本轮处理相关的信息。其中,通过getRootElements方法可以获取前一轮处理完成之后的可供处理的程序元素集合。程序元素由javax.lang.model.element.Element接口表示。Java源代码中包含的各种静态结构都由此接口或其子接口来表示。在RoundEnvironment接口提供的方法中,比较常用的是getElementsAnnotatedWith方法,用来获取包含指定注解类型声明的元素的集合。对于每个包含注解类型声明的元素,使用getAnnotationMirrors方法可以得到它所包含的所有注解。每个注解用javax.lang.model.element.AnnotationMirror接口表示。遍历所有这些注解,根据名称找到其中的Author注解。通过AnnotationMirror接口的getElementValues方法可以获取注解中包含的所有配置元素的值。遍历这些配置元素,可以找到元素name的值,即作者的姓名,之后即可对作者姓名进行统计。

由于注解处理的过程可能要经过多轮来完成,因此一个处理器的process方法会被多次调用。如果在某轮处理中,一个处理器被调用了,那么在后续的每轮处理中,即便没有注解可供该处理器来处理,也会调用该处理器。在process方法中可以通过RoundEnvironment接口的processingOver方法进行判断。在一轮处理中,如果processingOver方法返回true,则说明这是处理的最后一轮。如果本轮的处理过程依赖前一轮的执行结果,那么通过RoundEnvironment接口的errorRaised方法可以判断前一轮的处理是否出现错误。需要注意process方法的返回值的使用。如果返回值为true,说明对这些注解类型的处理由当前处理器独占完成,其他的处理器不会再进行处理;如果返回值为false,则其他处理器仍然有机会进行处理。如果某个处理器声明的注解处理类型是“*”,同时又在process方法中返回true,则相当于独占处理了所有的注解类型。其他处理器都不会得到处理的机会。

代码清单8-29 Author注解的处理器


@SupportedSourceVersion(SourceVersion.RELEASE_7)

@SupportedAnnotationTypes("com.java7book.chapter8.annotation.Author")


public class AuthorProcessor extends AbstractProcessor{

private Map<String, Integer>countMap=new HashMap<>();

private TypeElement authorElement;

public synchronized void init(ProcessingEnvironment processingEnv){

super.init(processingEnv);

Elements elementUtils=processingEnv.getElementUtils();

authorElement=elementUtils.getTypeElement("com.java7book.chapter8.annotation.Author");

}

public boolean process(Set<?extends TypeElement>annotations,

RoundEnvironment roundEnv){

Set<?extends Element>elements=roundEnv.getElementsAnnotatedWith(autho rElement);

for(Element element:elements){

processAuthor(element);

}

if(roundEnv.processingOver()){

for(Map.Entry<String, Integer>entry:countMap.entrySet()){

System.out.println(entry.getKey()+"==>"+entry.getValue());

}

}

return true;

}

private void processAuthor(Element element){

List<?extends Annotation Mirror>annotations=element.getAnnotationMirrors();

for(AnnotationMirror mirror:annotations){

if(mirror.getAnnotationType().asElement().equals(authorElement)){

String name=(String)getAnnotationValue(mirror,"name");

if(!countMap.containsKey(name)){

countMap.put(name,1);

}

else{

countMap.put(name, countMap.get(name)+1);

}

}

}

}

private Object getAnnotationValue(AnnotationMirror mirror, String name){

Map<?extends ExecutableElement,?extends AnnotationValue>values=mirror.getElementValues();

for(Map.Entry<?extends ExecutableElement,?extends AnnotationValue>entry:values.entrySet()){

ExecutableElement elem=entry.getKey();

AnnotationValue value=entry.getValue();

String elemName=elem.getSimpleName().toString();

if(name.equals(elemName)){

return value.getValue();

}

}

return null;

}

}


在得到包含注解的Element接口的实现对象之后,有两种方式可以获取该对象上包含的注解。一种方式是使用代码清单8-29中给出的getAnnotationMirrors方法。该方法返回的是所有注解的列表,需要遍历该列表进一步找到所需的注解。该方法返回注解列表中包含直接出现在元素中的注解,并不包含继承下来的注解。另一种方式是使用Element接口的getAnnotation方法。该方法接受一个注解类型的Class类的对象作为参数,返回元素上对应此类型的注解。通过这个方法得到的注解可能是直接出现的,也可能是继承而来的。这两个方法的显著区别在于注解信息的来源不同:getAnnotationMirrors方法使用的是源代码中出现的程序静态结构中的信息,getAnnotation方法使用的则是运行时通过反射得到的信息。尤其要注意这两个方法在处理注解中Class和Class[]类型的配置元素上的区别。如果注解中元素的类型是Class或Class[],那么通过getAnnotation方法得到的注解中元素的值不能直接使用。任何尝试使用得到的Class和Class[]类型的对象的操作,都会抛出异常。这是因为缺乏加载对应Java类所需的信息,无法定义出对应的Class类的对象。而通过getAnnotationMirrors方法可以得到相关的信息。每个Java类由javax.lang.model.type.TypeMirror接口来表示。

注解处理器的运行由Java编译器来完成。可以通过两种方式来声明在编译时要运行的注解处理器。一种方式是通过javac命令行工具的“-processor”参数来指定注解处理器的类名。另外一种方式是使用注解处理器的自动发现机制。编译器在编译时的类路径(CLASSPATH)中查找路径名为“META-INF/services/javax.annotation.processing.Processor”的文件。这个文件中的每一行都包含要运行的注解处理器的类名。除了类路径外,还可以通过编译器的“-processorpath”参数来显式指定查找路径。如果不希望进行注解处理,那么可以使用“-proc:none”参数来禁用。

2.创建和修改源代码

之前介绍的注解处理器只是从注解中提取出所需的相关信息,并没有对Java源代码本身做出修改。实际上,在注解处理器中既可以创建新的Java源文件、字节代码文件和资源文件,又可以对已有的源代码进行修改。创建新文件的操作由从Processing-Environment接口得到的javax.annotation.processing.Filer接口的实现对象来完成。对已有源代码的修改则由Java编译器提供的内部API来完成。

下面先介绍如何生成一个新的Java源代码文件。代码清单8-30给出了在注解处理器中生成新Java源代码的示例。其中,filer是在init方法中通过ProcessingEnvironment接口的getFiler方法得到的Filer接口的实现对象。通过Filer接口的createSourceFile方法创建一个新的JavaFileObject接口的实现对象,表示一个新的Java源文件。不过创建完成后,JavaFileObject接口的实现对象所表示的文件内容是空的。通过JavaFileObject接口的openOutputStream方法得到输出文件内容所需的OutputStream类的对象,再把源文件的内容写入即可。这里只写入了一个简单的Java类的源代码内容。

代码清单8-30 注解处理器中生成源代码的示例


private void generateSource()throws IOException{

String packageName="com.java7book.chapter8.annotation";

String className="HelloWorld";

String fullName=packageName+"."+className;

JavaFileObject javaFile=filer.createSourceFile(fullName,(Element[])null);

Writer writer=new OutputStreamWriter(javaFile.openOutputStream(),"UTF-8");

String source=getSource(packageName, className);

writer.write(source);

writer.close();

}

private String getSource(String packageName, String className){

StringBuilder builder=new StringBuilder();

builder.append("package"+packageName+";");

builder.append("public class"+className+"{}");

return builder.toString();

}


新的Java源文件被创建出来之后,Java编译器会对新文件进行编译。由于注解处理发生在编译之前,创建新的Java源文件的做法适合于自动代码生成的场景。在已有的代码中通过注解的方式来添加元数据,确定代码生成的逻辑。在注解处理过程中完成代码的生成。这种自动的方式解决了手工生成可能带来的变化不一致的问题。不过,由于生成的代码在编译之前对于程序中的其他部分来说是未知的,因此需要某些机制来保证这些代码可以被已有的代码使用。一般是在生成的代码中调用已有代码中的逻辑。

下面介绍如何对已有的源代码进行修改。对源代码进行修改需要分析源代码的结构,得到对应的抽象语法树,再对其中的元素进行修改。OpenJDK中使用的Java编译器的源代码是开放的,可以在Java程序中使用Java编译器所提供的API来对Java源代码进行分析。相关的API在“com.sun.tools.javac”包中。由于编译器的实现不是Java标准的一部分,它的API可能在不同版本之间发生变化,因此使用时要考虑这一点。也可以不对源代码进行语法分析,而是把源文件当成字符串来进行处理。这种做法的优势是简单易用,不需要附加库的支持,但是有可能出现语法错误。下面的介绍中使用的是OpenJDK中Java编译器的API。

先介绍一下示例的背景。Java语言提供了不同的访问控制级别,包括public、private和protected等。一般Java类内部的域和方法使用private来修饰,外部的Java类无法进行访问。这种做法的好处是提高了Java类的封装性,但是在进行测试的时候,无法对这些私有方法进行直接测试,只能通过Java类的公开方法进行间接测试。从方便测试的角度出发,可以把原始代码中的私有域和方法修改成公开的。修改之后的代码只为测试所用。

为了允许把代码中的私有域和方法的声明修改为公开的,需要增加一个新的注解类型Visible。该注解可以添加在需要进行转换的Java类上。完整的注解处理器的实现如代码清单8-31所示。通过编译器的API,可以得到表示程序元素的Element接口的实现对象所对应的com.sun.tools.javac.tree.JCTree类的对象。JCTree类的对象是一个树形结构,可以在上面添加访问者来进行处理。而com.sun.tools.javac.tree.TreeTranslator类是一个用来进行源代码转换的访问者实现。VisibilityTranslator类所实现的是在Java源代码中遇到域声明时,把它的访问控制修饰符的值直接设置为1,即public声明对应的标记值。经过这样的处理之后,源代码中的域都变成可以公开访问的。

代码清单8-31 把域的声明修改为public的注解处理器


@SupportedSourceVersion(SourceVersion.RELEASE_7)

@SupportedAnnotationTypes("com.java7book.chapter8.annotation.Visible")

public class VisibilityProcessor extends AbstractProcessor{

private TypeElement visibleElement;

private Trees trees;

private TreeMaker treeMaker;

public synchronized void init(ProcessingEnvironment processingEnv){

super.init(processingEnv);

trees=Trees.instance(processingEnv);

Context context=((JavacProcessingEnvironment)processingEnv).getContext();

treeMaker=TreeMaker.instance(context);

Elements elementUtils=processingEnv.getElementUtils();

visibleElement=elementUtils.getTypeElement("com.java7book.chapter8.annotation.Visible");

}

public boolean process(Set<?extends TypeElement>annotations,

RoundEnvironment roundEnv){

if(!roundEnv.processingOver()){

Set<?extends Element>elements=roundEnv.getElementsAnnotatedWith(v isibleElement);

for(Element element:elements){

JCTree tree=(JCTree)trees.getTree(element);

TreeTranslator visitor=new VisibilityTranslator();

tree.accept(visitor);

}

}

return true;

}

private class VisibilityTranslator extends TreeTranslator{

public void visitVarDef(JCVariableDecl def){

super.visitVarDef(def);

JCVariableDecl pub=treeMaker.VarDef(treeMaker.Modifiers(1),def.name, def.vartype, def.init);

result=pub;

}

}

}


3.使用反射API处理注解

如果不在编译之前对注解进行处理,那么另外一种做法是可以在运行时通过反射API来利用注解中提供的信息。在反射API中,java.lang.reflect.AnnotatedElement接口表示包含注解的元素。java.lang包中的Class和Package,以及java.lang.reflect包中的Constructor、Field和Method都实现了AnnotatedElement接口。AnnotatedElement接口中的getAnnotations方法用来获取当前元素上的所有注解,包括直接出现的和继承而来的,而getDeclaredAnnotations方法只返回直接出现的注解。使用getAnnotation方法可以根据注解类型来查找相应的注解声明。在得到注解之后,可以调用其中的方法来获得包含的配置元素的值。

一个典型的使用场景是把注解、反射API和动态代理结合起来使用。注解用来设置程序在运行时的行为,反射API用来解析注解,而动态代理负责应用具体的行为。相对于动态修改Java源代码或字节代码的方式来说,这种做法的实现都包括在程序的源代码中,开发和调试比较简单。

例如,在一个企业的员工管理系统中,对访问控制权限有比较严格的要求,某些操作只能由特定角色的用户来完成。如给员工加薪的操作,只能由具有“经理”角色的用户来完成。这种访问控制的限制一般是分级进行的,除了前端界面显示和后台服务接口需要有相应的权限检查逻辑之外,进行实际操作的业务逻辑实现代码也要有相应的检查逻辑。

从实现的角度来说,访问控制属于程序中横切的非功能性需求,可以由专门的人员负责开发。业务逻辑的实现代码只需要以声明的方式来描述访问控制的需求即可。使用代码清单8-32中的Role注解类型可以指定调用一个方法时,当前用户要具备的角色。Role类型的保留策略要设置为RUNTIME,这样可以在运行时通过反射API来查找到该注解的信息。

代码清单8-32 声明方法调用时所需用户角色的注解类型


@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public@interface Role{

String[]value();

}


代码清单8-33在对员工信息进行操作的EmployeeInfoManager接口的updateSalary方法中添加了Role注解,声明调用此方法时,当前用户需要具有“manager”角色。

代码清单8-33 修改员工薪酬的接口


public interface EmployeeInfoManager{

@Role("manager")

public void updateSalary();

}


在EmployeeInfoManager接口的实现类中并不需要考虑访问控制的问题,具体的访问控制是由动态代理来负责的。代码清单8-34中的AccessInvocationHandler类负责进行调用时的检查。如果方法中添加了Role注解,则获取此注解中包含的值,即调用此方法所应具备的角色。通过与程序中保存的当前用户的角色进行比较,可以判断该调用是否应该进行。使用者通过工厂方法得到EmployeeInfoManager接口的实现对象,该对象是一个封装了访问控制权限检查逻辑的动态代理对象。

代码清单8-34 用来获取修改员工薪酬的代理对象的工厂类


public class EmployeeInfoManagerFactory{

private static class AccessInvocationHandler<T>implements InvocationHandler{

private final T targetObject;

public AccessInvocationHandler(T targetObject){

this.targetObject=targetObject;

}

public Object invoke(Object proxy, Method method, Object[]args)throws Throwable{

Role annotation=method.getAnnotation(Role.class);

if(annotation!=null){

String[]roles=annotation.value();

String currentRole=AccessManager.getCurrentUserRole();

if(!Arrays.asList(roles).contains(currentRole)){

throw new RuntimeException("没有调用此方法的权限。");

}

}

return method.invoke(targetObject, args);

}

}

public static EmployeeInfoManager getManager(){

EmployeeInfoManager instance=new DefaultEmployeeInfoManager();

return(EmployeeInfoManager)Proxy.newProxyInstance(instance.getClass().getClassLoader(),new Class<?>[]{EmployeeInfoManager.class},new AccessInvocationHandler<EmployeeInfoManager>(instance));

}

}