节
春
度
欢

插入式注解处理器

一、简介

  插入式注解处理器是JSR-269中定义的API,该API可以在编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程,通过插入式注解处理器可以读取、修改、添加抽象语法树中的任意元素,这样就可以实现很多很cool的功能。

  著名的Lombok就用到插入式注解处理器,它可以通过注解来实现自动生成getter/setter方法、生成equals()和hashCode()方法等。

二、用法

很多IDEA都有代码校验插件,这里我们使用注解处理器API来编写自己的编码风格校验工具:NameCheckProcessor。

主要功能是在执行javac命令编译java文件时,校验代码命名是否符合以下的《Java语言规范》,如果不符合则输出警告信息。

  • 类(或接口):符合驼式命名法,首字母大写。
  • 方法:符合驼式命名法,首字母小写
  • 字段
    类或实例变量:符合驼式命名法,首字母小写。
    常量:要求全部由大写字母或下划线构成,且第一个字符不能是下划线。

代码实现

实现的注解处理器需要继承抽象类javax.annotation.processing.AbstractProcessor,并且子类必须实现抽象方法process()。

注解处理器NameCheckProcessor

  1. package cn.sp.complier; 
  2.  
  3. import javax.annotation.processing.AbstractProcessor; 
  4. import javax.annotation.processing.ProcessingEnvironment; 
  5. import javax.annotation.processing.RoundEnvironment; 
  6. import javax.annotation.processing.SupportedAnnotationTypes; 
  7. import javax.annotation.processing.SupportedSourceVersion; 
  8. import javax.lang.model.SourceVersion; 
  9. import javax.lang.model.element.Element; 
  10. import javax.lang.model.element.TypeElement; 
  11. import java.util.Set; 
  12.  
  13. /** 
  14. * 代码命名规范校验注解处理器 
  15. */ 
  16. @SupportedAnnotationTypes("*")// 表示对哪些注解感兴趣 
  17. @SupportedSourceVersion(SourceVersion.RELEASE_8)// 需要处理哪个版本的Java代码 
  18. public class NameCheckProcessor extends AbstractProcessor { 
  19.  
  20. private NameChecker nameChecker; 
  21.  
  22. @Override 
  23. public synchronized void init(ProcessingEnvironment processingEnv) { 
  24. super.init(processingEnv); 
  25. nameChecker = new NameChecker(processingEnv); 
  26. } 
  27.  
  28. /** 
  29. * 对输入的语法树的各个节点进行名称检查 
  30. * @param annotations 获取此注解处理器要处理的注解集合 
  31. * @param roundEnv 从该参数访问到当前这个轮次(Round)中的抽象语法树节点 
  32. * @return 
  33. */ 
  34. public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 
  35. if (!roundEnv.processingOver()){ 
  36. for(Element element : roundEnv.getRootElements()){ 
  37. nameChecker.checkNames(element); 
  38. } 
  39. } 
  40. // 返回false通知该伦次中代码并未改变 
  41. return false; 
  42. } 
  43. } 
  44.  

processingEnv是AbstractProcessor的一个protected变量,在执行init()方法时创建,代码注解处理器框架的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要这个实例变量。

