(2.1.19.3)深入理解Java:注解(Annotation)–编译时注解的处理
一、注解
Annotations是一种元数据,其作用在于提供程序本身以外的一些数据信息,也就是说Annotation他不会属于程序代码本身,不参与逻辑运算,故而不会对原程序代码的操作产生直接的影响。
一般来说Annotation有如下三种使用情形:
- Information for the compiler — Annotations can be used by the compiler to detect errors or suppress * warnings.
- Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
- Runtime processing — Some annotations are available to be examined at runtime.
-
SOURCE 标记一些信息,为编译器提供辅助信息。
- 可以为编译器提供而外信息,以便于检测错误,抑制警告等,譬如@Override、@SuppressWarnings等这类注解就是用于标识,可以用作一些检验。
-
CLASS 编译时动态处理。
- 一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的,也就是在类加载的时候丢弃。
- 会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别。ParcelableGenerator、butterknife 、androidannotaion都使用了类似技术
-
RUNTIME 运行时动态处理。
- 这个大家见得应该最多,在运行时拿到类的Class对象,然后遍历其方法、变量,判断有无注解声明,然后做一些事情。
- 譬如使用表单验证注解@Validate,不保留活动的@SaveInstance
来看一个示例:
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.FIELD, ElementType.TYPE })
public @interface InjectView
{
int value();
}
- @Retention后面的值 标示注解的类型
- @Target的值是一个ElementType[]数组,标明这个注解能标识哪些东西,比如类、变量、方法、甚至是注解本身(元注解)等
更多信息见 (2.1.19)注释与注解
在本篇中我们主要讲述的就是如何对 CLASS类的编译期注解的处理
二、注解处理器
在开始之前,我们再次申明一个非常重要的问题:我们并不讨论那些在运行时(Runtime)通过反射机制运行处理的注解,而是讨论在编译时(Compile time)处理的注解。
注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。到这里,我假设你已经知道什么是注解,并且知道怎么申明的一个注解类型。
一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。
这具体的含义什么呢?
你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译
三、虚处理器AbstractProcessor
我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:
package com.example;
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
-
init(ProcessingEnvironment env): 【核心】
- 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。
- ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。后面我们将看到详细的内容
Filer是个接口,支持通过注解处理器创建新文件
Elements 元素操作辅助工具类
-
process(Set< ? extends TypeElement> annotations, RoundEnvironment env):【核心】
- 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
- 输入参数annotations 请求处理的注解类型集合
- 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素,相当于“有关全局源码的上下文环境”。后面我们将看到详细的内容。
- @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
-
getSupportedAnnotationTypes()
- 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
- 换句话说,你在这里定义你的注解处理器注册到哪些注解上。
-
getSupportedSourceVersion()
- 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。
- 然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者
在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({// 合法注解全名的集合})
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
除此以外还有一个@SupportedOptions,这个一般是命令行时候用的,设置一些选项,but,命令行我不熟,因此:略。
注:如果大家找不到AbstractProcessor,记得右键build-path add library把jdk加进来
因为兼容的原因,特别是针对Android平台,我建议使用重载getSupportedAnnotationTypes()和getSupportedSourceVersion()方法代替@SupportedAnnotationTypes和@SupportedSourceVersion
接下来的你必须知道的事情是,注解处理器是运行它自己的虚拟机JVM中。
是的,你没有看错,javac启动一个完整Java虚拟机来运行注解处理器。这对你意味着什么?你可以使用任何你在其他java应用中使用的的东西。使用guava。如果你愿意,你可以使用依赖注入工具,例如dagger或者其他你想要的类库。但是不要忘记,即使是一个很小的处理,你也要像其他Java应用一样,注意算法效率,以及设计模式
3.1 处理器对全局代码的扫描处理流程
注解处理器对工程代码的扫描是多次的,可以注意到AbstractProcessor的process()方法的输入参数有一个是RoundEnvironment,这个代表一次扫描的结果
影响注解处理器执行顺序与逻辑的地方有三处
3.1.1 javax.annotation.processing.Processor中的书写顺序决定注册处理器的执行顺序
假设该文件中定义如下:
package.ProcessorB
package.ProcessorA
那么编译器每一轮扫描会先执行处理器ProcessorB,再执行处理器ProcessorA
3.1.2 AbstractProcessor中processor()方法的返回值决定是否要终结当前轮的处理
按照1中注册的顺序,假如ProcessorB中的process()方法返回true,则表示消费完这轮的注解扫描,将不再执行ProcessorA,只有当返回false时,才会接下来执行ProcessorB
3.1.3 没有输出文件跟输入文件时扫描结束
假设按照1中的注册顺序,ProcessorB中的process()方法返回true,并且ProcessorB在第一轮扫描会生成按文件GenerateB.java,则将在第三轮扫描后结束注解处理
也就是说针对生成的文件,还是再处理一次
过程如下
过程 | 输入文件 | 输出文件 | |
---|---|---|---|
第一轮 | 原工程 | GenerateB.java | |
第二轮 | GenerateB.java | 无 | |
第三轮 | 无 | 无 |
第三轮时编译器发现没有输出文件也没有输入文件,处理结束
PS:每个注解处理器在整个过程中都保持同一个实例
四 语言模型包的使用
4.1 Mirror
注解处理器因为操作的是源码,所以需要用到JAVA语言模型包,javax.lang.model及其子包都是Java的语言模型包。这个包是采用了Mirror设计,Java是一种可以自描述的语言,其反射机制就是一种自描述,传统的反射机制将自描述与其他操作合并在一起,Mirror机制将自描述跟其他操作隔离,自描述部分是Meta level,其他部分是Base level
4.2 Element详解
有了注解,必然需要有一个对应的注解处理器去处理注解,但是在处理注解的时候需要充分的了解注解处理器中的process方法及时核心的编译代码,而process方法的核心便是Element对象,所以在讲解注解处理器前,需要对Element有全面的认识,方能事半功倍。
Element在逻辑上代表语言元素,比如包,类,方法等
其实Element是定义的一个接口,定义了外部调用暴露出的接口
方法 | 解释 |
---|---|
TypeMirror asType(); | 返回此元素定义的类型,实际的java类型 |
ElementKind getKind(); | 返回此元素的种类:包、类、接口、方法、字段…,如下枚举值,方法返回一个枚举值TypeKind |
Set getModifiers() | 返回此元素的修饰符,如下枚举值 |
Name getSimpleName(); | 返回此元素的简单名称,比如activity名,变量就是变量名 |
Element getEnclosingElement(); | 返回封装此元素的最里层元素,即最近的外层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null. |
List getEnclosedElements(); | 获取所有的内层元素 |
< A extends Annotation> A getAnnotation(Class< A> var1); | 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的 |
4.2.1 转化
Element代表语言元素,比如包,类,方法等,但是Element并没有包含自身的信息,自身信息要通过Mirror来获取,每个Element都指向一个TypeMirror,这个TypeMirror里有自身的信息。通过下面获方法取Element中的Mirror
TypeMirror mirror=element.asType()
TypeMirror类型是DeclaredType或者TypeVariable时候可以转化成Element
Types types=processingEnvironment.getTypeUtils()
Element element=types.asElement(typeMirror)
4.2.2 获取类型getKind()
获取Element或者TypeMirror的类型都是通过getKind()获取类型,但是返回值虽然不同,但是都是枚举。
ElementKind element.getKind()
TypeKind typeMirror.getKind()
针对具体类型,要避免用instanceof,因为即使同一个类getKind()也有不同的结果。比如TypeElemnt的getKind()返回结果可以是枚举,类,注解,接口四种。
enum ElementKind {
PACKAGE,
ENUM,
CLASS, //类
ANNOTATION_TYPE,
INTERFACE, //接口
ENUM_CONSTANT,
FIELD, //普通的成员变量
PARAMETER, //函数或构造函数的参数
LOCAL_VARIABLE,//本地变量
EXCEPTION_PARAMETER,
METHOD,//函数
CONSTRUCTOR,//构造函数
STATIC_INIT,
INSTANCE_INIT,
OTHER,
RESOURCE_VARIABLE;
}
enum TypeKind {
BOOLEAN,
BYTE,
SHORT,
INT,
LONG,
CHAR,
FLOAT,
DOUBLE,
VOID,
NONE,
NULL,
DECLARED,
ERROR,
TYPEVAR,
WILDCARD,
PACKAGE,
EXECUTABLE,
OTHER,
UNION,
INTERSECTION;
}
4.2.3 Element子类
Element在逻辑上代表语言元素,比如包,类,方法等,因此也会有五个直接子接口,它们分别代表一种特定类型的元素
Tables | Are |
---|---|
PackageElement | 一个包程序元素 |
TypeElement | 一个类或接口程序元素 |
ExecutableElement | 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素 |
TypeParameterElement | 一般类、接口、方法或构造方法元素的泛型参数 |
VariableElement | 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 |
其实也正好对应了注解的@Target的作用域(详见 (2.1.19)注释与注解):
- packages:ElementType.PACKAGE
- types(类、接口、枚举、Annotation类型):ElementType.TYPE
- 类型成员(方法、构造方法、成员变量、枚举值):ElementType.METHOD
- 方法参数和本地变量(如循环变量、catch参数):ElementType.PARAMETER,ElementType.FIELD,ElementType.LOCAL_VARIABLE
五个子类各有各的用处并且有各种独立的方法,在使用的时候可以强制将Element对象转换成其中的任一一种,但是前提是满足条件的转换,不然会抛出异常
4.2.3.1 TypeElement详解
TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象
方法 | 解释 |
---|---|
NestingKind getNestingKind(); | 返回此类型元素的嵌套种类 |
Name getQualifiedName(); | 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.譬如 Activity就是包名+类名 |
TypeMirror getSuperclass(); | 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType |
List getInterfaces(); | 返回直接由此类实现或直接由此接口扩展的接口类型 |
List getTypeParameters(); | 按照声明顺序返回此类型元素的形式类型参数 |
4.2.3.2 VariableElement详解
VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
方法 | 解释 |
---|---|
getConstantValue | 变量初始化的值 |
getEnclosingElement | 获取相关类信息 |
4.3 源码中移动
比如一个类里面有方法,成员变量等,这个类相对于方法跟成员变量就是外层元素,而成员变量和方法相对于类就是内层元素。
Element是代表源码的类,源码中移动到其他位置必须是用Element,比如移动到当前元素的外层元素TypeElement
public static TypeElement findEnclosingTypeElement( Element e ){
while( e != null && !(e instanceof TypeElement) ) {
e = e.getEnclosingElement();//通过getEnclosingElement()获取外层元素
}
return TypeElement.class.cast( e );
}
也可以使用getEnclosedElements()获取当前Element内层的所有元素
4.4 类继承树结构中移动
TypeMirror是用来反应类本身信息的类,在继承树移动必须用到TypeMirror,比如查找某个类的父类
Types typeUtils = processingEnv.getTypeUtils();
while (!typeElement.toString().equals(Object.class.getName())) {
TypeMirror typeMirror=element.getSuperclass();
element = (TypeElement)typeUtils.asElement(typeMirror);
}
4.5 处理MirroredTypeException
编译器使用注解处理器来处理目标代码时候,目标源码还没有编译成字节码,所以任何指向目标源码字节码的代码都会发生MirroredTypeException。最常见的例子见下面
//已经编译的三方jar:
@Retention(RetentionPolicy.SOURCE)
@interface What{
Class<?> value()
}
//源码:
@What(A.class)
public class A{}
//注解处理器
public ProcessorA extends AbstractProcessor{
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
```
What what=typeElementA.getAnnotation(What.class);
Class<?> clazz=what.value(); //这里将有MirroredTypeException
```
}
}
注解处理器之所有MirroredTypeException,是因为此时类A还没有被编译成字节码,所以A.class不存在,解决这个问题需要异常,利用异常我能获取类A的名字等信息,但是却得不到A.class,如下
public ProcessorA extends AbstractProcessor{
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
```
What what=typeElementA.getAnnotation(What.class);
try {
Class<?> clazz=what.value(); //这里将有MirroredTypeException
}catch(MirroredTypeException mte){
TypeMirror classTypeMirror = mte.getTypeMirror();
TypeElement classTypeElement = (TypeElement)Types.asElement(classTypeMirro);
//获取canonicalName
String canonicalName= classTypeElement.getQualifiedName().toString();
//获取simple name
String simpleName = classTypeElement.getSimpleName().toString();
}
// ```
}
}
五、示例
5.1 打印Element信息
以上的部分是不是挺懵逼?~~ 赶紧来个简单示例吧
//注解
// SOURCE也能在编译期获得,用于打印信息等
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {
}
//注解处理器
@SupportedAnnotationTypes({"com.avenwu.annotation.PrintMe"})
public class MyProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {//annotations当前处理器支持的所有注解,env全局源代码环境
Messager messager = processingEnv.getMessager();
for (TypeElement te : annotations) {//遍历 “当前处理器支持的所有注解”
for (Element e : env.getElementsAnnotatedWith(te)) {//获得被该注解声明的元素
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString()); //Gradle Console的打印
}
}
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
//注解使用
@PrintMe
public class MainActivity extends Activity{
//```
@PrintMe
public void showDrawerFrame(```){
//```
}
}
Gradle Console的输出为:
5.2 生成json描述文件
定义Seriable 注解,
- 如果类上声明注解我们就将其所有的变量都生成一个json描述文件
- 如果仅仅是成员变量呢,那我们只提取声明的成员变量来动态生成
//目标1
public class User
{
@Seriable
private String username;
@Seriable
private String password;
private String three;
private String four;
}
//目标二
@Seriable
public class Article
{
private String title ;
private String content ;
}
//预期结果1:com_zhy_User文件
{class:"com.zhy.User",
fields:
{
username:"java.lang.String",
password:"java.lang.String"
}
}
//预期结果2:com_zhy_Article文件
{class:"com.zhy.Article",
fields:
{
content:"java.lang.String",
title:"java.lang.String"
}
}
基本分为两个过程:
1. 找出标识注解的类或成员变量,封装到maps中;
2. 遍历maps为每个类创建json文件。
- 我们把文件输出到了f://apt_test文件夹中,如果你没有f盘神马的,自行修改目录
@SupportedAnnotationTypes("com.zhy.annotationprocess.annotation.Seriable")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class BeanProcessor extends AbstractProcessor
{ // 元素操作的辅助类
Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
// 元素操作的辅助类
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv)
{
// 【1】获得被该注解声明的所有元素
Set<? extends Element> elememts = roundEnv
.getElementsAnnotatedWith(Seriable.class);
TypeElement classElement = null;// 声明类元素
List<VariableElement> fields = null;// 声明一个存放成员变量的列表
// 存放二者 类名:成员变量元素
Map<String, List<VariableElement>> maps = new HashMap<String, List<VariableElement>>();
// 【2】遍历 ,找出标识注解的类或成员变量,封装到maps中;
for (Element ele : elememts)
{
// 【2.1】判断该元素是否为类
if (ele.getKind() == ElementKind.CLASS)
{
classElement = (TypeElement) ele;
maps.put(classElement.getQualifiedName().toString(),
fields = new ArrayList<VariableElement>()); //向maps中加入一个映射对 类名:变量元素的空容器
} else if (ele.getKind() == ElementKind.FIELD) // 【2.2】判断该元素是否为成员变量
{
VariableElement varELe = (VariableElement) ele;
// 获取该元素封装类型,即外层元素
TypeElement enclosingElement = (TypeElement) varELe
.getEnclosingElement(); //【2.2.1】获取最近的外层类,其实就是Class对象
// 拿到key
String key = enclosingElement.getQualifiedName().toString();
fields = maps.get(key);
if (fields == null)
{
maps.put(key, fields = new ArrayList<VariableElement>());
}
fields.add(varELe); //【2.2.2】 向map的key=Class对象名所对应的Map中加入当前变量
}
}
//【2.3】 遍历Maps,如果key所对应的容器容量为0,则证明是Seriable注解被加入到了类上,应该全类记录
for (String key : maps.keySet())
{
if (maps.get(key).size() == 0)
{
TypeElement typeElement = elementUtils.getTypeElement(key); //根据 类名获取到目标 类 元素
List<? extends Element> allMembers = elementUtils
.getAllMembers(typeElement); //获取全部的 成员变量+成员函数 元素
if (allMembers.size() > 0)
{
maps.get(key).addAll(ElementFilter.fieldsIn(allMembers)); //过滤成员变量元素,并加入到对应的list中
}
}
}
//【3】遍历maps为每个类创建json文件
generateCodes(maps);
return true;
}
private void generateCodes(Map<String, List<VariableElement>> maps)
{
File dir = new File("f://apt_test");
if (!dir.exists())
dir.mkdirs();
// 遍历map
for (String key : maps.keySet())
{
// 创建文件
File file = new File(dir, key.replaceAll("\\.", "_") + ".txt");
try
{
/**
* 编写json文件内容
*/
FileWriter fw = new FileWriter(file);
fw.append("{").append("class:").append("\"" + key + "\"")
.append(",\n ");
fw.append("fields:\n {\n");
List<VariableElement> fields = maps.get(key);
for (int i = 0; i < fields.size(); i++)
{
VariableElement field = fields.get(i);
fw.append(" ").append(field.getSimpleName()).append(":")
.append("\"" + field.asType().toString() + "\"");
if (i < fields.size() - 1)
{
fw.append(",");
fw.append("\n");
}
}
fw.append("\n }\n");
fw.append("}");
fw.flush();
fw.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
}
5.3 butterknife的 @BindView
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_msg)
TextView tvMsg;
@BindView(R.id.tv_other)
TextView tvOther;
}
- 先判断Element类型,如果是VariableElement则继续获取相关的包名(这里必须在app包名一致,不然获取不到android类)类对象信息,以及@BindView注解修饰的参数数据;
- 最后将所有需要的数据通过javapoet和Filer自动编译创建一个java文件
最终输出:
package com.wzgiceman.viewinjector;
import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;
public class MainActivityViewInjector implements ViewInjector<MainActivity> {
@Override
public void inject(MainActivity host, Object object) {
if(object instanceof android.app.Activity){
host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
}
else{
host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
}
}
}
处理器代码:
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
//一、收集信息
for (Element element : elements) {
/*检查类型*/
if (!(element instanceof VariableElement)) {//BindView仅会作用在成员变量上,也可以使用ele.getKind() == ElementKind.FIELD判断
return false;
}
VariableElement variableElement = (VariableElement) element;
/*获取类信息*/
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
/*类的绝对路径*/
String qualifiedName = typeElement.getQualifiedName().toString();
/*类名*/
String clsName = typeElement.getSimpleName().toString();
/*获取包名*/
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
/*获取注解*/
BindView annotation = variableElement.getAnnotation(BindView.class);
int id = annotation.value();
/*参数名*/
String name = variableElement.getSimpleName().toString();
/*参数对象类*/
String type = variableElement.asType().toString();
ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector");
ClassName host = ClassName.bestGuess(qualifiedName);
MethodSpec main = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addAnnotation(Override.class)
.addParameter(host, "host")
.addParameter(Object.class, "object")
.addCode(""
+ " if(object instanceof android.app.Activity){\n"
+ " host." + name + " = (" + type + ")(((android.app.Activity)object).findViewById(" + id + "));\n"
+ " }\n"
+ "else{\n"
+ " host." + name + " = (" + type + ")(((android.view.View)object).findViewById(" + id + "));\n"
+ "}\n")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(main)
.addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
.build();
try {
// 生成 com.example.HelloWorld.java
JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
需要注意的是,上述代码中,再一次遍历后就进行了IO操作,这也就导致了只初始化了tvMsg对象,tvOther并没有初始化
所以常规的做法应该是,统一扫描,统一处理,尤其注意重复数据的剔除
六、注册你的处理器
在自定义完注解处理器后,必须将它注册到javac中,才会在编译期间调用处理器对注解进行处理。
6.1 Android studio
1.注解器模块配置如下
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
dependencies {
compile 'com.google.auto.service:auto-service:1.0-rc2'
}
2.android studio会自动生成为javax.annotation.processing.Processor,只需为注解器添加@AutoService注解,这样就可以自动生成META-INF内的文件,如下
@AutoService(Processor.class)
public class ProcessorA extends AbstractProcessor{
```
}
- 工程的build.gradle:
dependencies {
// 其他classpath
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' //添加apt插件
}
- 调用模块的build.gradle:
applying plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' //使用apt插件
dependencies {
apt '注解处理器.jar' //注解处理器
compile '关于处理器生成文件的调用包.jar' //API包
}
或者
直接使用annotationProcessor就可以标示注解处理器
annotationProcessor project(':arouter-compiler')
// annotationProcessor 'com.alibaba:arouter-compiler:1.0.3'
6.2 eclipse
- 你必须提供一个.jar文件。就像其他.jar文件一样,你打包你的注解处理器到此文件中
- 在你的jar中,你需要打包一个特定的文件”javax.annotation.processing.Processor”到META-INF/services路径下
- 打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是,注解处理器的合法的全名列表,每一个元素换行分割
- 把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器
所以,你的.jar文件看起来就像下面这样:
MyProcessor.jar
com
example
MyProcessor.class
META-INF
services
javax.annotation.processing.Processor
其中内容为
com.example.MyProcessor
com.foo.OtherProcessor
net.blabla.SpecialProcessor
七、其他
7.1 编译期的日志打印
注解处理器的打印日志全部应该交由Messager处理,并且需要在打印错误时利用异常抛出来终止处理器
Messager messager=processingEnv.getMessager()
//打印非error级别log
messager.printMessage(Kind.NOTE,message);
//打印error时抛出异常来中断注解处理器
messager.printMessage(Kind.ERROR,error);
throw new RuntimeException(error);
7.2 javapoet代码生成辅助工具库
JavaPoet一个是创建 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,GitHub上面有非常详细的用法,建议好好阅读相关的使用
processors依赖:
compile 'com.squareup:javapoet:1.8.0'
来看一个简单示例:
// 创建main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, clsNmae+"-"+msg)
.build();
// 创建HelloWorld类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.wzgiceman.viewinjector.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.wzgiceman.viewinjector", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
运行后,在build文件夹的class文件夹下可以看到:
【其他2】
7.3 android-apt编译期文件冲突解决
编译时注解的自动写入也会导致代码混乱,可能在多次build编译过程中出现文件冲突的情况,所以这里需要引入android-apt
android-apt能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio项目的对应目录中存放注解处理器在编译期间生成的文件
依赖使用:
//根目录build.gradle
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
//app中
apply plugin: 'com.neenbedankt.android-apt'
apt project(':processors') //这里是apt替换compile依赖processors
7.4 代码结构
编译时注解越来越多的出现在各大开源框架使用中,比如
- JakeWharton/butterknife view
- greenrobot/EventBus 事件
- square/dagger 依赖注入
编译时注解框架在编写时有相对固定的格式,分包为例
【其他1】
其中除了app是Android moudel以外,其他全部均是Java moudel
- 依赖关系
- annotations 无
- processors 依赖 api || annotations
- app依赖有 api && annotations && processors
参考文献
上一篇: ThinkPHP的常用配置选项汇总
下一篇: Spark与Flink:对比与分析