10.4.2 代码实现

要通过注解处理器API实现一个编译器插件,首先需要了解这组API的一些基本知识。我们实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个必须覆盖的abstract方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程,我们可以从这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。在JDK 1.6新增的javax.lang.model包中定义了16类Element,包括了Java代码中最常用的元素,如:“包(PACKAGE)、枚举(ENUM)、类(CLASS)、注解(ANNOTATION_TYPE)、接口(INTERFACE)、枚举值(ENUM_CONSTANT)、字段(FIELD)、参数(PARAMETER)、本地变量(LOCAL_VARIABLE)、异常(EXCEPTION_PARAMETER)、方法(METHOD)、构造函数(CONSTRUCTOR)、静态语句块(STATIC_INIT,即static{}块)、实例语句块(INSTANCE_INIT,即{}块)、参数化类型(TYPE_PARAMETER,既泛型尖括号内的类型)和未定义的其他语法树节点(OTHER)”。除了process()方法的传入参数之外,还有一个很常用的实例变量“processingEnv”,它是AbstractProcessor中的一个protected变量,在注解处理器初始化的时候(init()方法执行的时候)创建,继承了AbstractProcessor的注解处理器代码可以直接访问到它。它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

注解处理器除了process()方法及其参数之外,还有两个可以配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣,后者指出这个注解处理器可以处理哪些版本的Java代码。

每一个注解处理器在运行的时候都是单例的,如果不需要改变或生成语法树的内容,process()方法就可以返回一个值为false的布尔值,通知编译器这个Round中的代码未发生变化,无须构造新的JavaCompiler实例,在这次实战的注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值都是false。关于注解处理器的API,笔者就简单介绍这些,对这个领域有兴趣的读者可以阅读相关的帮助文档。下面来看看注解处理器NameCheckProcessor的具体代码,如代码清单10-11所示。

代码清单10-11 注解处理器NameCheckProcessor


//可以用"*"表示支持所有Annotations

@SupportedAnnotationTypes("*")

//只支持JDK 1.6的Java代码

@SupportedSourceVersion(SourceVersion.RELEASE_6)

public class NameCheckProcessor extends AbstractProcessor{

private NameChecker nameChecker;

/**

*初始化名称检查插件

*/

@Override

public void init(ProcessingEnvironment processingEnv){

super.init(processingEnv);

nameChecker=new NameChecker(processingEnv);

}

/**

*对输入的语法树的各个节点进行名称检查

*/

@Override

public boolean process(Set<?extends TypeElement>annotations,RoundEnvironment roundEnv){

if(!roundEnv.processingOver()){

for(Element element:roundEnv.getRootElements())

nameChecker.checkNames(element);

}

return false;

}

}


从上面代码可以看出,NameCheckProcessor能处理基于JDK 1.6的源码,它不限于特定的注解,对任何代码都“感兴趣”,而在process()方法中是把当前Round中的每一个RootElement传递到一个名为NameChecker的检查器中执行名称检查逻辑,NameChecker的代码如代码清单10-12所示。

代码清单10-12 命名检查器NameChecker


/**

*程序名称规范的编译器插件:<br>

*如果程序命名不合规范,将会输出一个编译器的WARNING信息

*/