命名检查器NameChecker

  1. package cn.sp.complier; 
  2.  
  3. import javax.annotation.processing.Messager; 
  4. import javax.annotation.processing.ProcessingEnvironment; 
  5. import javax.lang.model.element.Element; 
  6. import javax.lang.model.element.ElementKind; 
  7. import javax.lang.model.element.ExecutableElement; 
  8. import javax.lang.model.element.Modifier; 
  9. import javax.lang.model.element.Name; 
  10. import javax.lang.model.element.TypeElement; 
  11. import javax.lang.model.element.VariableElement; 
  12. import javax.lang.model.util.ElementScanner8; 
  13.  
  14. import java.util.EnumSet; 
  15.  
  16. import static javax.tools.Diagnostic.Kind.WARNING; 
  17.  
  18. /** 
  19. * 程序名称规范的编译器插件:<br> 
  20. * 如果程序命名不合规范,将会输出一个编译器的WANING信息 
  21. */ 
  22. public class NameChecker { 
  23.  
  24. private final Messager messager; 
  25.  
  26. private NameCheckScanner nameCheckScanner = new NameCheckScanner(); 
  27.  
  28. public NameChecker(ProcessingEnvironment processingEnv) { 
  29. this.messager = processingEnv.getMessager(); 
  30. } 
  31.  
  32. /** 
  33. * @param element 
  34. */ 
  35. public void checkNames(Element element) { 
  36. nameCheckScanner.scan(element); 
  37. } 
  38.  
  39. /** 
  40. * 名称检查器实现类,继承了JDK 8中提供的ElementScanner8, 
  41. * 将会以Visitor模式访问抽象语法树中元素 
  42. */ 
  43. private class NameCheckScanner extends ElementScanner8<Void, Void> { 
  44.  
  45. /** 
  46. * 检查变量命名是否合法 
  47. * 
  48. * @param e 
  49. * @param p 
  50. * @return 
  51. */ 
  52. @Override 
  53. public Void visitVariable(VariableElement e, Void p) { 
  54. // 如果这个变量是常量或枚举,则按照大写命名检查,否则按照驼式命名法规则检查 
  55. if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || 
  56. heuristicallyConstant(e)) { 
  57. checkAllCaps(e); 
  58. } else { 
  59. checkCamelCase(e, false); 
  60. } 
  61. return null; 
  62. } 
  63.  
  64.  
  65. /** 
  66. * 判断一个变量是否为常量 
  67. * 
  68. * @param e 
  69. * @return 
  70. */ 
  71. private boolean heuristicallyConstant(VariableElement e) { 
  72. if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) { 
  73. return true; 
  74. } else if (e.getKind() == ElementKind.FIELD && 
  75. e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL))) { 
  76. return true; 
  77. } 
  78. return false; 
  79. } 
  80.  
  81. /** 
  82. * 检查类名是否合法 
  83. * 
  84. * @param e 
  85. * @param p 
  86. * @return 
  87. */ 
  88. @Override 
  89. public Void visitType(TypeElement e, Void p) { 
  90. scan(e.getTypeParameters(), p); 
  91. checkCamelCase(e, true); 
  92. super.visitType(e, p); 
  93. return null; 
  94. } 
  95.  
  96.  
  97. /** 
  98. * 大写命名检查 
  99. * 要求第一个字母必须是大写的英文字母,其余部门可以是下划线或大写字母 
  100. * 
  101. * @param e 
  102. */ 
  103. private void checkAllCaps(Element e) { 
  104. String name = e.getSimpleName().toString(); 
  105. boolean conventional = true; 
  106. int firstCodePoint = name.codePointAt(0); 
  107. if (!Character.isUpperCase(firstCodePoint)) { 
  108. // 第一个字符不是大写字母 
  109. conventional = false; 
  110. } else { 
  111. boolean previousUnderscore = false; 
  112. int cp = firstCodePoint; 
  113. for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { 
  114. cp = name.codePointAt(i); 
  115. if (cp == (int) '_') { 
  116. if (previousUnderscore) { 
  117. // 连续两个_ 
  118. conventional = false; 
  119. break; 
  120. } 
  121. previousUnderscore = true; 
  122. } else { 
  123. previousUnderscore = false; 
  124. if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) { 
  125. conventional = false; 
  126. break; 
  127. } 
  128. } 
  129. } 
  130. } 
  131.  
  132. if (!conventional) { 
  133. messager.printMessage(WARNING, "常量 " + name + " 应该全部以大写字母或下划线命名,并且以字母开头", e); 
  134. } 
  135. } 
  136.  
  137.  
  138. /** 
  139. * 检查传入的Element是否符合驼峰命名法,如果不符合输出警告信息 
  140. * 
  141. * @param e 
  142. * @param initialCaps 
  143. */ 
  144. private void checkCamelCase(Element e, boolean initialCaps) { 
  145. String name = e.getSimpleName().toString(); 
  146. // 上个字母是否大写 
  147. boolean previousUpper = false; 
  148. boolean conventional = true; 
  149. int firstCodePoint = name.codePointAt(0); 
  150.  
  151. if (Character.isUpperCase(firstCodePoint)) { 
  152. previousUpper = true; 
  153. if (!initialCaps) { 
  154. messager.printMessage(WARNING, "名称 " + name + " 应该以小写字母开头", e); 
  155. return; 
  156. } 
  157. } else if (Character.isLowerCase(firstCodePoint)) { 
  158. if (initialCaps) { 
  159. messager.printMessage(WARNING, "名称 " + name + " 应该以大写字母开头", e); 
  160. return; 
  161. } 
  162. } else { 
  163. conventional = false; 
  164. } 
  165.  
  166. if (conventional) { 
  167. int cp = firstCodePoint; 
  168. for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { 
  169. cp = name.codePointAt(i); 
  170. if (Character.isUpperCase(cp)) { 
  171. if (previousUpper) { 
  172. conventional = false; 
  173. break; 
  174. } 
  175. previousUpper = true; 
  176. } else { 
  177. previousUpper = false; 
  178. } 
  179. } 
  180. } 
  181.  
  182. if (!conventional) { 
  183. messager.printMessage(WARNING, "名称 " + name + " 应该符合驼式命名法(Camel Case Names)", e); 
  184. } 
  185. } 
  186.  
  187. /** 
  188. * 检查方法命名是否合法 
  189. * 
  190. * @param e 
  191. * @param p 
  192. * @return 
  193. */ 
  194. @Override 
  195. public Void visitExecutable(ExecutableElement e, Void p) { 
  196. if (e.getKind() == ElementKind.METHOD) { 
  197. Name name = e.getSimpleName(); 
  198. if (name.contentEquals(e.getEnclosingElement().getSimpleName())) { 
  199. messager.printMessage(WARNING, "一个普通方法 " + name + " 不应当与类名重复,避免与构造函数产生混淆", e); 
  200. } 
  201. checkCamelCase(e, false); 
  202. } 
  203. super.visitExecutable(e, p); 
  204. return null; 
  205. } 
  206. } 
  207.  
  208. } 
  209.  

