专业编程基础技术教程

网站首页 > 基础教程 正文

Java 自定义插入式注解器 java自定义注解怎么用

ccvgpt 2024-11-08 10:58:52 基础教程 5 ℃

一、概述

从前面文章中我们可以了解到,javac 的三个步骤中,程序员唯一能干涉的就是注解处理器部分,注解处理器类似于编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。因此,只要有足够的创意,程序员可以通过自定义插入式注解处理器来实现许多原本只能在编码中完成的事情。我们常见的 Lombok、Hibernate Validator 等都是基于自定义插入式注解器来实现的。

要实现注解处理器首先要做的就是继承抽象类 javax.annotation.processing.AbstractProcessor,然后重写它的 process() 方法,process() 方法是 javac 编译器在执行注解处理器代码时要执行的过程。

Java 自定义插入式注解器 java自定义注解怎么用

public?abstract?boolean?process(Set<??extends?TypeElement>?annotations,?RoundEnvironment?roundEnv);

该方法有两个参数,“annotations” 表示此处理器所要处理的注解集合;“roundEnv” 表示当前这个 Round 中的语法树节点,每个语法树节点都表示一个 Element(javax.lang.model.element.ElementKind 可以查看到相关 Element)。

该方法的返回值是一个 boolean 类型,通知编译器这个 Round 中的代码是否发生变化,是否需要构建新的 JavaCompiler 实例,是否需要开启新的 Round。

除了 process() 方法外,还有两个可以配合使用的 Annotations:

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)

@SupportedAnnotationTypes 表示注解处理器对哪些注解感兴趣,“*” 表示对所有的注解都感兴趣;@SupportedSourceVersion 指出这个注解处理器可以处理最高哪个版本的 Java 代码。

另外 AbstractProcessor 还有一个很常用的实例变量 “processingEnv”,它在 init() 方法执行的时候创建,它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

????public?synchronized?void?init(ProcessingEnvironment?processingEnv)?{
??????//?...?
????}

tips:每一个注解处理器在运行的时候都是单例的。

二、自定义

我们现在要自定义一个插入式注解器 — NameCheckProcessor,它要做的事情是对 Java 程序命名进行检查,检查的规则如下:

  • 类(或接口):符合驼式命名法,首字母大写
  • 方法:符合驼式命名法,首字母小写
  • 类或实例变量:符合驼式命名法,首字母小写
  • 常量要求全部是大写字母或下划线构成,并且第一个字符不能是下划线
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public?class?NameCheckProcessor?extends?AbstractProcessor?{

????private?NameChecker?nameChecker;

????@Override
????public?synchronized?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 最高能处理 JDK1.8 的代码,并对所有的注解都感兴趣,而在 process() 方法中是把当前 Round 中的每一个 RootElement 传递到一个名为 NameChecker 的检查器中检查逻辑,process() 方法返回 false,因为它只是检查命名规范,并未改变语法树。

以下是 NameChecker 的代码:

public class NameChecker {

    private final Messager messager;

    private NameCheckScanner nameCheckScanner = new NameCheckScanner();
    
    public NameChecker(ProcessingEnvironment processingEnv) {
        this.messager = processingEnv.getMessager();
    }
    
    public void checkNames(Element element) {
        nameCheckScanner.scan(element);
    }
    
    private class NameCheckScanner extends ElementScanner8<Void, Void> {

        /**
         * 检查类命名
         */
        @Override
        public Void visitType(TypeElement e, Void aVoid) {
            scan(e.getTypeParameters(), aVoid);
            checkCamelCase(e, true);
            return super.visitType(e, aVoid);
        }

        /**
         * 检查方法命名
         */
        @Override
        public Void visitExecutable(ExecutableElement e, Void aVoid) {
            if (e.getKind() == ElementKind.METHOD) {
                Name simpleName = e.getSimpleName();
                if (simpleName.contentEquals(e.getEnclosingElement().getSimpleName())) {
                    messager.printMessage(Diagnostic.Kind.WARNING, e.getClass().getName() + "一个普通方法" + simpleName + "不应当与类名冲突,避免与构造函数产生混淆", e);
                }
                checkCamelCase(e, false);
            }
            return super.visitExecutable(e, aVoid);
        }

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

        /**
         * 判断一个变量是否是常量
         */
        private boolean heuristicallyConstant(VariableElement e) {
            if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE) {
                return true;
            }
            if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL))) {
                return true;
            }
            return false;

        }

        /**
         * 检查传入的元素是否符合驼式命名法,如果不符合,则输出警告信息
         *
         * @param e           元素
         * @param initialCaps 首字母是否要大写
         */
        private void checkCamelCase(Element e, boolean initialCaps) {
            Name simpleName = e.getSimpleName();
            // 命名前面一位是否大写
            boolean previousUpper = false;
            // 常规命名(首字母大写或者小写)
            boolean conventional = true;
            IntStream codePoints = simpleName.codePoints();
            int firstCodePoint = codePoints.findFirst().getAsInt();
            // 首字母大写
            if (Character.isUpperCase(firstCodePoint)) {
                if (!initialCaps) {
                    messager.printMessage(Diagnostic.Kind.WARNING, "名称:" + simpleName + "应当以小写字母开头", e);
                    return;
                }
                previousUpper = true;
            }
            // 首字母小写
            else if (Character.isLowerCase(firstCodePoint)) {
                if (initialCaps) {
                    messager.printMessage(Diagnostic.Kind.WARNING, "名称:" + simpleName + "应当以大写字母开头", e);
                    return;
                }
            } else {
                // 首字母既非大写也非小写,直接 WARNNING
                conventional = false;
            }

            if (conventional) {
                int[] ints = simpleName.codePoints().toArray();
                for (int i = 1; i < ints.length; i++) {
                    if (Character.isUpperCase(ints[i])) {
                        if (previousUpper) {
                            conventional = false;
                            break;
                        }
                        previousUpper = true;
                    } else {
                        previousUpper = false;
                    }
                }
            }
            if (!conventional) {
                messager.printMessage(Diagnostic.Kind.WARNING, "名称" + simpleName + "应当符合驼式命名法(Camel Case Names)", e);
            }
        }

        /**
         * 大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母
         *
         * @param e
         */
        private void checkAllCaps(VariableElement e) {
            Name simpleName = e.getSimpleName();
            IntStream codePoints = simpleName.codePoints();
            // 是否符合常规(第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母)
            boolean conventional = true;
            int firstCodePoint = codePoints.findFirst().getAsInt();
            if (!Character.isUpperCase(firstCodePoint)) {
                conventional = false;
            } else {
                // 前一个下划线
                boolean previousUnderscore = false;
                int[] ints = simpleName.codePoints().toArray();
                for (int i = 1; i < ints.length; i++) {
                    int cp = ints[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(Diagnostic.Kind.WARNING, "常量" + simpleName + "应当全部以大写字母或下划线命名,并且以字母开头", e);
            }
        }
    }
}

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

