该案例主要为实现一个检查Java代码规范的编译器插件功能,编码规范遵循下面标准:

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

驼式命名法(Camel Case Name)是当前Java语言中主流的命名规范,我们的实战目标就是为Javac编译器添加一个额外的功能,在编译程序时检查程序名是否符合上述对类(或接口)、方法、字段的命名要求。

编码实现

AbstractProcessor抽象类

实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个子类必须实现的抽象方法process()

它是Javac编译器在执行注解处理器代码时要调用的过程。

  • 我们可以从这个方法的第一个参数annotations中获取到此注解处理器所要处理的注解集合;
  • 从第二个参数roundEnv中访问到当前这个轮次(Round)中的抽象语法树节点,每个语法树节点在这里都表示为一个Element。

AbstractProcessor抽象类还有一个很重要的实例变量processingEnv

1
2
3
4
/**
* Processing environment providing by the tool framework.
*/
protected ProcessingEnvironment processingEnv;

它是AbstractProcessor中的一个protected变量,在注解处理器初始化的时候(init()方法执行的时候)创建,继承了AbstractProcessor的注解处理器代码可以直接访问它。它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

ElementKind枚举类

在javax.lang.model.ElementKind中定义了17类Element,已经包括了Java代码中可能出现的全部元素。

  • javax.lang.model.ElementKind枚举类:
    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
    import javax.lang.model.element.Element;

    public enum ElementKind {

    /** 包 */
    PACKAGE,

    // Declared types
    /** 枚举 */
    ENUM,
    /** 类 */
    CLASS,
    /** 注解 */
    ANNOTATION_TYPE,
    /**
    * 接口
    */
    INTERFACE,

    // Variables
    /** 枚举值. */
    ENUM_CONSTANT,
    /**
    * 字段值
    */
    FIELD,
    /** 参数 */
    PARAMETER,
    /** 本地变量. */
    LOCAL_VARIABLE,
    /** 异常. */
    EXCEPTION_PARAMETER,

    // Executables
    /** 方法. */
    METHOD,
    /** 构造函数. */
    CONSTRUCTOR,
    /** 静态语句块 即static{}块. */
    STATIC_INIT,
    /** 实例语句块 即{}块. */
    INSTANCE_INIT,

    /** 参数化类型:泛型尖括号内的类型. */
    TYPE_PARAMETER,

    /**
    * 未定义的其他语法树节点
    */
    OTHER,

    /**
    * 资源变量:try-resource中定义d变量.
    */
    RESOURCE_VARIABLE;

    /**
    * Returns {@code true} if this is a kind of class:
    * either {@code CLASS} or {@code ENUM}.
    *
    * @return {@code true} if this is a kind of class
    */
    public boolean isClass() {
    return this == CLASS || this == ENUM;
    }

    /**
    * Returns {@code true} if this is a kind of interface:
    * either {@code INTERFACE} or {@code ANNOTATION_TYPE}.
    *
    * @return {@code true} if this is a kind of interface
    */
    public boolean isInterface() {
    return this == INTERFACE || this == ANNOTATION_TYPE;
    }

    /**
    * Returns {@code true} if this is a kind of field:
    * either {@code FIELD} or {@code ENUM_CONSTANT}.
    *
    * @return {@code true} if this is a kind of field
    */
    public boolean isField() {
    return this == FIELD || this == ENUM_CONSTANT;
    }
    }

两个注解

注解处理器除了process()方法及其参数之外,还有两个经常配合着使用的注解,分别是:

  • @SupportedAnnotationTypes
    代表了这个注解处理器对哪些注解感兴趣,可以使用星号 * 作为通配符代表对所有的注解都感兴趣。
  • @SupportedSourceVersion
    指出这个注解处理器可以处理哪些版本的Java代码。

每一个注解处理器在运行时都是单例的,如果不需要改变或添加抽象语法树中的内容,process()方法就可以返回一个值为false的布尔值,通知编译器这个轮次中的代码未发生变化,无须构造新的JavaCompiler实例,在这次实战的注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值一律都是false。

实现

注解处理器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
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
package com.bubble.processor;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;

/**
* 插入式注解处理器:对Java程序命名进行检查
*
* @author wugang
* date: 2020-07-28 16:23
**/
// 表示支持所有的Annotations
@SupportedAnnotationTypes("*")
// 只支持JDK8的Java代码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;

/**
* 初始化名称检查插件
*
* @param processingEnv 它是AbstractProcessor中的一个protected变量,在注解处理器初始化的时候(init()方法执行的时候)创建,
* 继承了AbstractProcessor的注解处理器代码可以直接访问它。
* 它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量
*/
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}