javax.lang.model.element.ElementKind是个枚举类,里面定义了18种Element包括了Java代码中可能出现的全部元素。

三、测试

首先写一个命名不规范的代码样例

  1. package cn.sp.complier; 
  2.  
  3. public class BADLY_NAMED_CODE { 
  4.  
  5. enum colors { 
  6. red, blue, green; 
  7. } 
  8.  
  9. static final int _FORTY_TWO = 42; 
  10.  
  11. public static int NOT_A_CONSTANT = _FORTY_TWO; 
  12.  
  13. protected void BADLY_NAMED_CODE() { 
  14.  
  15. } 
  16.  
  17. public void NOTcamelCASEmethodNAME() { 
  18. return; 
  19. } 
  20. } 
  21.  
  • 编译NameChecker.java
E:\idea\processor-demo\src\main\java>javac -encoding utf-8 cn/sp/complier/NameChecker.java
  • 编译NameCheckProcessor.java
E:\idea\processor-demo\src\main\java>javac -encoding utf-8 cn/sp/complier/NameCheckProcessor.java
  • 编译BADLY_NAMED_CODE.java
E:\idea\processor-demo\src\main\java>javac -encoding utf-8 -processor cn.sp.complier.NameCheckProcessor cn/sp/complier/BADLY_NAMED_CODE.java

注意: 这里需要用-processor参数指定用到的注解处理器,如果是多个的话用逗号分隔。

命令台打印信息如下,说明程序执行成功。

警告信息
警告信息

遇到的坑: 第二步编译NameCheckProcessor.java的时候老是报错,提示找不到符号(NameChecker),后来发现如果是.java文件的当前目前执行javac,默认会去当前目录/包名(E:\idea\processor-demo\src\main\java\cn\sp\complier\cn\sp\complier)下找所依赖的class文件,解决办法是直接到package所在的目录执行即可。

可以参考这篇文章。
附上一篇Lombok注解原理的文章。

posted @ 2020-05-19 22:02  烟味i  阅读(565)  评论(0)    收藏  举报