Java注解处理器学习之编译时处理的注解详析
1.一些基本概念
在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解。
编译时注解跟运行时注解到底区别在什么地方?其实说大也不大,主要是考虑到性能上面的问题。运行时注解主要是完全依赖于反射,反射的效率比原生的慢,所以在内存比较少,CPU比较烂的机器上会有一些卡顿现象出现。而编译时注解完全不会有这个问题,因为它在我们编译过程(java->class)中,通过一些注解标示,去动态生成一些类或者文件,所以跟我们的APK运行完全没有任何关系,自然就不存在性能上的问题。所以一般比较著名的开源项目如果采用注解功能,通常采用编译时注解
注解处理器是javac自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。这里,我假设你已经了解什么是注解及如何自定义注解。如果你还未了解注解的话,可以查看官方文档。注解处理器在Java5的时候就已经存在了,但直到Java6(发布于2006看十二月)的时候才有可用的API。过了一段时间java的使用者们才意识到注解处理器的强大。所以最近几年它才开始流行。
一个特定注解的处理器以java源代码(或者已编译的字节码)作为输入,然后生成一些文件(通常是.java文件)作为输出。那意味着什么呢?你可以生成java代码!这些java代码在生成的.java文件中。因此你不能改变已经存在的java类,例如添加一个方法。这些生成的java文件跟其他手动编写的java源代码一样,将会被javac编译。
Annotationprocessing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(AnnotationProcessor)不能改变读入的Java类,比如不能加入或删除Java方法。
2.AbstractProcessor
让我们来看一下处理器的API。所有的处理器都继承了AbstractProcessor,如下所示:
packagecom.example; importjava.util.LinkedHashSet; importjava.util.Set; importjavax.annotation.processing.AbstractProcessor; importjavax.annotation.processing.ProcessingEnvironment; importjavax.annotation.processing.RoundEnvironment; importjavax.annotation.processing.SupportedAnnotationTypes; importjavax.annotation.processing.SupportedSourceVersion; importjavax.lang.model.SourceVersion; importjavax.lang.model.element.TypeElement; publicclassMyProcessorextendsAbstractProcessor{ @Override publicbooleanprocess(Setannoations, RoundEnvironmentenv){ returnfalse; } @Override publicSetgetSupportedAnnotationTypes(){ Set annotataions=newLinkedHashSet (); annotataions.add("com.example.MyAnnotation"); returnannotataions; } @Override publicSourceVersiongetSupportedSourceVersion(){ returnSourceVersion.latestSupported(); } @Override publicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){ super.init(processingEnv); } }
init(ProcessingEnvironmentprocessingEnv):所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment提供了一些实用的工具类Elements,Types和Filer。我们在后面将会使用到它们。
process(Setannoations,RoundEnvironmentenv) :这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成java文件。使用RoundEnvironment参数,你可以查询被特定注解标注的元素(原文:youcanqueryforelementsannotatedwithacertainannotation)。后面我们将会看到详细内容。
getSupportedAnnotationTypes():在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。
getSupportedSourceVersion():用来指定你使用的java版本。通常你应该返回SourceVersion.latestSupported()。不过,如果你有足够的理由坚持用java6的话,你也可以返回SourceVersion.RELEASE_6。我建议使用SourceVersion.latestSupported()。在Java7中,你也可以使用注解的方式来替代重写getSupportedAnnotationTypes()和getSupportedSourceVersion(),如下所示:
@SupportedSourceVersion(value=SourceVersion.RELEASE_7) @SupportedAnnotationTypes({ //Setoffullqullifiedannotationtypenames "com.example.MyAnnotation", "com.example.AnotherAnnotation" }) publicclassMyProcessorextendsAbstractProcessor{ @Override publicbooleanprocess(Setannoations, RoundEnvironmentenv){ returnfalse; } @Override publicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){ 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(提拉米苏)。
publicinterfaceMeal{ publicfloatgetPrice(); } publicclassMargheritaPizzaimplementsMeal{ @Override publicfloatgetPrice(){ return6.0f; } } publicclassCalzonePizzaimplementsMeal{ @Override publicfloatgetPrice(){ return8.5f; } } publicclassTiramisuimplementsMeal{ @Override publicfloatgetPrice(){ return4.5f; } } publicclassPizzaStore{ publicMealorder(StringmealName){ if(null==mealName){ thrownewIllegalArgumentException("nameofmealisnull!"); } if("Margherita".equals(mealName)){ returnnewMargheritaPizza(); } if("Calzone".equals(mealName)){ returnnewCalzonePizza(); } if("Tiramisu".equals(mealName)){ returnnewTiramisu(); } thrownewIllegalArgumentException("Unknownmeal'"+mealName+"'"); } privatestaticStringreadConsole(){ Scannerscanner=newScanner(System.in); Stringmeal=scanner.nextLine(); scanner.close(); returnmeal; } publicstaticvoidmain(String[]args){ System.out.println("welcometopizzastore"); PizzaStorepizzaStore=newPizzaStore(); Mealmeal=pizzaStore.order(readConsole()); System.out.println("Bill:$"+meal.getPrice()); } }
正如你所见,在order()方法中,我们有许多if条件判断语句。并且,如果我们添加一种新的pizza的话,我们就得添加一个新的if条件判断。但是等一下,使用注解处理器和工厂模式,我们可以让一个注解处理器生成这些if语句。如此一来,我们想要的代码就像这样子:
publicclassPizzaStore{ privateMealFactoryfactory=newMealFactory(); publicMealorder(StringmealName){ returnfactory.create(mealName); } privatestaticStringreadConsole(){ Scannerscanner=newScanner(System.in); Stringmeal=scanner.nextLine(); scanner.close(); returnmeal; } publicstaticvoidmain(String[]args){ System.out.println("welcometopizzastore"); PizzaStorepizzaStore=newPizzaStore(); Mealmeal=pizzaStore.order(readConsole()); System.out.println("Bill:$"+meal.getPrice()); } } publicclassMealFactory{ publicMealcreate(Stringid){ if(id==null){ thrownewIllegalArgumentException("idisnull!"); } if("Calzone".equals(id)){ returnnewCalzonePizza(); } if("Tiramisu".equals(id)){ returnnewTiramisu(); } if("Margherita".equals(id)){ returnnewMargheritaPizza(); } thrownewIllegalArgumentException("Unknownid="+id); } }
5.@FactoryAnnotation
能猜到么,我们打算使用注解处理器生成MealFactory类。更一般的说,我们想要提供一个注解和一个处理器用来生成工厂类。
让我们看一下@Factory注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public@interfaceFactory{ /** *Thenameofthefactory */ Class>type(); /** *Theidentifierfordeterminingwhichitemshouldbeinstantiated */ Stringid(); }
思想是这样的:我们注解那些食物类,使用type()表示这个类属于哪个工厂,使用id()表示这个类的具体类型。让我们将@Factory注解应用到这些类上吧:
@Factory(type=MargheritaPizza.class,id="Margherita") publicclassMargheritaPizzaimplementsMeal{ @Override publicfloatgetPrice(){ return6.0f; } } @Factory(type=CalzonePizza.class,id="Calzone") publicclassCalzonePizzaimplementsMeal{ @Override publicfloatgetPrice(){ return8.5f; } } @Factory(type=Tiramisu.class,id="Tiramisu") publicclassTiramisuimplementsMeal{ @Override publicfloatgetPrice(){ return4.5f; } }
你可能会问,我们是不是可以只将@Factory注解应用到Meal接口上?答案是不行,因为注解是不能被继承的。即在classX上有注解,classYextendsX,那么classY是不会继承classX上的注解的。在我们编写处理器之前,需要明确几点规则:
- 只有类能够被@Factory注解,因为接口和虚类是不能通过new操作符实例化的。
- 被@Factory注解的类必须提供一个默认的无参构造函数。否则,我们不能实例化一个对象。
- 被@Factory注解的类必须直接继承或者间接继承type指定的类型。(或者实现它,如果type指定的是一个接口)
- 被@Factory注解的类中,具有相同的type类型的话,这些类就会被组织起来生成一个工厂类。工厂类以Factory作为后缀,例如:type=Meal.class将会生成MealFactory类。
- id的值只能是字符串,且在它的type组中必须是唯一的。
注解处理器:
publicclassFactoryProcessorextendsAbstractProcessor{ privateTypestypeUtils; privateElementselementUtils; privateFilerfiler; privateMessagermessager; privateMapfactoryClasses= newLinkedHashMap (); @Override publicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){ super.init(processingEnv); typeUtils=processingEnv.getTypeUtils(); elementUtils=processingEnv.getElementUtils(); filer=processingEnv.getFiler(); messager=processingEnv.getMessager(); } @Override publicbooleanprocess(Setarg0, RoundEnvironmentarg1){ ... returnfalse; } @Override publicSet getSupportedAnnotationTypes(){ Set annotataions=newLinkedHashSet (); annotataions.add(Factory.class.getCanonicalName()); returnannotataions; } @Override publicSourceVersiongetSupportedSourceVersion(){ returnSourceVersion.latestSupported(); } }
在getSupportedAnnotationTypes()方法中,我们指定@Factory注解将被这个处理器处理。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。