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

一、概述

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

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

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 负责检查命名规范,这是它 github代码链接,哈哈,具体代码就不在文章里贴了,再贴一下文章就没法看了都。

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

原文地址:https://www.cnblogs.com/jmcui/p/12159541.html

时间: 2024-08-13 02:32:52

JVM系列六(自定义插入式注解器).的相关文章

SpringMVC经典系列-14自定义SpringMVC的拦截器---【LinusZhu】

注意:此文章是个人原创,希望有转载需要的朋友们标明文章出处,如果各位朋友们觉得写的还好,就给个赞哈,你的鼓励是我创作的最大动力,LinusZhu在此表示十分感谢,当然文章中如有纰漏,请联系[email protected],敬请朋友们斧正,谢谢. 这部分主要讲解SpringMVC的拦截器的部分,会带着大家完成定义拦截器的两种方式的实例,不多说了,开始-- SpringMVC的拦截器主要是用于拦截用户的请求,并且进行相应的处理,如:权限验证.判断登录等. 定义拦截器的两种方式,如下: 1. 实现接

JVM系列(六) - JVM垃圾回收器

前言 在之前的几篇博客中,我们大致介绍了,常见的 垃圾回收算法 及 JVM 中常见的分类回收算法.这些都是从算法和规范上分析 Java 中的垃圾回收,属于方法论.在 JVM 中,垃圾回收的具体实现是由 垃圾回收器(Garbage Collector)负责. 正文 概述 在了解 垃圾回收器 之前,首先得了解一下垃圾回收器的几个名词. 1. 吞吐量 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值.比如说虚拟机总运行了 100 分钟,用户代码 时间 99 分钟,垃圾回收 时间 1 分钟,那

深入理解java虚拟机(17):插入式注解处理器实战

实战目标实现一个java命名格式规范检查的插件 类或接口,符合驼峰命名法,首字母大写 方法,符合驼峰命名法,首字母小写 字段: 类或实例变量:符合驼峰命名法,首字母小写 常量:要求全部大写字母或下划线构成,并且第一个字符不能是下划线 给javac编译器添加一个额外的功能,在编译程序时检查程序是否符合以上标准. 原文地址:https://www.cnblogs.com/xiaofeiyang/p/11968014.html

JVM系列五(javac 编译器).

一.概述 我们都知道 *.java 文件要首先被编译成 *.class 文件才能被 JVM 认识,这部分的工作主要由 Javac 来完成,类似于 Javac 这样的我们称之为前端编译器: 但是 *.class 文件也不是机器语言,怎么才能让机器识别呢?就需要 JVM 将 *.class 文件编译成机器码,这部分工作由JIT 编译器完成: 除了这两种编译器,还有一种直接把 *.java 文件编译成本地机器码的编译器,我们称之AOT 编译器. 二.javac 的编译过程 首先,我们先导一份 java

深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

一.概述 定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的加载方式 1):本地编译好的class中直接加载 2):网络加载:java.net.URLClassLoader可以加载url指定的类 3):从jar.zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类 4):从java源代码文件动态编译成为class文件 三.类加载的时机

深入JVM系列(二)之GC机制、收集器与GC调优(转)

一.回顾JVM内存分配 需要了解更多内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配 1.1.内存分配: 1.对象优先在EDEN分配2.大对象直接进入老年代 3.长期存活的对象将进入老年代 4.适龄对象也可能进入老年代:动态对象年龄判断 动态对象年龄判断: 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,当Survivor空间的相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

深入JVM系列(二)之GC机制、收集器与GC调优

一.回想JVM内存分配 须要了解很多其它内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配 1.1.内存分配: 1.对象优先在EDEN分配 2.大对象直接进入老年代 3.长期存活的对象将进入老年代 4.适龄对象也可能进入老年代:动态对象年龄推断 动态对象年龄推断: 虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才干晋升到老年代,当Survivor空间的同样年龄的全部对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入

转--深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

深入JVM系列(三)之类加载.类加载器.双亲委派机制与常见问题 http://blog.csdn.net/vernonzheng/article/details/8461380 转--深入JVM系列(三)之类加载.类加载器.双亲委派机制与常见问题

JVM系列文章(三):Class文件内容解析

作为一个程序员,仅仅知道怎么用是远远不够的.起码,你需要知道为什么可以这么用,即我们所谓底层的东西. 那到底什么是底层呢?我觉得这不能一概而论.以我现在的知识水平而言:对于Web开发者,TCP/IP.HTTP等等协议可能就是底层:对于C.C++程序员,内存.指针等等可能就是底层的东西.那对于Java开发者,你的Java代码运行所在的JVM可能就是你所需要去了解.理解的东西. 我会在接下来的一段时间,和读者您一起去学习JVM,所有内容均参考自<深入理解Java虚拟机:JVM高级特性与最佳实践>(