zoukankan      html  css  js  c++  java
  • 插入式注解处理器

    一、简介

      插入式注解处理器是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. * @param annotations 获取此注解处理器要处理的注解集合 
    30. * @param roundEnv 从该参数访问到当前这个轮次(Round)中的抽象语法树节点 
    31. * @return 
    32. */ 
    33. public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
    34. if (!roundEnv.processingOver()){ 
    35. for(Element element : roundEnv.getRootElements()){ 
    36. nameChecker.checkNames(element); 
    37. // 返回false通知该伦次中代码并未改变 
    38. return false
    39.  

    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. * @param element 
    33. */ 
    34. public void checkNames(Element element)
    35. nameCheckScanner.scan(element); 
    36.  
    37. /** 
    38. * 名称检查器实现类,继承了JDK 8中提供的ElementScanner8, 
    39. * 将会以Visitor模式访问抽象语法树中元素 
    40. */ 
    41. private class NameCheckScanner extends ElementScanner8<Void, Void>
    42.  
    43. /** 
    44. * 检查变量命名是否合法 
    45. * 
    46. * @param e 
    47. * @param p 
    48. * @return 
    49. */ 
    50. @Override 
    51. public Void visitVariable(VariableElement e, Void p)
    52. // 如果这个变量是常量或枚举,则按照大写命名检查,否则按照驼式命名法规则检查 
    53. if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || 
    54. heuristicallyConstant(e)) { 
    55. checkAllCaps(e); 
    56. } else
    57. checkCamelCase(e, false); 
    58. return null
    59.  
    60.  
    61. /** 
    62. * 判断一个变量是否为常量 
    63. * 
    64. * @param e 
    65. * @return 
    66. */ 
    67. private boolean heuristicallyConstant(VariableElement e)
    68. if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) { 
    69. return true
    70. } else if (e.getKind() == ElementKind.FIELD && 
    71. e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL))) { 
    72. return true
    73. return false
    74.  
    75. /** 
    76. * 检查类名是否合法 
    77. * 
    78. * @param e 
    79. * @param p 
    80. * @return 
    81. */ 
    82. @Override 
    83. public Void visitType(TypeElement e, Void p)
    84. scan(e.getTypeParameters(), p); 
    85. checkCamelCase(e, true); 
    86. super.visitType(e, p); 
    87. return null
    88.  
    89.  
    90. /** 
    91. * 大写命名检查 
    92. * 要求第一个字母必须是大写的英文字母,其余部门可以是下划线或大写字母 
    93. * 
    94. * @param e 
    95. */ 
    96. private void checkAllCaps(Element e)
    97. String name = e.getSimpleName().toString(); 
    98. boolean conventional = true
    99. int firstCodePoint = name.codePointAt(0); 
    100. if (!Character.isUpperCase(firstCodePoint)) { 
    101. // 第一个字符不是大写字母 
    102. conventional = false
    103. } else
    104. boolean previousUnderscore = false
    105. int cp = firstCodePoint; 
    106. for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { 
    107. cp = name.codePointAt(i); 
    108. if (cp == (int) '_') { 
    109. if (previousUnderscore) { 
    110. // 连续两个_ 
    111. conventional = false
    112. break
    113. previousUnderscore = true
    114. } else
    115. previousUnderscore = false
    116. if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) { 
    117. conventional = false
    118. break
    119.  
    120. if (!conventional) { 
    121. messager.printMessage(WARNING, "常量 " + name + " 应该全部以大写字母或下划线命名,并且以字母开头", e); 
    122.  
    123.  
    124. /** 
    125. * 检查传入的Element是否符合驼峰命名法,如果不符合输出警告信息 
    126. * 
    127. * @param e 
    128. * @param initialCaps 
    129. */ 
    130. private void checkCamelCase(Element e, boolean initialCaps)
    131. String name = e.getSimpleName().toString(); 
    132. // 上个字母是否大写 
    133. boolean previousUpper = false
    134. boolean conventional = true
    135. int firstCodePoint = name.codePointAt(0); 
    136.  
    137. if (Character.isUpperCase(firstCodePoint)) { 
    138. previousUpper = true
    139. if (!initialCaps) { 
    140. messager.printMessage(WARNING, "名称 " + name + " 应该以小写字母开头", e); 
    141. return
    142. } else if (Character.isLowerCase(firstCodePoint)) { 
    143. if (initialCaps) { 
    144. messager.printMessage(WARNING, "名称 " + name + " 应该以大写字母开头", e); 
    145. return
    146. } else
    147. conventional = false
    148.  
    149. if (conventional) { 
    150. int cp = firstCodePoint; 
    151. for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { 
    152. cp = name.codePointAt(i); 
    153. if (Character.isUpperCase(cp)) { 
    154. if (previousUpper) { 
    155. conventional = false
    156. break
    157. previousUpper = true
    158. } else
    159. previousUpper = false
    160.  
    161. if (!conventional) { 
    162. messager.printMessage(WARNING, "名称 " + name + " 应该符合驼式命名法(Camel Case Names)", e); 
    163.  
    164. /** 
    165. * 检查方法命名是否合法 
    166. * 
    167. * @param e 
    168. * @param p 
    169. * @return 
    170. */ 
    171. @Override 
    172. public Void visitExecutable(ExecutableElement e, Void p)
    173. if (e.getKind() == ElementKind.METHOD) { 
    174. Name name = e.getSimpleName(); 
    175. if (name.contentEquals(e.getEnclosingElement().getSimpleName())) { 
    176. messager.printMessage(WARNING, "一个普通方法 " + name + " 不应当与类名重复,避免与构造函数产生混淆", e); 
    177. checkCamelCase(e, false); 
    178. super.visitExecutable(e, p); 
    179. return null
    180.  
    181.  

    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. static final int _FORTY_TWO = 42
    9.  
    10. public static int NOT_A_CONSTANT = _FORTY_TWO; 
    11.  
    12. protected void BADLY_NAMED_CODE()
    13.  
    14.  
    15. public void NOTcamelCASEmethodNAME()
    16. return
    17.  
    • 编译NameChecker.java
    E:ideaprocessor-demosrcmainjava>javac -encoding utf-8 cn/sp/complier/NameChecker.java
    
    • 编译NameCheckProcessor.java
    E:ideaprocessor-demosrcmainjava>javac -encoding utf-8 cn/sp/complier/NameCheckProcessor.java
    
    • 编译BADLY_NAMED_CODE.java
    E:ideaprocessor-demosrcmainjava>javac -encoding utf-8 -processor cn.sp.complier.NameCheckProcessor cn/sp/complier/BADLY_NAMED_CODE.java
    

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

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

    警告信息
    警告信息

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

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

  • 相关阅读:
    Linux下运行当前目录需要加./的原因
    Linux mint界面过小无法安装(解决方法)
    哈工大机考:数组逆置
    哈工大机考:字符串内排序
    哈工大机考:求最大值
    八皇后问题的简单分析
    哈工大机考:字符串去特定字符
    哈工大机考:计算两个矩阵的乘积
    iOS 字号转换问题
    iOS 十六进制的颜色值转换为UIColor
  • 原文地址:https://www.cnblogs.com/2YSP/p/12919880.html
Copyright © 2011-2022 走看看