博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
简单介绍 Java 中的编译时注解
阅读量:7142 次
发布时间:2019-06-29

本文共 6873 字,大约阅读时间需要 22 分钟。

1. 前言

主要介绍了什么是 注解 (Annotation) 以及如何读取 运行时注解 中的数据, 同时用注解实现了简单的 ORM 功能. 这次介绍另一部分: 如何读取 编译时注解 ( RetentionPolicy.SOURCE )

2. 作用

编译时注解可以用来动态生成代码. 使用 SOURCE 类型注解的代码会在编译时被解析, 生成新的 java 文件, 然后和原来的 java 文件一起编译成字节码. 由于不使用反射功能, 编译时注解不会拖累性能, 因而被许多框架使用, 比如 Butter Knife, Dragger2 等.

3. 例子

1. 代码

还是从简单的例子开始看. 这里要做的是生成一个 java 类, 其拥有一个打印注解信息的方法.

先定义一个注解

package apt;......@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留@Target(ElementType.TYPE) // 用于修饰类public @interface Hello {    String name() default "";}

使用注解的类

package apt;@Hello(name = "world")public class Player {}

不使用注解的类, 用于对比

package apt;public class Ignored {}

上一篇说过, 注解没有行为, 只有数据, 需要对应的处理器才能发挥作用. javac 提供了解析编译时注解的注解处理器 ( Annotation Processor ). 对于自定义的注解, 需要手动实现它的注解处理器.下面来看一个简单的注解处理器实现.

