资讯专栏INFORMATION COLUMN

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

solocoder / 1725人阅读

摘要:如果在中没有找到该错误请通过报告页建立该编译器。请在报告中附上您的程序和以下诊断信息。

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;

public class ")
                .append(className)
                .append(" {
  public static void main(String[] args) {
")
                .append("    System.out.println("")
                .append(output)
                .append("");
  }
}");
        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 outproduction srcaptHelloProcessor.java srcaptHello.java

接着执行注解处理器: javac -encoding UTF-8 -cp outproduction -processor apt.HelloProcessor -d outproduction -s src srcapt*.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 outproductionelevator 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 , 其中 的格式为 -xx xxxx, 都是配对出现的, 用于指定一些信息.

这里 的位置并没有讲究, 只要在 javac 后面就行了, 在两个 xxx.java 之间出现也是可以的, 比如: javac -d outproduction srcaptHelloProcessor.java -encoding UTF-8 srcaptHello.java 正常执行.

一些

-cp <路径>

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

-d <目录>

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

-s <目录>

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

-processorpath <路径>

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

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

-processor [,,...]

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

4. 问题

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

AbstractProcessor, Elememt 分别是什么

process 为什么执行了 3 次

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

这里先说一下第三个问题. javac 运行注解处理器的时候, 会开一个完整的 java 虚拟机执行代码, 所以自定义的注解处理器是可以使用各种类库的.
接下来讲一下一些基本概念, 用来回答上面两个问题.

4.概念 1. AbstractProcessor

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

接口

void init(ProcessingEnvironment processingEnv);

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

boolean process(Set annotations, RoundEnvironment roundEnv);

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

SourceVersion getSupportedSourceVersion();

用来指定使用的 java 版本

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70117.html

相关文章

  • 简单介绍 Java 中的注解 (Annotation)

    摘要:例子首先来看一个例子这里用了目的是告诉编译器这个方法重写了父类的方法如果编译器发现父类中没有这个方法就会报错这个注解的作用大抵是防止手滑写错方法同时增强了程序的可读性这里需要指出一点去掉并不会影响程序的执行只是起到标记的作用找到的实现关注点 1. 例子 首先来看一个例子: @Override public String toString() { return xxxxx; ...

    LMou 评论0 收藏0
  • Java™ 教程(注解

    注解 注解(一种元数据形式)提供有关不属于程序本身的程序的数据,注解对它们注解的代码的操作没有直接影响。 注解有许多用途,其中包括: 编译器的信息 — 编译器可以使用注解来检测错误或抑制警告。 编译时和部署时处理 — 软件工具可以处理注解信息以生成代码、XML文件等。 运行时处理 — 可以在运行时检查某些注解。 本课程介绍了可以使用注解的位置,以及如何应用注解,Java平台标准版(Java S...

    econi 评论0 收藏0
  • Java 注解实战

    摘要:注解是的一个新特性。很重要,生产中我们开发常用此值表示注解是否可被子元素继承。类注解方法注解通过反射获取方法对象此部分内容可参考通过反射获取注解信息注解处理器实战接下来我通过在公司中的一个实战改编来演示一下注解处理器的真实使用场景。 前言:Java 注解,对于很多人都不陌生了,但是在公司的实际开发中,可能让我们自己去定义注解并应用到生产环境中的机会比较少,所以会导致一部分人对注解的理解...

    Jochen 评论0 收藏0
  • APT案例之点击事件

    摘要:杨充一定时间内该点击事件只能执行一次用来修饰这是一个什么类型的注解。杨充自定义编译器获取遍历,并生成代码配置文件文件配置的作用是向系统注册自定义注解处理器,执行编译时使用进行处理。 目录介绍 01.创建项目步骤 1.1 项目搭建 1.2 项目功能 02.自定义注解 03.创建Processor 04.compiler配置文件 05.编译jar 06.如何使用 07.编译生成代...

    cyixlq 评论0 收藏0
  • 关于Apt注解实践与总结【包含20篇博客】

    摘要:使用实现功能运行期注解案例使用简单的注解,便可以设置布局,等效于使用实现路由综合型案例比较全面的介绍从零起步,一步一步封装简易的路由开源库。申明注解用的就是。返回值表示这个注解里可以存放什么类型值。 YCApt关于apt方案实践与总结 目录介绍 00.注解系列博客汇总 01.什么是apt 02.annotationProcessor和apt区别 03.项目目录结构 04.该案例作用 ...

    gnehc 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<