Java注解处理器学习之编译时处理的注解详析

1. 一些基本概念

在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解。

编译时注解跟运行时注解到底区别在什么地方?其实说大也不大,主要是考虑到性能上面的问题。运行时注解主要是完全依赖于反射,反射的效率比原生的慢,所以在内存比较少,CPU比较烂的机器上会有一些卡顿现象出现。而编译时注解完全不会有这个问题,因为它在我们编译过程(java->class)中,通过一些注解标示,去动态生成一些类或者文件,所以跟我们的APK运行完全没有任何关系,自然就不存在性能上的问题。所以一般比较著名的开源项目如果采用注解功能,通常采用编译时注解

注解处理器是 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注解将被这个处理器处理。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 深入理解Java注解的使用方法

    注解是jdk1.5新增的特性.大家都知道,jdk1.5在java的发展史上有着划时代的意义.而注解的出现,在某种程度上颠覆了框架的设计.比如,spring在注解出现后,改善了原先五大组件的模式,增加了基于注解的实现方式.现在重点讲讲注解的使用. 元注解: jdk1.5定义了4个元注解,元注解的作用是注解其他的注解. 1.@Retention 2.@Target 3.@Documented 4.@Inherited @Retention用于指明该注解存在的时机.参数有三个值可选:Retention

  • Java注解与反射原理说明

    一 点睛 注解若想发挥更大作用,还需借助反射机制之力.通过反射,可以取得一个方法上声明的注解的全部内容. 一般有两种需求: 1 取得方法中全部的注解,通过调用getAnnotations来实现. 2 判断操作是否是指定注解,通过调用getAnnotation来实现. 下面从源码角度来说明怎样获取这些注解信息. 二 源码导读--取得方法中全部的注解 public class AccessibleObject implements AnnotatedElement { ... //取得全部Annot

  • 深入理解 Java注解及实例

     Java注解 什么是注解? Java中的注解就是Java源代码的元数据,也就是说注解是用来描述Java源代码的. 基本语法就是:@后面跟注解的名称. ①Override:标识某一个方法是否正确覆盖了它的父类的方法. ②Deprecated:表示已经不建议使用这个类成员了. 它是一个标记注解. ③SuppressWarnings:用来抑制警告信息 等等. 要更好的理解注解,我们可以自己写一个注解 @Target : 用来限制注解可以用到那几个地方.比方可以用到类上,可以用到方法胜都可以用@Tar

  • 浅谈Java注解和动态代理

    本文主要介绍Java中与注解和动态代理有关的部分知识,接下来我们看看具体内容. Annotation(注解) 其实就是代码里的特殊标记, 它用于替代配置文件,也就是说,传统方式通过配置文件告诉类如何运行,有了注解技术后,开发人员可以通过注解告诉类如何运行. 1. 三个基本的Annotation: Override:限定重写父类方法, 该注解只能用于方法 Deprecated:用于表示某个程序元素(类, 方法等)已过时 SuppressWarnings:抑制编译器警告. 2.自定义Annotati

  • 全面剖析java中的注解(Annotation)

    1.什么是注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: @Override public String toString() { return "This is String Representation of current object."; } 上面的代码中,我重写了toString()方法并使用了@Override注解.但是,即使我不使用@Override注解标记代码,程序也能够正常执行.那么,该注解

  • Java注解Annotation与自定义注解详解

    一:Java注解简介 开发中经常使用到注解,在项目中也偶尔会见到过自定义注解,今天就来探讨一下这个注解是什么鬼,以及注解的应用场景和如何自定义注解. 下面列举开发中常见的注解 @Override:用于标识该方法继承自超类, 当父类的方法被删除或修改了,编译器会提示错误信息(我们最经常看到的toString()方法上总能看到这货) @Deprecated:表示该类或者该方法已经不推荐使用,已经过期了,如果用户还是要使用,会生成编译的警告 @SuppressWarnings:用于忽略的编译器警告信息

  • java注解的全面分析

    全面解析java注解 Java中的常见注解 a.JDK中的注解 @Override 覆盖父类或者父接口的方法     @Deprecated 表示方法已经过时     @SuppressWarnings("deprecation") 忽略方法过时警告 b.常见的第三方注解 例如Spring中的@Autowired(自动注入) 注解的分类 a.按照运行机制分 1.源码注解         注解只在源码中存在,编译成class文件就不存在了 2.编译时注解         注解在源码和cl

  • 详解Java注解的实现与使用方法

    详解Java注解的实现与使用方法 Java注解是java5版本发布的,其作用就是节省配置文件,增强代码可读性.在如今各种框架及开发中非常常见,特此说明一下. 如何创建一个注解 每一个自定义的注解都由四个元注解组成,这四个元注解由java本身提供: @Target(ElementType.**) 这是一个枚举,它置顶是该自定义的注解使用的地方,像类.变量.方法等 @Retention(RetentionPolicy.**)作用是标明注解保存在什么级别,像在编译时.class文件中,vm运行中 @D

  • Java注解机制之Spring自动装配实现原理详解

    Java中使用注解的情况主要在SpringMVC(Spring Boot等),注解实际上相当于一种标记语言,它允许你在运行时动态地对拥有该标记的成员进行操作.注意:spring框架默认不支持自动装配的,要想使用自动装配需要修改spring配置文件中<bean>标签的autowire属性. 自动装配属性有6个值可选,分别代表不同的含义: byName ->从Spring环境中获取目标对象时,目标对象中的属性会根据名称在整个Spring环境中查找<bean>标签的id属性值.如果

  • Java注解处理器简单实例

    如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了.使用注解的过程中,很重要的一部分就是创建于使用注解处理器.JavaSE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器. 注解处理器类库(java.lang.reflect.AnnotatedElement): Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口.除此之外,Java在java.lang.reflect包下新增了AnnotatedElement

随机推荐