public class NameChecker{

private final Messager messager;

NameCheckScanner nameCheckScanner=new NameCheckScanner();

NameChecker(ProcessingEnvironment processsingEnv){

this.messager=processsingEnv.getMessager();

}

/**

*对Java程序命名进行检查,根据《Java语言规范(第3版)》第6.8节的要求,Java程序命名应当符合下列格式:

*

*<ul>

*<li>类或接口:符合驼式命名法,首字母大写。

*<li>方法:符合驼式命名法,首字母小写。

*<li>字段:

*<ul>

*<li>类、实例变量:符合驼式命名法,首字母小写。

*<li>常量:要求全部大写。

*</ul>

*</ul>

*/

public void checkNames(Element element){

nameCheckScanner.scan(element);

}

/**

*名称检查器实现类,继承了JDK 1.6中新提供的ElementScanner6<br>

*将会以Visitor模式访问抽象语法树中的元素

*/

private class NameCheckScanner extends ElementScanner6<Void,Void>{

/**

*此方法用于检查Java类

*/

@Override

public Void visitType(TypeElement e,Void p){

scan(e.getTypeParameters(),p);

checkCamelCase(e,true);

super.visitType(e,p);

return null;

}

/**

*检查方法命名是否合法

*/

@Override

public Void visitExecutable(ExecutableElement e,Void p){

if(e.getKind()==METHOD){

Name name=e.getSimpleName();

if

(name.contentEquals(e.getEnclosingElement().getSimpleName()))

messager.printMessage(WARNING,"一个普通方法""+name+""不应当与类名重复,避免与构造函数产生混淆",e);

checkCamelCase(e,false);

}

super.visitExecutable(e,p);

return null;

}

/**

*检查变量命名是否合法

*/

@Override

public Void visitVariable(VariableElement e,Void p){

//如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查

if(e.getKind()==ENUM_CONSTANT||e.getConstantValue()!=null||heuristicallyConstant(e))

checkAllCaps(e);

else

checkCamelCase(e,false);

return null;

}

/**

*判断一个变量是否是常量

*/

private boolean heuristicallyConstant(VariableElement e){

if(e.getEnclosingElement().getKind()==INTERFACE)

return true;

else if(e.getKind()==FIELD&&e.getModifiers().containsAll(EnumSet.of(PUBLIC,STATIC,FINAL)))

return true;

else{

return false;

}

}

/**

*检查传入的Element是否符合驼式命名法,如果不符合,则输出警告信息

*/

private void checkCamelCase(Element e,boolean initialCaps){

String name=e.getSimpleName().toString();

boolean previousUpper=false;

boolean conventional=true;

int firstCodePoint=name.codePointAt(0);

if(Character.isUpperCase(firstCodePoint)){

previousUpper=true;

if(!initialCaps){

messager.printMessage(WARNING,"名称""+name+""应当以小写字母开头",e);

return;

}

}else if(Character.isLowerCase(firstCodePoint)){

if(initialCaps){

messager.printMessage(WARNING,"名称""+name+""应当以大写字母开头",e);

return;

}

}else

conventional=false;

if(conventional){

int cp=firstCodePoint;

for(int i=Character.charCount(cp);i<name.length();i+=Character.charCount(cp)){

cp=name.codePointAt(i);

if(Character.isUpperCase(cp)){

if(previousUpper){

conventional=false;

break;

}

previousUpper=true;

}else

previousUpper=false;

}

}

if(!conventional)

messager.printMessage(WARNING,"名称""+name+""应当符合驼式命名法(Camel Case Names)",e);

}

/**

*大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母

*/

private void checkAllCaps(Element e){

String name=e.getSimpleName().toString();

boolean conventional=true;

int firstCodePoint=name.codePointAt(0);

if(!Character.isUpperCase(firstCodePoint))

conventional=false;

else{

boolean previousUnderscore=false;

int cp=firstCodePoint;

for(int i=Character.charCount(cp);i<name.length();i+=Character.charCount(cp)){

cp=name.codePointAt(i);

if(cp==(int)'_'){

if(previousUnderscore){

conventional=false;

break;

}

previousUnderscore=true;

}else{

previousUnderscore=false;

if(!Character.isUpperCase(cp)&&!Character.isDigit(cp))

{

conventional=false;

break;

}

}

}

}

if(!conventional)

messager.printMessage(WARNING,"常量""+name+""应当全部以大写字母或下划线命名,并且以字母开头",e);

}

}

}


NameChecker的代码看起来有点长,但实际上注释占了很大一部分,其实即使算上注释也不到190行。它通过一个继承于javax.lang.model.util.ElementScanner6的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法,这3个visit方法对各自的命名规则做相应的检查,checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查。

整个注解处理器只需NameCheckProcessor和NameChecker两个类就可以全部完成,为了验证我们的实战成果,代码清单10-13中提供了一段命名规范的“反面教材”代码,其中的每一个类、方法及字段的命名都存在问题,但是使用普通的Javac编译这段代码时不会提示任何一个Warning信息。

代码清单10-13 包含了多处不规范命名的代码样例


public class BADLY_NAMED_CODE{

enum colors{

red,blue,green;

}

static final int_FORTY_TWO=42;

public static int NOT_A_CONSTANT=_FORTY_TWO;

protected void BADLY_NAMED_CODE(){

return;

}

public void NOTcamelCASEmethodNAME(){

return;

}

}