package apt;import javax.annotation.processing.*;import javax.lang.model.SourceVersion;import javax.lang.model.element.Element;import javax.lang.model.element.TypeElement;import javax.tools.JavaFileObject;import java.io.IOException;import java.io.Writer;import java.util.Set;/** * Created by away on 2017/6/12. */@SupportedSourceVersion(SourceVersion.RELEASE_8) // 源码级别, 这里的环境是 jdk 1.8@SupportedAnnotationTypes("apt.Hello") // 处理的注解类型, 这里需要处理的是 apt 包下的 Hello 注解(这里也可以不用注解, 改成重写父类中对应的两个方法)public class HelloProcessor extends AbstractProcessor {    // 计数器, 用于计算 process() 方法运行了几次    private int count = 1;    // 用于写文件    private Filer filer;    @Override    public synchronized void init(ProcessingEnvironment processingEnv) {        super.init(processingEnv);        filer = processingEnv.getFiler();    }    // 处理编译时注解的方法    @Override    public boolean process(Set
annotations, RoundEnvironment roundEnv) { System.out.println("start process, count = " + count++); // 获得所有类 Set
rootElements = roundEnv.getRootElements(); System.out.println("all class:"); for (Element rootElement : rootElements) { System.out.println(" " + rootElement.getSimpleName()); } // 获得有注解的元素, 这里 Hello 只能修饰类, 所以只有类 Set
elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class); System.out.println("annotated class:"); for (Element element : elementsAnnotatedWith) { String className = element.getSimpleName().toString(); System.out.println(" " + className); String output = element.getAnnotation(Hello.class).name(); // 产生的动态类的名字 String newClassName = className + "_New"; // 写 java 文件 createFile(newClassName, output); } return true; } private void createFile(String className, String output) { StringBuilder cls = new StringBuilder(); cls.append("package apt;\n\npublic class ") .append(className) .append(" {\n public static void main(String[] args) {\n") .append(" System.out.println(\"") .append(output) .append("\");\n }\n}"); try { JavaFileObject sourceFile = filer.createSourceFile("apt." + className); Writer writer = sourceFile.openWriter(); writer.write(cls.toString()); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } }}

代码的逻辑很简单:

  • 获得所有标有注解的类

  • 取出注解中的信息

  • 生成新的 java 文件

这里只需要知道, 自定义注解处理器要继承 AbstractProcessor 类, 并重写 process 方法.

2. 运行

此时项目目录如下, 这里 out 目录为手动创建

  • out

    • production

      • apt

  • src

    • apt

在命令行中进入项目根目录, 即 src 文件夹的上一层.

首先编译注解处理器: javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java

接着执行注解处理器: javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java

得到如下输出

start process, count = 1all class:  Hello  HelloProcessor  Ignored  Playerannotated class:  Playerstart process, count = 2all class:  Player_Newannotated class:start process, count = 3all class:annotated class:

这时 src/apt 目录下会出现新的 Player_New.java 文件, 内容如下

package apt;public class Player_New {  public static void main(String[] args) {    System.out.println("world");  }}

执行 java -cp out\production\elevator apt.Player_New

得到输出 world.

到这里, 编译时注解便处理成功了. 我们定义了一个极其简单的注解处理器, 读取了注解信息, 并生成了新的 java 类来打印该信息.

这里可能会报一个错误

编译器 (1.8.0_131) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。java.lang.IllegalStateException: endPosTable already set......

这时把产生的 Player_New.java 文件删去重新执行注解处理器就好了

3. javac

这里稍微解释一下 javac 命令, IDE 用多了, 写的时候都忘得差不多了 (:зゝ∠)

javac 用于启动 java 编译器, 格式为 javac <options> <source files>, 其中 <options> 的格式为 -xx xxxx, 都是配对出现的, 用于指定一些信息.

这里 <options> 的位置并没有讲究, 只要在 javac 后面就行了, 在两个 xxx.java 之间出现也是可以的, 比如: javac -d out\production\ src\apt\HelloProcessor.java -encoding UTF-8 src\apt\Hello.java 正常执行.

一些 <option>

  • -cp <路径>

    • -classpath <路径> 一样, 用于指定查找用户类文件和注释处理程序的位置

  • -d <目录>

    • 指定放置生成的类文件的位置

  • -s <目录>

    • 指定放置生成的源文件的位置

  • -processorpath <路径>

    • 指定查找注释处理程序的位置

    • 不写的话会使用 -cp 的位置

  • -processor <class1>[,<class2>,<class3>...]

    • 要运行的注释处理程序的名称; 绕过默认的搜索进程

4. 问题

到这里应该会有一些问题, 比如

  1. AbstractProcessor, Elememt 分别是什么

  2. process 为什么执行了 3 次

  3. 运行注解处理器的时候会启动 jvm

这里先说一下第三个问题. javac 运行注解处理器的时候, 会开一个完整的 java 虚拟机执行代码, 所以自定义的注解处理器是可以使用各种类库的.

接下来讲一下一些基本概念, 用来回答上面两个问题.

4.概念

1. AbstractProcessor

  • 这是处理器的API,所有的处理器都是基于 AbstractProcessor, 它实现了接口 Processor

  • 接口

    • void init(ProcessingEnvironment processingEnv);

      • 会被注解处理工具调用, ProcessingEnvironment 提供了一些实用的工具类 Elements, TypesFiler.

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

      • 相当于 main 函数, 是注解处理器的入口. 输入参数 RoundEnviroment 可以查询出包含特定注解的被注解元素

    • SourceVersion getSupportedSourceVersion();

      • 用来指定使用的 java 版本

    • Set<String> getSupportedAnnotationTypes();

      • 指定这个注解处理器是注册给哪个注解的, 这里需要用注解的全称, 比如上面的 apt.Hello

    • 最后两个也可以用注解的形式实现, 例子中的代码就是这么做的

2. Element

  • 程序的元素, 例如包, 类或者方法. 每个 Element 代表一个静态的, 语言级别的构件. 可以参考下面的代码理解

package com.example;    // PackageElementpublic class Foo {        // TypeElement    private int a;      // VariableElement    private Foo other;  // VariableElement    public Foo () {}    // ExecuteableElement    public void setA (  // ExecuteableElement                     int newA   // TypeElement                     ) {}}

由此可见 roundEnv.getElementsAnnotatedWith(xxx.class) 得到的并不一定是类, 也可能是方法, 成员变量等, 只是例子中用的注解只能用于修饰类.

3. 注解处理器的执行

javadoc 中对此的描述如下

Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing.

概况来说, 就是 process() 方法会被调用多次, 直到没有新的类产生为止.

因为新生成的文件中也可能包含 @Hello 注解,它们会继续被 HelloProcessor 处理.

Round input output
1 Hello.java
HelloProcessor.java
Ignored.java
Player.java
Player_New.java
2 Player_New.java -
3 - -

下一篇会开始分析 Butter Knife 的源码.

转载地址:http://rvwgl.baihongyu.com/

你可能感兴趣的文章
广西男子酒驾冲撞宵夜摊多人受伤 民众自发抬车救人
查看>>
QuestMobile:抖音快手双巨头并进 短视频时长超越在线视频
查看>>
2019年春运首日 河北实现“空地一体”立体化救援
查看>>
蚂蚁金服mPaaS 3.0发布 助力客户智能化构建超级App生态
查看>>
如何实现全屏遮罩(附Vue.extend和el-message源码学习)
查看>>
阿里:千亿交易背后的0故障发布
查看>>
利用angular4和nodejs-express构建一个简单的网站(十)—好友模块
查看>>
极光大数据告诉你,程序员们都在"愁"些啥?
查看>>
前端基础知识学习记录(三)
查看>>
LeanCloud + Ionic3 迅速重构应用
查看>>
chrome扩展推荐:帮你留住每一次ctrl+c --- Clipboard History 2
查看>>
Spring Web Services 3.0.4.RELEASE和2.4.3.RELEASE发布
查看>>
配置一次,到处运行:将配置与运行时解耦
查看>>
菜鸟成都未来园区启动,无人车首次进入园区调拨运输环节 ...
查看>>
算法不扎实的程序员,每个都很慌
查看>>
Element 2.6.3 发布,基于 Vue 2.0 的桌面端组件库
查看>>
基于kubeadm的kubernetes高可用集群部署
查看>>
定位「数字化助手」,腾讯想用服务创新助力产业智慧升级
查看>>
golang之sync.Mutex互斥锁源码分析
查看>>
SAP增强的PA教材内容
查看>>