一、概述

从前面 文章 中我们可以了解到,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

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

  1. jvm系列(六):Java服务GC参数调优案例

    本文介绍了一次生产环境的JVM GC相关参数的调优过程,通过参数的调整避免了GC卡顿对JAVA服务成功率的影响. 这段时间在整理jvm系列的文章,无意中发现本文,作者思路清晰通过步步分析最终解决问题. ...

  2. jvm系列(六):jvm调优-工具篇

    ## jdk自带的工具### jconsole Jconsole(Java Monitoring and Management Console)是从java5开始,在JDK中自带的java监控和管理控 ...

  3. jvm系列(六):jvm调优-从eclipse开始

    jvm调优-从eclipse开始 概述 什么是jvm调优呢?jvm调优就是根据gc日志分析jvm内存分配.回收的情况来调整各区域内存比例或者gc回收的策略:更深一层就是根据dump出来的内存结构和线程 ...

  4. jvm系列(三):java GC算法 垃圾收集器

    GC算法 垃圾收集器 概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计 ...

  5. jvm系列六、windows用jdk自带工具jps、jstack找出性能最差的代码

    一.运行程序TestGC 二.用jps找出当前应用的进程号PID 到jdk安装目录的bin目录下输入: jps -l PID为1264 三.启动Process Explorer(下载地址:https: ...

  6. jvm系列 (二) ---垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 前言:本文基于<深入java虚拟机>再加上个人的理解以及其他相关资料,对内容进行整理浓缩总结.本文中的图来自网络,感谢图的作者.如果有不正确的地方,欢迎指出. 目 ...

  7. jvm系列(七):jvm调优-工具篇

    16年的时候花了一些时间整理了一些关于jvm的介绍文章,到现在回顾起来还是一些还没有补充全面,其中就包括如何利用工具来监控调优前后的性能变化.工具做为图形化界面来展示更能直观的发现问题,另一方面一些耗 ...

  8. JVM系列之二:编译过程

    1. Java的编译和执行 编译包括两种情况: 1,源码编译成字节码2,字节码编译成本地机器码(符合本地系统专属的指令) 解释执行也包括两种情况: 1,源码解释执行2,字节码解释执行 解释和编译执行的 ...

  9. 【JVM】关于JVM,你需要掌握这些 | 一文彻底吃透JVM系列

    写在前面 最近,一直有小伙伴让我整理下关于JVM的知识,经过十几天的收集与整理,初版算是整理出来了.希望对大家有所帮助. JDK 是什么? JDK 是用于支持 Java 程序开发的最小环境. Java ...

随机推荐

  1. PHP实现购物车的思路和源码分析

    正文内容 这里主要是记录下自己的购物车的思路,具体功能实现,但是尚未在实际项目中用到,不对之处欢迎指正 项目中需要添加购物车. 目录说明 buy.php 点击购买之后的操作 car.php 购物车,显 ...

  2. hdu 1156 && poj 2464 Brownie Points II (BIT)

    2464 -- Brownie Points II Problem - 1156 hdu分类线段树的题.题意是,给出一堆点的位置,stan和ollie玩游戏,stan通过其中一个点画垂线,ollie通 ...

  3. [C#] ServiceStack.Redis如何批量的pop数据?

    要安全的批量pop数据,有两个办法: 1.用事务(不用事务的话可能导致重复读.ServiceStack的pipeline是没有自带事务的.) 2.执行lua脚本 我这里提供用事务的实现方法: publ ...

  4. 解决Pycharm中SystemError报错

    报错描述- 代码逻辑大致是, 开启线程, 监听kafka生产者push的topic消息.- 问题出现在监听过程中, 线程在接收几条topic之后出现报错, 不再处理数据12报错代码Exception ...

  5. 2002年NOIP普及组复赛题解

    题目涉及算法: 级数求和:入门题: 选数:搜索: 产生数:搜索.高精度: 过河卒:动态规划. 级数求和 题目链接:https://www.luogu.org/problemnew/show/P1035 ...

  6. SELinux: Could not downgrade policy file

    在配置nfs服务器,设定selinux时,碰到了SELinux: Could not downgrade policy file的错误提示,下文是其解决方案. 一.故障现象 [root@system1 ...

  7. JPA 一对多、多对一注解

    1. @OneToMany @OneToMany 是属性或方法级别的注解,用于定义源实体与目标实体是一对多的关系. 参数 类型 描述 targetEntity Class 源实体关联的目标实体类型,默 ...

  8. Laravel 5.* 执行seeder命令出现错误的解决方法

    最近在使用Laravel开发一个项目,测试中需要增加数据库基础数据动作,当第一次执行完`php artisan db:seed` 后,增加新的seeder文件时执行会报错.错误信息如下`[Reflec ...

  9. ArrayList中remove方法和set(null)的区别

    在分析源码ArrayList.remove()时,偶然发现了一个疑惑的点,就是:源码也是将最后一个对象的引用指向null(源码:elementData[--size] = null; // clear ...

  10. vue用法父组件调用子组件方法--->$refs

    vue下载excel模板(放入弹框独立出来)后再导入表格 子组件 <el-dialog title="导入" :visible.sync="dialogVisibl ...