10.4.2 代码实现
要通过注解处理器API实现一个编译器插件,首先需要了解这组API的一些基本知识。我们实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个子类必须实现的抽象方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程,我们可以从这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个轮次(Round)中的抽象语法树节点,每个语法树节点在这里都表示为一个Element。在javax.lang.model.ElementKind中定义了18类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,泛型尖括号内的类型)、资源变量(RESOURCE_VARIABLE,try-resource 中定义的变量)、模块(MODULE)和未定义的其他语法树节点(OTHER)”。除了process()方法的传入参数之外,还有一个很重要的实例变量“processingEnv”,它是AbstractProcessor中的一个protected 变量,在注解处理器初始化的时候(init()方法执行的时候)创建,继承了AbstractProcessor的注解处理器代码可以直接访问它。它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。
注解处理器除了process()方法及其参数之外,还有两个经常配合着使用的注解,分别是: @SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣,后者指出这个注解处理器可以处理哪些版本的Java代码。
每一个注解处理器在运行时都是单例的,如果不需要改变或添加抽象语法树中的内容,process() 方法就可以返回一个值为false的布尔值,通知编译器这个轮次中的代码未发生变化,无须构造新的JavaCompiler实例,在这次实战的注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值一律都是false。
关于注解处理器的API,笔者就简单介绍这些,对这个领域有兴趣的读者可以阅读相关的帮助文档。我们来看看注解处理器NameCheckProcessor的具体代码,如代码清单10-16所示。
代码清单10-16 注解处理器NameCheckProcessor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @SupportedAnnotationTypes("*") @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; } }
|
从代码清单10-16中可以看到NameCheckProcessor能处理基于JDK 6的源码,它不限于特定的注解,对任何代码都“感兴趣”,而在process()方法中是把当前轮次中的每一个RootElement传递到一个名为NameChecker的检查器中执行名称检查逻辑,NameChecker的代码如代码清单10-17所示。
代码清单10-17 命名检查器NameChecker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
public class NameChecker { private final Messager messager; NameCheckScanner nameCheckScanner = new NameCheckScanner(); NameChecker(ProcessingEnvironment processsingEnv) { this.messager = processsingEnv.getMessager(); }
public void checkNames(Element element) { nameCheckScanner.scan(element); }
private class NameCheckScanner extends ElementScanner6<Void, Void> {
@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) { if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e)) checkAllCaps(e); elsecheckCamelCase(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; } }
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; } } elseconventional = 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; } elsepreviousUpper = 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^1的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法,这3个visit*()方法对各自的命名规则做相应的检查,checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查。
整个注解处理器只需NameCheckProcessor和NameChecker两个类就可以全部完成,为了验证我们的实战成果,代码清单10-18中提供了一段命名规范的“反面教材”代码,其中的每一个类、方法及字段的命名都存在问题,但是使用普通的Javac编译这段代码时不会提示任意一条警告信息。
代码清单10-18 包含了多处不规范命名的代码样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } }
|