Java注解处理器--编译时处理的注解
1. 一些基本概念
在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解。
注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。这里,我假设你已经了解什么是注解及如何自定义注解。如果你还未了解注解的话,可以查看官方文档。注解处理器在 Java 5 的时候就已经存在了,但直到 Java 6 (发布于2006看十二月)的时候才有可用的API。过了一段时间java的使用者们才意识到注解处理器的强大。所以最近几年它才开始流行。
一个特定注解的处理器以 java 源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java
文件)作为输出。那意味着什么呢?你可以生成 java 代码!这些 java 代码在生成的.java
文件中。因此你不能改变已经存在的java类,例如添加一个方法。这些生成的 java 文件跟其他手动编写的 java 源代码一样,将会被 javac 编译。
Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。
2. AbstractProcessor
让我们来看一下处理器的 API。所有的处理器都继承了AbstractProcessor
,如下所示:
package com.example; import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement; public class MyProcessor extends AbstractProcessor { @Override
public boolean process(Set<? extends TypeElement> annoations,
RoundEnvironment env) {
return false;
} @Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add("com.example.MyAnnotation");
return annotataions;
} @Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
} @Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
} }
init(ProcessingEnvironment processingEnv)
:所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init()
,它会被注解处理工具调用,以ProcessingEnvironment
作为参数。ProcessingEnvironment 提供了一些实用的工具类Elements
,Types
和Filer
。我们在后面将会使用到它们。process(Set<? extends TypeElement> annoations, RoundEnvironment env)
:这类似于每个处理器的main()
方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment
参数,你可以查询被特定注解标注的元素(原文:you can query for elements annotated with a certain annotation )。后面我们将会看到详细内容。getSupportedAnnotationTypes()
:在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String
集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。getSupportedSourceVersion()
: 用来指定你使用的 java 版本。通常你应该返回SourceVersion.latestSupported()
。不过,如果你有足够的理由坚持用 java 6 的话,你也可以返回SourceVersion.RELEASE_6
。我建议使用SourceVersion.latestSupported()
。在 Java 7 中,你也可以使用注解的方式来替代重写getSupportedAnnotationTypes()
和getSupportedSourceVersion()
,如下所示:
@SupportedSourceVersion(value=SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({
// Set of full qullified annotation type names
"com.example.MyAnnotation",
"com.example.AnotherAnnotation"
})
public class MyProcessor extends AbstractProcessor { @Override
public boolean process(Set<? extends TypeElement> annoations,
RoundEnvironment env) {
return false;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
}
由于兼容性问题,特别是对于 android ,我建议重写getSupportedAnnotationTypes()
和 getSupportedSourceVersion()
,而不是使用 @SupportedAnnotationTypes
和 @SupportedSourceVersion
。
接下来你必须知道的事情是:注解处理器运行在它自己的 JVM 中。是的,你没看错。javac 启动了一个完整的 java 虚拟机来运行注解处理器。这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。使用 guava
! 你可以使用依赖注入工具,比如dagger
或者任何其他你想使用的类库。但不要忘记,即使只是一个小小的处理器,你也应该注意使用高效的算法及设计模式,就像你在开发其他 java 程序中所做的一样。
3. 注册你的处理器
你可能会问 “怎样注册我的注解处理器到 javac ?”。你必须提供一个.jar
文件。就像其他 .jar 文件一样,你将你已经编译好的注解处理器打包到此文件中。并且,在你的 .jar 文件中,你必须打包一个特殊的文件javax.annotation.processing.Processor
到META-INF/services
目录下。因此你的 .jar 文件目录结构看起来就你这样:
MyProcess.jar
-com
-example
-MyProcess.class
-META-INF
-services
-javax.annotation.processing.Processor
javax.annotation.processing.Processor
文件的内容是一个列表,每一行是一个注解处理器的全称。例如:
com.example.MyProcess
com.example.AnotherProcess
4. 例子:工厂模式
我们要解决的问题是:我们要实现一个 pizza 店,这个 pizza 店提供给顾客两种 pizza (Margherita 和 Calzone),还有甜点 Tiramisu(提拉米苏)。
public interface Meal {
public float getPrice();
}
public class MargheritaPizza implements Meal{
@Override
public float getPrice() {
return 6.0f;
}
}
public class CalzonePizza implements Meal{
@Override
public float getPrice() {
return 8.5f;
}
}
public class Tiramisu implements Meal{
@Override
public float getPrice() {
return 4.5f;
}
} public class PizzaStore { public Meal order(String mealName) {
if (null == mealName) {
throw new IllegalArgumentException("name of meal is null!");
}
if ("Margherita".equals(mealName)) {
return new MargheritaPizza();
} if ("Calzone".equals(mealName)) {
return new CalzonePizza();
} if ("Tiramisu".equals(mealName)) {
return new Tiramisu();
} throw new IllegalArgumentException("Unknown meal '" + mealName + "'");
} private static String readConsole() {
Scanner scanner = new Scanner(System.in);
String meal = scanner.nextLine();
scanner.close();
return meal;
} public static void main(String[] args) {
System.out.println("welcome to pizza store");
PizzaStore pizzaStore = new PizzaStore();
Meal meal = pizzaStore.order(readConsole());
System.out.println("Bill:$" + meal.getPrice());
}
}
正如你所见,在order()
方法中,我们有许多 if 条件判断语句。并且,如果我们添加一种新的 pizza 的话,我们就得添加一个新的 if 条件判断。但是等一下,使用注解处理器和工厂模式,我们可以让一个注解处理器生成这些 if 语句。如此一来,我们想要的代码就像这样子:
public class PizzaStore { private MealFactory factory = new MealFactory(); public Meal order(String mealName) {
return factory.create(mealName);
} private static String readConsole() {
Scanner scanner = new Scanner(System.in);
String meal = scanner.nextLine();
scanner.close();
return meal;
} public static void main(String[] args) {
System.out.println("welcome to pizza store");
PizzaStore pizzaStore = new PizzaStore();
Meal meal = pizzaStore.order(readConsole());
System.out.println("Bill:$" + meal.getPrice());
}
} public class MealFactory { public Meal create(String id) {
if (id == null) {
throw new IllegalArgumentException("id is null!");
}
if ("Calzone".equals(id)) {
return new CalzonePizza();
} if ("Tiramisu".equals(id)) {
return new Tiramisu();
} if ("Margherita".equals(id)) {
return new MargheritaPizza();
} throw new IllegalArgumentException("Unknown id = " + id);
}
}
5. @Factory Annotation
能猜到么,我们打算使用注解处理器生成MealFactory
类。更一般的说,我们想要提供一个注解和一个处理器用来生成工厂类。
让我们看一下@Factory
注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory { /**
* The name of the factory
*/
Class<?> type(); /**
* The identifier for determining which item should be instantiated
*/
String id();
}
思想是这样的:我们注解那些食物类,使用type()
表示这个类属于哪个工厂,使用id()
表示这个类的具体类型。让我们将@Factory
注解应用到这些类上吧:
@Factory(type=MargheritaPizza.class, id="Margherita")
public class MargheritaPizza implements Meal{ @Override
public float getPrice() {
return 6.0f;
}
} @Factory(type=CalzonePizza.class, id="Calzone")
public class CalzonePizza implements Meal{ @Override
public float getPrice() {
return 8.5f;
}
} @Factory(type=Tiramisu.class, id="Tiramisu")
public class Tiramisu implements Meal{ @Override
public float getPrice() {
return 4.5f;
}
}
你可能会问,我们是不是可以只将@Factory
注解应用到Meal
接口上?答案是不行,因为注解是不能被继承的。即在class X
上有注解,class Y extends X
,那么class Y
是不会继承class X
上的注解的。在我们编写处理器之前,需要明确几点规则:
- 只有类能够被
@Factory
注解,因为接口和虚类是不能通过new
操作符实例化的。 - 被
@Factory
注解的类必须提供一个默认的无参构造函数。否则,我们不能实例化一个对象。 - 被
@Factory
注解的类必须直接继承或者间接继承type
指定的类型。(或者实现它,如果type
指定的是一个接口) - 被
@Factory
注解的类中,具有相同的type
类型的话,这些类就会被组织起来生成一个工厂类。工厂类以Factory
作为后缀,例如:type=Meal.class
将会生成MealFactory
类。 id
的值只能是字符串,且在它的type
组中必须是唯一的。
注解处理器:
public class FactoryProcessor extends AbstractProcessor { private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
private Map<String, FactoryGroupedClasses> factoryClasses =
new LinkedHashMap<String, FactoryGroupedClasses>(); @Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
} @Override
public boolean process(Set<? extends TypeElement> arg0,
RoundEnvironment arg1) {
...
return false;
} @Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(Factory.class.getCanonicalName());
return annotataions;
} @Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
在getSupportedAnnotationTypes()
方法中,我们指定@Factory
注解将被这个处理器处理。
Java注解处理器--编译时处理的注解的更多相关文章
- apt 根据注解,编译时生成代码
apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...
- java中的编译时常量与运行时常量
常量是程序运行期间恒定不变的量,许多程序设计语言都有某种方式,向编译器告知一块数据是恒定不变的,例如C++中的const和Java中的final. 根据编译器的不同行为,常量又分为编译时常量和运行时常 ...
- cmd命令对java程序进行编译时出现:编码GBK的不可映射字符
原因:由于JDK是国际版的,在编译的时候,如果我们没有用-encoding参数指定JAVA源程序的编码格式,则java.exe首先获得我们才做系统默认采用的编码格式,也即在编译JAVA程序时,若我们不 ...
- Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)
注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理.在编译期处理注解时,会处理到不再产生新的源文件为止,之后再对所有源文件进行编译. Java5中提供了apt工具来进行编译期的注解处 ...
- Java注解处理器(转)
Java中的注解(Annotation)是一个很神奇的东西,特别现在有很多Android库都是使用注解的方式来实现的.一直想详细了解一下其中的原理.很有幸阅读到一篇详细解释编写注解处理器的文章.本文的 ...
- Java注解处理器使用详解
在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor).在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么:然后,我将一步一步实 ...
- Java注解(2)-注解处理器(运行时|RetentionPolicy.RUNTIME)
如果没有用来读取注解的工具,那注解将基本没有任何作用,它也不会比注释更有用.读取注解的工具叫作注解处理器.Java提供了两种方式来处理注解:第一种是利用运行时反射机制:另一种是使用Java提供的API ...
- Android 打造编译时注解解析框架 这只是一个开始
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...
- 使用编译时注解简单实现类似 ButterKnife 的效果
这篇文章是学习鸿洋前辈的 Android 如何编写基于编译时注解的项目 的笔记,用于记录我的学习收获. 读完本文你将了解: 什么是编译时注解 APT 编译时注解如何使用与编写 举个例子 思路 创建注解 ...
随机推荐
- React Native的WebStorm基本设置
jsx语法设置 在没有进行设置的情况下,每次打开WebStorm的时候打开包含jsx语法的.js文件都会有以下提示: 当然我们点击转换后就可以了,但是每次都会提示,所以还是来一个一劳永逸的方法把它给去 ...
- 9.4、Libgdx简单字符输入
(官网:www.libgdx.cn) 如果应用需要输入一个字符,比如用户名和密码,可以通过简单的对话框实现. 在桌面中使用一个Swing对话框,提示用户输入字符. 在Android中将会打开一个标准的 ...
- CentOS安装并设置MariaDB
作者: 铁锚 日期: 2013年12月27日 部分参考: Centos 使用YUM安装MariaDB 说明: 首先必须能链接外网. 如果不能直接访问,那也可以设置代理,请参考: 在内网机器上设置yum ...
- rpcz VC2010 构建
rpcz VC2010 构建 rpcz 是应用ZeroMQ和Protobuf开发的RPC. 见: https://github.com/reinferio/rpcz 及 https://code.go ...
- 1.Linux下libevent和memcached安装
1 下载libevent-2.0.22-stable.tar.gz,下载地址是:http://libevent.org/ 2 下载memcached,下载地址是:http://memcached ...
- Android源码浅析(一)——VMware Workstation Pro和Ubuntu Kylin 16.04 LTS安装配置
Android源码浅析(一)--VMware Workstation Pro和Ubuntu Kylin 16.04 LTS安装配置 最近地方工作,就是接触源码的东西了,所以好东西还是要分享,系列开了这 ...
- 《java入门第一季》之面向对象(方法重写问题)
方法重载的引入:根据一个案例: /* 继承中成员方法的关系: A:子类中的方法和父类中的方法声明不一样,这个太简单. B:子类中的方法和父类中的方法声明一样,这个该怎么玩呢? 通过子类对象调用方法: ...
- mysql进阶(十三)命令行导出导入数据库
MySQL命令行导出导入数据库 MySQL命令行导出数据库: 1,进入MySQL目录下的bin文件夹:cd MySQL中到bin文件夹的目录 如我输入的命令行:cd D:\Program Files\ ...
- Unix - 文件中构成一个空洞的分析
lseek函数显示地为一个打开文件设置偏移量,文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的.位于文件中但没有写过的字节都被读为 ...
- 二分算法C实现
#include <stdio.h> #include <stdlib.h> #define NR(x) (sizeof(x)/sizeof(x[0])) int Binary ...