简单介绍 Java 中的编译时注解

1. 前言

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

2. 做用

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

3. 例子

1. 代码

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

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

使用注解的类框架

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

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

package apt;
public class Ignored {
}

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

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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("start process, count = " + count++);
        // 得到全部类
        Set<? extends Element> rootElements = roundEnv.getRootElements();
        System.out.println("all class:");

        for (Element rootElement : rootElements) {
            System.out.println("  " + rootElement.getSimpleName());
        }

        // 得到有注解的元素, 这里 Hello 只能修饰类, 因此只有类
        Set<? extends Element> 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 文件ui

这里只须要知道, 自定义注解处理器要继承 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 = 1
all class:
  Hello
  HelloProcessor
  Ignored
  Player
annotated class:
  Player
start process, count = 2
all class:
  Player_New
annotated class:
start process, count = 3
all 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;    // PackageElement

public 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 的源码.

相关文章
相关标签/搜索