/**
* 对输入的语法树的各个节点进行名称检查
*
* 该方法是Javac编译器在执行注解处理器代码时要调用的过程:
* 每一个注解处理器在运行时都是单例的,如果不需要改变或添加抽象语法树中的内容,
* process() 方法就可以返回一个值为false的布尔值,通知编译器这个轮次中的代码未发生变化,无须构造新的 JavaCompiler实例。
* 自定义的此注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值一律都是false。
*
* @param annotations 获取到此注解处理器所要处理的注解集合
* @param roundEnv 参数“roundEnv”中访问到当前这个轮次(Round)中的抽象语法树节点,
* 每个语法树节点在这里都表示为一个Element。
* 在javax.lang.model.ElementKind中定义了18类Element。
* @return false
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
roundEnv.getRootElements().forEach(element -> nameChecker.checkNames(element));
}
return false;
}
}

命名检查器NameChecker

它通过一个继承于javax.lang.model.util.ElementScanner8的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法,这3个visit*()方法对各自的命名规则做相应的检查,checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查。

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package com.bubble.processor;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementScanner8;
import java.util.EnumSet;

import static javax.tools.Diagnostic.Kind.WARNING;

/**
* 检查程序名称规范的编译器插件:
* 如果程序命名不符合规范,会输出一个编译器的WARNING信息
*
* @author wugang
* date: 2020-07-28 16:27
**/
public class NameChecker {
/**
* Messager用于向编译器发送信息
*/
private final Messager messager;

private NameCheckScanner nameCheckScanner = new NameCheckScanner();

public NameChecker(ProcessingEnvironment processingEnv) {
this.messager = processingEnv.getMessager();
}

/**
* 对Java程序命名进行检查,根据《Java语言规范》Java程序命名应当符合下列格式:
* - 类或接口:符合驼式命名法,首字母大写。
* - 方法:符合驼式命名法,首字母小写。
* - 字段:
* 类或实例变量。符合驼式命名法,首字母小写。
* 常量。要求全部由大写字母或下划线构成,并且第一个字符不能是下划线。
*
* @param element
*/
public void checkNames(Element element) {
nameCheckScanner.scan(element);
}

/**
* 名称检查器实现类。
* 继承了ElementScanner8,会以Visitor模式访问抽象语法树中的元素。
* 命名规则判断中将不对语法树进行修改,因此全部返回值都为null。
*/
private class NameCheckScanner extends ElementScanner8<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() == ElementKind.METHOD) {
Name name = e.getSimpleName();
if (name.contentEquals(e.getEnclosingElement().getSimpleName())) {
messager.printMessage(WARNING, "一个普通方法[" + name + "]不应当与类名重复,避免与构造函数产生冲突");
checkCamelCase(e, false);
}
}
super.visitExecutable(e, p);
return null;
}

/**
* 检查变量命名是否合法
*/
@Override
public Void visitVariable(VariableElement e, Void p) {
// 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e)) {
checkAllCaps(e);
} else {
checkCamelCase(e, false);
}
return null;
}

/**
* 判断一个变量是否是常量
*/
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;
}

/**
* 检查传入的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 + "]应当全部以大写字母或下划线命名,并且以字母开头");
}
}

}

}

编译测试

命名规范的“反面教材”代码

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
package com.bubble.processor;

/**
* 命名规范的“反面教材”代码:
* 使用:
* 可以通过Javac命令的“-processor”参数来执行编译时需要附带的注解处理器,
* 如果有多个注解 处理器的话,用逗号分隔。
* 还可以使用-XprintRounds和-XprintProcessorInfo参数来查看注解处理器运作的详细信息。
*
* @author wugang
* date: 2020-07-28 17:38
**/
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;
}

}

编译测试

们可以通过Javac命令的-processor参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号分隔。
还可以使用-XprintRounds和-XprintProcessorInfo参数来查看注解处理器运 作的详细信息。

编译:

1
2
3
4
5
cd code/java/multi-dev/data-structure/src/main/java/
javac com/bubble/processor/NameChecker.java
javac com/bubble/processor/NameCheckProcessor.java

javac -processor com.bubble.processor.NameCheckProcessor com/bubble/processor/BADLY_NAMED_CODE.java

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
com/bubble/processor/BADLY_NAMED_CODE.java:13: 警告: 名称[BADLY_NAMED_CODE]应当符合驼式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
^
com/bubble/processor/BADLY_NAMED_CODE.java:15: 警告: 名称[colors]应当以大写字母开头
enum colors {
^
警告: 常量[red]应当全部以大写字母或下划线命名,并且以字母开头
警告: 常量[blue]应当全部以大写字母或下划线命名,并且以字母开头
警告: 常量[green]应当全部以大写字母或下划线命名,并且以字母开头
警告: 常量[_FORTY_TWO]应当全部以大写字母或下划线命名,并且以字母开头
警告: 一个普通方法[BADLY_NAMED_CODE]不应当与类名重复,避免与构造函数产生冲突
com/bubble/processor/BADLY_NAMED_CODE.java:22: 警告: 名称[BADLY_NAMED_CODE]应当以小写字母开头
protected void BADLY_NAMED_CODE() {
^
8 个警告

评论