插入式注解处理器

Posted 2ysp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了插入式注解处理器相关的知识,希望对你有一定的参考价值。

一、简介

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

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

二、用法

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

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

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

代码实现

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

注解处理器NameCheckProcessor

package cn.sp.complier;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

/**
 * 代码命名规范校验注解处理器
 */
@SupportedAnnotationTypes("*")// 表示对哪些注解感兴趣
@SupportedSourceVersion(SourceVersion.RELEASE_8)// 需要处理哪个版本的Java代码
public class NameCheckProcessor extends AbstractProcessor {

    private NameChecker nameChecker;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        nameChecker = new NameChecker(processingEnv);
    }

    /**
     * 对输入的语法树的各个节点进行名称检查
     * @param annotations  获取此注解处理器要处理的注解集合
     * @param roundEnv 从该参数访问到当前这个轮次(Round)中的抽象语法树节点
     * @return
     */
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()){
            for(Element element : roundEnv.getRootElements()){
                nameChecker.checkNames(element);
            }
        }
        // 返回false通知该伦次中代码并未改变
        return false;
    }
}

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

命名检查器NameChecker

package cn.sp.complier;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner8;

import java.util.EnumSet;

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

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

    private final Messager messager;

    private NameCheckScanner nameCheckScanner = new NameCheckScanner();

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

    /**
     * @param element
     */
    public void checkNames(Element element) {
        nameCheckScanner.scan(element);
    }

    /**
     * 名称检查器实现类,继承了JDK 8中提供的ElementScanner8,
     * 将会以Visitor模式访问抽象语法树中元素
     */
    private class NameCheckScanner extends ElementScanner8<Void, Void> {

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


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

        /**
         * 检查类名是否合法
         *
         * @param e
         * @param p
         * @return
         */
        @Override
        public Void visitType(TypeElement e, Void p) {
            scan(e.getTypeParameters(), p);
            checkCamelCase(e, true);
            super.visitType(e, p);
            return null;
        }


        /**
         * 大写命名检查
         * 要求第一个字母必须是大写的英文字母,其余部门可以是下划线或大写字母
         *
         * @param 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);
            }
        }


        /**
         * 检查传入的Element是否符合驼峰命名法,如果不符合输出警告信息
         *
         * @param e
         * @param initialCaps
         */
        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);
            }
        }

        /**
         * 检查方法命名是否合法
         *
         * @param e
         * @param p
         * @return
         */
        @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 + " 不应当与类名重复,避免与构造函数产生混淆", e);
                }
                checkCamelCase(e, false);
            }
            super.visitExecutable(e, p);
            return null;
        }
    }

}

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

三、测试

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

package cn.sp.complier;

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() {

    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
}

  • 编译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注解原理的文章。

以上是关于插入式注解处理器的主要内容,如果未能解决你的问题,请参考以下文章

Java代码的编译过程(没写完,不要点进来)

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

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

Javac 编译器

[ jquery 文档处理 insertBefore(content) before(content|fn) ] 此方法用于把所有匹配的元素插入到另一个指定的元素元素集合的前面,实现外部插入(代码片段

JVM系列六(自定义插入式注解器).