JDK 的编译子系统暴露给用户直接控制的功能相对很少,除了虚拟机即时编译的若干参数,便只有 JSR-296
中定义的插入式注解处理器 API;
1. 目标
前端编译器在讲 Java 源码编译成字节码时对源码做了各方面检查,主要是看程序写到对不对
,较少去看写得好不好
,业界有一些如 CheckStyle、FindBug、Klocwork 等的工具用于检查代码好坏,有一些用于扫描 Java 源码,也有一些是扫描字节码;我们的目标是通过注解处理器 API 实现自己编码风格的校验工具:NameCheckProcessor
;
根据《Java 语言规范》要求,Java 程序命名推荐使用如下书写规范:
- 类(接口):驼峰命名法,首字母大写;
- 方法:驼峰命名法,首字母小写;
- 字段
- 类或实例变量:驼峰命名法,首字母小写;
- 常量:全部由大写字母或下划线构成,第一个字符不能是下划线;
为 javac 编译器添加额外的功能,在编译程序时检查程序名是否符合上述对类、接口、方法、字段的命名要求;
2. 实现
注解处理器可以通过继承抽象类 javax.annotation.processing.AbstractProcessor
复写抽象方法 process()
来实现;javac 编译器在执行注解处理器代码时会调用 process()
;
annotations
,此注解处理器所要处理的注解集合;roundEnv
,当前这个轮次(Round)中的抽象语法树节点,每个语法树节点表示一个 Element;processingEnv
,AbstractProcessor 中的一个 protected 变量,代表了注解处理器框架提供的一个上下文环境,可用来创建新的代码、向编译器输出信息、获取其他工具类等;
javax.lang.model.ElementKind 定义了 18 类 Element,包含 Java 代码中可能出现的全部元素,如包(Package)、枚举(Enum)、类(Class)、注解(AnnotationType)、接口(Interface)、枚举值(EnumConstant)、字段(Field)、参数(Parameter)、本地变量(LocalVariable)、异常(ExceptionnParameter)、方法(Method)、构造函数(Constructor)、静态语句块(StaticInit,即 static{} 块)、实例语句块(InstanceInit,即 {} 块)、参数化类型(TypeParameter,泛型尖括号内的类型)、资源变量(ResourceVariable,try-resource 中定义的变量)、模块(Module)、未定义的其他语法树节点(Other);
@SupportedAnnotationTypes
,代表这个注解处理器对哪些注解感兴趣;@SuppertedSourceVersion
,表示这个注解处理器可以处理哪些版本的 Java 代码;
注解处理器 NameCheckProcessor
/**
* "*" 表示支持所有 Annotations
* 只支持 JDK 8 的 Java 代码
*
* @author Aurelius Shu
* @since 2023-02-19
*/
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
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;
}
}
命名检查器 NameChecker
/**
* 程序名称规范的编译器插件
* 如果程序命名不合规范,将会输出一个编译器的 WARNING 信息
*
* @author Aurelius Shu
* @since 2023-02-19
*/
public class NameChecker {
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();
NameChecker(ProcessingEnvironment processsingEnv) {
this.messager = processsingEnv.getMessager();
}
/**
* 对Java程序命名进行检查,根据《Java语言规范》第三版第 6.8 节的要求,Java程序命名应当符合下列格式:
* 类或接口:符合驼式命名法,首字母大写。
* 方法:符合驼式命名法,首字母小写。
* 字段:
* 类、实例变量: 符合驼式命名法,首字母小写。
* 常量: 要求全部大写。
*/
public void checkNames(Element element) {
nameCheckScanner.scan(element);
}
/**
* 名称检查器实现类,继承了 JDK 8 中新提供的ElementScanner8<br> * 将会以 Visitor 模式访问抽象语法树中的元素
*/
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() == 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) {
// 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
checkAllCaps(e);
else checkCamelCase(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;
}
}
/**
* 检查传入的 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 + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
}
}
}
}
NameChecker
通过继承 javax.lang.model.util.ElementScanner8
的 NameCheckScanner
类,以 Visitor 模式实现对语法树的遍历;通过 checkCamelCase() 与 checkAllCaps() 实现驼峰命名法和全大写命名的检查;分别在 visitType()、visitVariable()、visitExecutable() 中对类、字段、方法进行检查;
3. 运行与测试
不符合规范的代码演示
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;
}
}
运行注解处理器
# 编译注解处理器,切换到 src/main/java 路径
javac edu/aurelius/jvm/compiler/NameChecker.java
javac edu/aurelius/jvm/compiler/NameCheckProcessor.java
# 编译时运行注解处理器
javac -processor edu.aurelius.jvm.compiler.NameCheckProcessor edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java
检查效果
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:7: warning: 名称“BADLY_NAMED_CODE”应当符合驼式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:8: warning: 名称“colors”应当以大写字母开头
enum colors {
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:9: warning: 常量“red”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:9: warning: 常量“blue”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:9: warning: 常量“green”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:12: warning: 常量“_FORTY_TWO”应当全部以大写字母或下划线命名,并且以字母开头
static final int _FORTY_TWO = 42;
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:13: warning: 名称“NOT_A_CONSTANT”应当以小写字母开头
public static int NOT_A_CONSTANT = _FORTY_TWO;
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:15: warning: 一个普通方法 “BADLY_NAMED_CODE”不应当与类名重复,避免与构造函数产生混淆
protected void BADLY_NAMED_CODE() {
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:15: warning: 名称“BADLY_NAMED_CODE”应当以小写字母开头
protected void BADLY_NAMED_CODE() {
^
edu/aurelius/jvm/compiler/BADLY_NAMED_CODE.java:19: warning: 名称“NOTcamelCASEmethodNAME”应当以小写字母开头
public void NOTcamelCASEmethodNAME() {
^
10 warnings
-XprintRounds
、-XprintProcessorInfo
参数可以进一步跟踪注解处理器的运作详细信息;
4. 其他应用案例
- 校验 Hibernate 标签的正确性(Hibernate Validator Annotation Processor);
- 自动为字段生成 getter、setter 方法等辅助内容(Lombok,根据已有元素生成新的语法树元素);
上一篇:「JVM 编译优化」Java 语法糖(泛型、自动装箱/拆箱、条件编译)
下一篇:「JVM 编译优化」即时编译器
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》