插入式注解处理器
Lombok就用到插入式注解处理器,Lombok通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString等方法。
插入式注解处理器是JSR-269中定义的API,该API可以在编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程,通过插入式注解处理器可以读取、修改、添加抽象语法树中的任意元素,这样就可以实现很多很cool的功能。
示例:
很多IDEA都有代码校验插件,这里我们使用注解处理器API来编写自己的编码风格校验工具:NameCheckProcessor。
主要功能是在执行javac命令编译java文件时,校验代码命名是否符合以下的《Java语言规范》,如果不符合则输出警告信息。
- 类(或接口):符合驼式命名法,首字母大写。
- 方法:符合驼式命名法,首字母小写
- 字段
类或实例变量:符合驼式命名法,首字母小写。
常量:要求全部由大写字母或下划线构成,且第一个字符不能是下划线。
代码实现
实现的注解处理器需要继承抽象类javax.annotation.processing.AbstractProcessor,并且子类必须实现抽象方法process()。
注解处理器NameCheckProcessor
package compile; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import java.util.Set; /** * 代码命名规范校验注解处理器 */ @SupportedAnnotationTypes("*")// 表示对哪些注解感兴趣 @SupportedSourceVersion(SourceVersion.RELEASE_8)// 需要处理哪个版本的Java代码 public class NameCheckProcessor extends AbstractProcessor { private NameChecker nameChecker; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); nameChecker = new NameChecker(processingEnv); } /** * 对输入的语法树的各个节点进行名称检查 * @param annotations 获取此注解处理器要处理的注解集合 * @param roundEnv 从该参数访问到当前这个轮次(Round)中的抽象语法树节点 * @return */ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()){ for(Element element : roundEnv.getRootElements()){ nameChecker.checkNames(element); } } // 返回false通知该伦次中代码并未改变 return false; } }
processingEnv是AbstractProcessor的一个protected变量,在执行init()方法时创建,代码注解处理器框架的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要这个实例变量。
命名检查器NameChecker
package compile; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.util.ElementScanner8; import java.util.EnumSet; import static javax.tools.Diagnostic.Kind.WARNING; /** * 程序名称规范的编译器插件:<br> * 如果程序命名不合规范,将会输出一个编译器的WANING信息 */ public class NameChecker { private final Messager messager; private NameCheckScanner nameCheckScanner = new NameCheckScanner(); public NameChecker(ProcessingEnvironment processingEnv) { this.messager = processingEnv.getMessager(); } /** * @param element */ public void checkNames(Element element) { nameCheckScanner.scan(element); } /** * 名称检查器实现类,继承了JDK 8中提供的ElementScanner8, * 将会以Visitor模式访问抽象语法树中元素 */ private class NameCheckScanner extends ElementScanner8<Void, Void> { /** * 检查变量命名是否合法 * * @param e * @param p * @return */ @Override public Void visitVariable(VariableElement e, Void p) { // 如果这个变量是常量或枚举,则按照大写命名检查,否则按照驼式命名法规则检查 if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e)) { checkAllCaps(e); } else { checkCamelCase(e, false); } return null; } /** * 判断一个变量是否为常量 * * @param e * @return */ private boolean heuristicallyConstant(VariableElement e) { if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) { return true; } else if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL))) { return true; } return false; } /** * 检查类名是否合法 * * @param e * @param p * @return */ @Override public Void visitType(TypeElement e, Void p) { scan(e.getTypeParameters(), p); checkCamelCase(e, true); super.visitType(e, p); return null; } /** * 大写命名检查 * 要求第一个字母必须是大写的英文字母,其余部门可以是下划线或大写字母 * * @param 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); } } /** * 检查传入的Element是否符合驼峰命名法,如果不符合输出警告信息 * * @param e * @param initialCaps */ 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); } } /** * 检查方法命名是否合法 * * @param e * @param p * @return */ @Override public Void visitExecutable(ExecutableElement e, Void p) { if (e.getKind() == ElementKind.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; } } }
javax.lang.model.element.ElementKind是个枚举类,里面定义了18种Element包括了Java代码中可能出现的全部元素。
测试
首先写一个命名不规范的代码样例
package compile; 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() { } public void NOTcamelCASEmethodNAME() { return; } public static void main(String[] args) { System.out.println("nihao"); } }
- 编译NameChecker.java
- 编译NameCheckProcessor.java
- 编译BADLY_NAMED_CODE.java
D:\gitspace\Test\src>javac compile/NameChecker.java D:\gitspace\Test\src>javac compile/NameCheckProcessor.java D:\gitspace\Test\src>javac -processor compile.NameCheckProcessor compile/BADLY_NAMED_CODE.java compile\BADLY_NAMED_CODE.java:2: 警告: 名称 BADLY_NAMED_CODE 应该符合驼式命名法(Camel Case Names) public class BADLY_NAMED_CODE { ^ compile\BADLY_NAMED_CODE.java:4: 警告: 名称 colors 应该以大写字母开头 enum colors { ^ compile\BADLY_NAMED_CODE.java:5: 警告: 常量 red 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ compile\BADLY_NAMED_CODE.java:5: 警告: 常量 blue 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ compile\BADLY_NAMED_CODE.java:5: 警告: 常量 green 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ compile\BADLY_NAMED_CODE.java:8: 警告: 常量 _FORTY_TWO 应该全部以大写字母或下划线命名,并且以字母开头 static final int _FORTY_TWO = 42; ^ compile\BADLY_NAMED_CODE.java:10: 警告: 名称 NOT_A_CONSTANT 应该以小写字母开头 public static int NOT_A_CONSTANT = _FORTY_TWO; ^ compile\BADLY_NAMED_CODE.java:12: 警告: 一个普通方法 BADLY_NAMED_CODE 不应当与类名重复,避免与构造函数产生混淆 protected void BADLY_NAMED_CODE() { ^ compile\BADLY_NAMED_CODE.java:12: 警告: 名称 BADLY_NAMED_CODE 应该以小写字母开头 protected void BADLY_NAMED_CODE() { ^ compile\BADLY_NAMED_CODE.java:16: 警告: 名称 NOTcamelCASEmethodNAME 应该以小写字母开头 public void NOTcamelCASEmethodNAME() { ^ 10 个警告 D:\gitspace\Test\src>
注意: 这里需要用-processor参数指定用到的注解处理器,如果是多个的话用逗号分隔。
或者:
package compile; import javax.tools.ToolProvider; public class Test { public static void main(String[] args) { javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); int results = compiler.run(null, null, null, new String[] { "-processor", "compile.NameCheckProcessor", "-processorpath", "D:/gitspace/Test/bin/", "-d", "E:/javaclass", "D:/gitspace/Test/src/compile/BADLY_NAMED_CODE.java" }); System.out.println(results); } }
结果输出:
D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:2: 警告: 名称 BADLY_NAMED_CODE 应该符合驼式命名法(Camel Case Names) public class BADLY_NAMED_CODE { ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:4: 警告: 名称 colors 应该以大写字母开头 enum colors { ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:5: 警告: 常量 red 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:5: 警告: 常量 blue 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:5: 警告: 常量 green 应该全部以大写字母或下划线命名,并且以字母开头 red, blue, green; ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:8: 警告: 常量 _FORTY_TWO 应该全部以大写字母或下划线命名,并且以字母开头 static final int _FORTY_TWO = 42; ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:10: 警告: 名称 NOT_A_CONSTANT 应该以小写字母开头 public static int NOT_A_CONSTANT = _FORTY_TWO; ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:12: 警告: 一个普通方法 BADLY_NAMED_CODE 不应当与类名重复,避免与构造函数产生混淆 protected void BADLY_NAMED_CODE() { ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:12: 警告: 名称 BADLY_NAMED_CODE 应该以小写字母开头 protected void BADLY_NAMED_CODE() { ^ D:\gitspace\Test\src\compile\BADLY_NAMED_CODE.java:16: 警告: 名称 NOTcamelCASEmethodNAME 应该以小写字母开头 public void NOTcamelCASEmethodNAME() { ^ 10 个警告 0
参考:https://www.cnblogs.com/2YSP/p/12919880.html
参考《Java编译器》