实战:javac插入式注解处理器-阿里云开发者社区

开发者社区> 朱培> 正文

实战:javac插入式注解处理器

简介: 一套编程语言中编译子系统的优劣,很大程度上决定了程序运行性能的好坏和编码效率的高低,尤其在Java语言中,运行期即时编译与虚拟机执行子系统非常紧密地互相依赖、配合运作。了解JDK如何编译和优化代码,有助于我们写出适合JDK自优化的程序。
+关注继续查看

一套编程语言中编译子系统的优劣,很大程度上决定了程序运行性能的好坏和编码效率的高低,尤其在Java语言中,运行期即时编译与虚拟机执行子系统非常紧密地互相依赖、配合运作。了解JDK如何编译和优化代码,有助于我们写出适合JDK自优化的程序。看过javac源码,我们就知道,当我们的编译器在把java文件编译为字节码的时候,会对java源程序做各方面的校验,在本文的实战中,我们将会使用注解处理器API来编写一款拥有自己编码风格的校验工具,为Javac编译器添加一个额外的功能,在编译程序时检查程序名是否符合上述对类(或接口)、方法、字段的命名要求。

前提

Java程序命名应当符合下列格式的书写规范。

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

代码实现

我们实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个必须覆盖的abstract方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程,我们可以从这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。

注解处理器除了process()方法及其参数之外,还有两个可以配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣,后者指出这个注解处理器可以处理哪些版本的Java代码。

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

注解处理器NameCheckProcessor.java


package cn.tf.jvm.part10;

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

// 可以用"*"表示支持所有Annotations
@SupportedAnnotationTypes("*")
// 只支持JDK 1.8的Java代码
@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;
    }
}

从上面代码可以看出,NameCheckProcessor能处理基于JDK 1.8的源码,它不限于特定的注解,对任何代码都“感兴趣”,而在process()方法中是把当前Round中的每一个RootElement传递到一个名为NameChecker的检查器中执行名称检查逻辑。

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

package cn.tf.jvm.part10;

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

import java.util.EnumSet;

import static javax.lang.model.element.ElementKind.*;
import static javax.lang.model.element.Modifier.*;
import static javax.tools.Diagnostic.Kind.WARNING;

/**
 * 程序名称规范的编译器插件:<br>
 * 如果程序命名不合规范,将会输出一个编译器的WARNING信息
 */
public class NameChecker {
private final Messager messager;

NameCheckScanner nameCheckScanner = new NameCheckScanner();

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

/**
 * 对Java程序命名进行检查,根据《Java语言规范》第三版第6.8节的要求,Java程序命名应当符合下列格式:
 *
 * <ul>
 * <li>类或接口:符合驼式命名法,首字母大写。
 * <li>方法:符合驼式命名法,首字母小写。
 * <li>字段:
 * <ul>
 * <li>类、实例变量: 符合驼式命名法,首字母小写。
 * <li>常量: 要求全部大写。
 * </ul>
 * </ul>
 */
public void checkNames(Element element) {
     nameCheckScanner.scan(element);
}

/**
 * 名称检查器实现类,继承了JDK 1.6中新提供的ElementScanner6<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);
}
}
}

在BADLY_NAMED_CODE.java中包含多个不规范的命名,我们需要用到前面的两个类来校验下面这个文件是否符合要求。


    package cn.tf.jvm.part10;

    public class BADLY_NAMED_CODE {
        enum colors {
        red, blue, green;
    }

    static final int _FORTY_TWO = 66;

    public static int NOT_A_CONSTANT = _FORTY_TWO;

    protected void BADLY_NAMED_CODE() {
        return;
    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
    }

运行和测试

我们可以通过Javac命令的“-processor”参数来执行编译时需要附带的注解处理器,在相应的工程下src/java/mian目录下执行以下命令编译

 javac -encoding UTF-8 cn/tf/jvm/part10/NameChecker.java
 javac -encoding UTF-8 cn/tf/jvm/part10/NameCheckProcessor.java

最后使用编译好的文件进行使用

 javac -processor cn.tf.jvm.part10.NameCheckProcessor cn/tf/jvm/part10/BADLY_NAMED_CODE.java

执行结果如下:

cn\tf\jvm\part10\BADLY_NAMED_CODE.java:3: 警告: 名称“BADLY_NAMED_CODE”应当符合驼式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:4: 警告: 名称“colors”应当以大写字母开头
enum colors {
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“red”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“blue”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:5: 警告: 常量“green”应当全部以大写字母或下划线命名,并且以字母开头
red, blue, green;
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:8: 警告: 常量“_FORTY_TWO”应当全部以大写字母或下划线命名,并且以字母开头
static final int _FORTY_TWO = 66;
 ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:10: 警告: 名称“NOT_A_CONSTANT”应当以小写字母开头
public static int NOT_A_CONSTANT = _FORTY_TWO;
  ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 一个普通方法 “BADLY_NAMED_CODE”不应当与类名重复,避免与构造函数产生混淆
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:12: 警告: 名称“BADLY_NAMED_CODE”应当以小写字母开头
protected void BADLY_NAMED_CODE() {
   ^
cn\tf\jvm\part10\BADLY_NAMED_CODE.java:16: 警告: 名称“NOTcamelCASEmethodNAME”应当以小写字母开头
public void NOTcamelCASEmethodNAME() {
^
10 个警告

扩展

Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类,javac编译入口,重要类源码如下:

public void compile(List<JavaFileObject> sourceFileObjects,
                    List<String> classnames,
                    Iterable<? extends Processor> processors)
    throws IOException // TODO: temp, from JavacProcessingEnvironment
{
    if (processors != null && processors.iterator().hasNext())
        explicitAnnotationProcessingRequested = true;
    // as a JavaCompiler can only be used once, throw an exception if
    // it has been used before.
    if (hasBeenUsed)
        throw new AssertionError("attempt to reuse JavaCompiler");
    hasBeenUsed = true;

    start_msec = now();
    try {
        /**
         * 插入注解处理
         */
        initProcessAnnotations(processors);