自定义注解器写好了,那么问题来了,注解器怎么用呢?

  • 通过 javac 命令的 “-processor” 参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号进行分割。
  • 通过 JAVA SPI 加载。在 resources 目录下新增 META-INF/services 目录,目录内添加名为 javax.annotation.processing.Processor 的文件,内容是自定义注解器的全类名,一行表示一个注解器。

三、应用

这里主要介绍下利用 Java SPI 加载自定义注解器的方式,我们的目标是生成一个 jar 包,类似于 Lombok ,这样其它应用一旦引用了这个 jar 包,自定义注解器就能自动生效了。

1. 生成注解器 jar 包

首先,我们先来看下自定义注解器的目录结构,在 javax.annotation.processing.Processor 文件中是自定义注解器的全类名。

org.jvm.processor.name.check.NameCheckProcessor

然后,在 pom.xml 中配置 proc 属性,如果不配置的话,会有个 WARNNING 提示— 找不到 processor 的异常。

????<build>
????????<plugins>
????????????<plugin>
????????????????<groupId>org.apache.maven.plugins</groupId>
????????????????<artifactId>maven-compiler-plugin</artifactId>
????????????????<configuration>
????????????????????<proc>none</proc>
????????????????</configuration>
????????????</plugin>
????????</plugins>
????</build>

最后,愉快的使用 mvn clean install 来 build 你的注解器 jar 包吧!

2. 使用注解器 jar 包

首先,在 pom.xml 中引入注解器 jar 包的依赖

????????<dependency>
????????????<groupId>org.jvm.processor</groupId>
????????????<artifactId>processor</artifactId>
????????????<version>1.0.0-SNAPSHOT</version>
????????</dependency>


其实,进行到这一步你的自定义注解器已经生效了!另外,maven-compiler-plugin 支持手动对需要运行的注解器进行设置。

????????????<plugin>
????????????????<groupId>org.apache.maven.plugins</groupId>
????????????????<artifactId>maven-compiler-plugin</artifactId>
????????????????<configuration>
????????????????????<annotationProcessors>
????????????????????????<annotationProcessor>
????????????????????????????org.jvm.processor.name.check.NameCheckProcessor
????????????????????????</annotationProcessor>
????????????????????</annotationProcessors>
????????????????</configuration>
????????????</plugin>

tips: maven-compile-plugin 等编译插件会吞掉 javax.annotation.processing.Messager 所打印的东西,而手动用 javac 编译器则不会。

四、总结

上文的注解器案例主要参考《深入理解 JVM 虚拟机》,后来又在网上看了一些大家的实践,觉得还挺开拓思维的,大家可以试试看。

自定义注解器这东西,类似于拦截器功能,只要思维都大胆,感觉能玩出花来!

上文的演示的代码可参见:https://github.com/JMCuixy/jvm-demo

Tags:

最近发表
标签列表