        /**
         * 词法分析、语法分析
         * parseFiles(sourceFileObjects) 分析源码。获取语法树JCCompilationUnit 集合
         * 
         * 填充符号表
         * enterTrees() 抽象语法树的顶局节点都先被放到待处理列表中并逐个处理列表中的节点。
         * 所有的类符号被输入到外围作用域的符号表中确定类的参数(对泛型类型而言)、超类型和接口
         * 如果需要添加默认构造器,将类中出现的符号输入到类自身的符号表中。
         */
        // These method calls must be chained to avoid memory leaks
        delegateCompiler =
            processAnnotations(
                enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                classnames);
        
        /**
         * 语义分析
         */
        delegateCompiler.compile2();
        delegateCompiler.close();
        elapsed_msec = delegateCompiler.elapsed_msec;
    } catch (Abort ex) {
        if (devVerbose)
            ex.printStackTrace();
    } finally {
        if (procEnvImpl != null)
            procEnvImpl.close();
    }
}

总结:NameCheckProcessor的实战例子只演示了JSR-269嵌入式注解处理器API中的一部分功能,基于这组API支持的项目还有用于校验Hibernate标签使用正确性的本质上与NameCheckProcessor所做的事情差不多)、自动为字段生成getter和setter方法的Lombok等。

参考资料:《深入理解Java虚拟机2》

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Spring Cloud Alibaba实战(一) - 概述
1 什么是Spring Cloud Alibaba ◆ 阿里巴巴结合自身微服务实践,开源的微服务全家桶◆ 在Spring Cloud项目中孵化,很可能成为Spring Cloud第二代的标准实现◆ 在业界广泛使用,已有很多成功案例 Github issue :使用的公司及场景 2 应用场景 ...
44649 0
MySQL Master High Available 实战篇
测试背景 以下所有测试,全部基于以下复制结构完成 host_1(host_1:3306) (current master) +--host_2(host_2:3306) +--host_3(host_3:3306) 一、MHA安装 mha node 所有服务器节点都要安装 mha manager 只需要安装在manager节点 MHA NODE 1.
6674 0
《Java8实战》-读书笔记第一章(02)
从方法传递到Lambda 接着上次的Predicate,继续来了解一下,如果继续简化代码。 把方法作为值来传递虽然很有用,但是要是有很多类似与isHeavyApple和isGreenApple这种可能只用一两次的方法定义一堆确实有点烦人。
1663 0
[Spring实战系列](1)Eclipse下创建Spring-HelloWorld项目
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SunnyYoona/article/details/50600600 1. 简介 Spring Tool Suite 是一个Eclipse插件,利用该插件可以很方便的在Eclipse平台上开发基于Spring的应用。
1712 0
Ajax技术实战操练课堂学习笔记
版权声明:本文为博主原创文章,未经博主允许不得转载。更多学习资料请访问我爱科技论坛:www.52tech.tech https://blog.csdn.net/m0_37981569/article/details/78412799 ...
872 0
【Web API系列教程】3.7 — 实战:处理数据(创建UI视图)
在本节,你将开始为app定义HTML,并在HTML和视图模型间添加数据绑定。 打开Views/Home/Index.cshtml文件。
654 0
+关注
朱培
CSDN博客专家,大数据与云计算领域,常年活跃于Github、CSDN、开源中国等开源社区。
122
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载