一小时搞明白注解处理器(Annotation Processor Tool)

本文介绍了如何使用Java注解处理器自动生成绑定控件的代码,包括注解处理器的基本原理、编写流程及实战案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java中的注解是个很神奇的东西,还不了解的可以看下一小时搞明白自定义注解(Annotation)。现在很多Android的库都用使用注解实现的,比如ButterKnife,我们不防也来学习一下,学完注解处理器,我们尝试写一个简单的类似ButterKnife的东西来绑定控件。


什么是注解处理器?

        注解处理器是(Annotation Processor)是javac的一个工具,用来在编译时扫描和编译和处理注解(Annotation)。你可以自己定义注解和注解处理器去搞一些事情。一个注解处理器它以Java代码或者(编译过的字节码)作为输入,生成文件(通常是java文件)。这些生成的java文件不能修改,并且会同其手动编写的java代码一样会被javac编译。看到这里加上之前理解,应该明白大概的过程了,就是把标记了注解的类,变量等作为输入内容,经过注解处理器处理,生成想要生成的java代码。


处理器AbstractProcessor

        处理器的写法有固定的套路,继承AbstractProcessor。如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class MyProcessor extends AbstractProcessor {  
  2.   
  3.     @Override  
  4.     public synchronized void init(ProcessingEnvironment processingEnv) {  
  5.         super.init(processingEnv);  
  6.     }  
  7.   
  8.     @Override  
  9.     public Set<String> getSupportedAnnotationTypes() {  
  10.         return null;  
  11.     }  
  12.   
  13.     @Override  
  14.     public SourceVersion getSupportedSourceVersion() {  
  15.         return SourceVersion.latestSupported();  
  16.     }  
  17.   
  18.     @Override  
  19.     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
  20.         return true;  
  21.     }  
  22. }  

  • init(ProcessingEnvironment processingEnv) 被注解处理工具调用,参数ProcessingEnvironment 提供了Element,Filer,Messager等工具
  • getSupportedAnnotationTypes() 指定注解处理器是注册给那一个注解的,它是一个字符串的集合,意味着可以支持多个类型的注解,并且字符串是合法全名。
  • getSupportedSourceVersion 指定Java版本
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 这个也是最主要的,在这里扫描和处理你的注解并生成Java代码,信息都在参数RoundEnvironment 里了,后面会介绍。
在Java7 中还可以使用

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @SupportedSourceVersion(SourceVersion.latestSupported())  
  2. @SupportedAnnotationTypes({  
  3.    // 合法注解全名的集合  
  4.  })  
代替  getSupportedSourceVersion() 和 getSupportedAnnotationType() ,没毛病,还可以在注解处理离器中使用注解。


注册注解处理器

打包注解处理器的时候需要一个特殊的文件 javax.annotation.processing.Processor 在 META-INF/services 路径下

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. --myprcessor.jar  
  2. ----com  
  3. ------example  
  4. --------MyProcessor.class  
  5. ----META-INF  
  6. ------services  
  7. --------javax.annotation.processing.Processor  

打包进javax.annotation.processing.Processor的内容是处理器的合法全称,多个处理器之间换行。

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. com.example.myprocess.MyProcessorA  
  2. com.example.myprocess.MyProcessorB  

google提供了一个注册处理器的库

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. compile 'com.google.auto.service:auto-service:1.0-rc2'  

一个注解搞定:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @AutoService(Processor.class)  
  2. public class MyProcessor extends AbstractProcessor {  
  3.       ...  
  4. }  

读到这里ButterKnife用到的知识点我们都已经了解了

1.自定义注解

2.用注解处理器解析注解

3.解析完成后生成Java文件

BufferKnife使用:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     @Bind(R.id.rxjava_demo)  
  4.     Button mRxJavaDemo;  
  5.   
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.activity_main);  
  10.         ButterKnife.bind(this);  
  11.         mRxJavaDemo.setText("Text");  
  12.     }  
  13.   
  14. }  
然后我们编译一下,打开路径:/app/build/intermediates/classes/release/com/ming/rxdemo/MainActivity$$ViewBinder.class

这就是我们生成的Java文件,可以看到Button已经在bind里面初始化了。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {  
  2.     public MainActivity$$ViewBinder() {  
  3.     }  
  4.   
  5.     public void bind(Finder finder, T target, Object source) {  
  6.         View view = (View)finder.findRequiredView(source, 2131492944"field \'mRxJavaDemo\'");  
  7.         target.mRxJavaDemo = (Button)finder.castView(view, 2131492944"field \'mRxJavaDemo\'");  
  8.     }  
  9.   
  10.     public void unbind(T target) {  
  11.         target.mRxJavaDemo = null;  
  12.     }  
  13. }  

接下来我们创建一个项目,写一个简单的用注解绑定控件的例子

项目结构

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. --apt-demo  
  2. ----bindview-annotation(Java Library)  
  3. ----bindview-api(Android Library)  
  4. ----bindview-compiler(Java Library)  
  5. ----app(Android App)  
  • bindview-annotation 注解声明
  • bindview-api 调用Android SDK API
  • bindview-compiler 注解处理器相关
  • app 测试App

1.在 bindview-annotation 下创建一个@BindView注解,该注解返回一个值,整型,名字为value,用来表示控件ID。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @Target(ElementType.FIELD)  
  2. @Retention(RetentionPolicy.CLASS)  
  3. public @interface BindView {  
  4.     /** 
  5.      * 用来装id 
  6.      * 
  7.      * @return 
  8.      */  
  9.     int value();  
  10. }  

2.在 bindview-compiler 中创建注解处理器 BindViewProcessor 并注册,做基本的初始化工作。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @AutoService(Processor.class)  
  2. public class BindViewProcessor extends AbstractProcessor {  
  3.     /** 
  4.      * 文件相关的辅助类 
  5.      */  
  6.     private Filer mFiler;  
  7.     /** 
  8.      * 元素相关的辅助类 
  9.      */  
  10.     private Elements mElementUtils;  
  11.     /** 
  12.      * 日志相关的辅助类 
  13.      */  
  14.     private Messager mMessager;  
  15.     /** 
  16.      * 解析的目标注解集合 
  17.      */  
  18.     private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();  
  19.   
  20.     @Override  
  21.     public synchronized void init(ProcessingEnvironment processingEnv) {  
  22.         super.init(processingEnv);  
  23.         mElementUtils = processingEnv.getElementUtils();  
  24.         mMessager = processingEnv.getMessager();  
  25.         mFiler = processingEnv.getFiler();  
  26.     }  
  27.   
  28.     @Override  
  29.     public Set<String> getSupportedAnnotationTypes() {  
  30.         Set<String> types = new LinkedHashSet<>();  
  31.         types.add(BindView.class.getCanonicalName());//返回该注解处理器支持的注解集合  
  32.         return types;  
  33.     }  
  34.   
  35.     @Override  
  36.     public SourceVersion getSupportedSourceVersion() {  
  37.         return SourceVersion.latestSupported();  
  38.     }  
  39.   
  40.     @Override  
  41.     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
  42.         return true;  
  43.     }  
  44. }  

是不是注意到了里面有个Map容器,而且类型是AnnotatedClass,这是干啥的呢?这个很好理解,我们在解析XML,解析Json的时候数据解析完之后是不是要以对象的形式表示出来,这里也一样,@BindView用来标记类成员,一个类下可以有多个成员,好比一个Activity中可以有多个控件,一个容器下有多个控件等。如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.mingwei.myprocess.model;  
  2.   
  3. import com.mingwei.myprocess.TypeUtil;  
  4. import com.squareup.javapoet.ClassName;  
  5. import com.squareup.javapoet.JavaFile;  
  6. import com.squareup.javapoet.MethodSpec;  
  7. import com.squareup.javapoet.ParameterizedTypeName;  
  8. import com.squareup.javapoet.TypeName;  
  9. import com.squareup.javapoet.TypeSpec;  
  10.   
  11. import java.util.ArrayList;  
  12. import java.util.List;  
  13.   
  14. import javax.lang.model.element.Modifier;  
  15. import javax.lang.model.element.TypeElement;  
  16. import javax.lang.model.util.Elements;  
  17.   
  18. /** 
  19.  * Created by mingwei on 12/10/16. 
  20.  * 优快云:    http://blog.youkuaiyun.com/u013045971 
  21.  * Github:  https://github.com/gumingwei 
  22.  */  
  23. public class AnnotatedClass {  
  24.     /** 
  25.      * 类名 
  26.      */  
  27.     public TypeElement mClassElement;  
  28.     /** 
  29.      * 成员变量集合 
  30.      */  
  31.     public List<BindViewField> mFiled;  
  32.     /** 
  33.      * 元素辅助类 
  34.      */  
  35.     public Elements mElementUtils;  
  36.   
  37.     public AnnotatedClass(TypeElement classElement, Elements elementUtils) {  
  38.         this.mClassElement = classElement;  
  39.         this.mElementUtils = elementUtils;  
  40.         this.mFiled = new ArrayList<>();  
  41.     }  
  42.     /** 
  43.      * 获取当前这个类的全名 
  44.      */  
  45.     public String getFullClassName() {  
  46.         return mClassElement.getQualifiedName().toString();  
  47.     }  
  48.     /** 
  49.      * 添加一个成员 
  50.      */  
  51.     public void addField(BindViewField field) {  
  52.         mFiled.add(field);  
  53.     }  
  54.     /** 
  55.      * 输出Java 
  56.      */  
  57.     public JavaFile generateFinder() {  
  58.         return null;  
  59.     }  
  60.     /** 
  61.      * 包名 
  62.      */  
  63.     public String getPackageName(TypeElement type) {  
  64.         return mElementUtils.getPackageOf(type).getQualifiedName().toString();  
  65.     }  
  66.     /** 
  67.      * 类名 
  68.      */  
  69.     private static String getClassName(TypeElement type, String packageName) {  
  70.         int packageLen = packageName.length() + 1;  
  71.         return type.getQualifiedName().toString().substring(packageLen).replace('.''$');  
  72.     }  
  73. }  
成员用BindViewField表示,没什么复杂的逻辑,在构造函数判断类型和初始化,简单的get函数

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.mingwei.myprocess.model;  
  2.   
  3. import com.mingwe.myanno.BindView;  
  4.   
  5. import javax.lang.model.element.Element;  
  6. import javax.lang.model.element.ElementKind;  
  7. import javax.lang.model.element.Name;  
  8. import javax.lang.model.element.VariableElement;  
  9. import javax.lang.model.type.TypeMirror;  
  10.   
  11. /** 
  12.  * Created by mingwei on 12/10/16. 
  13.  * 优快云:    http://blog.youkuaiyun.com/u013045971 
  14.  * Github:  https://github.com/gumingwei 
  15.  * 被BindView注解标记的字段的模型类 
  16.  */  
  17. public class BindViewField {  
  18.   
  19.     private VariableElement mFieldElement;  
  20.   
  21.     private int mResId;  
  22.   
  23.     public BindViewField(Element element) throws IllegalArgumentException {  
  24.         if (element.getKind() != ElementKind.FIELD) {//判断是否是类成员  
  25.             throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",  
  26.                     BindView.class.getSimpleName()));  
  27.         }  
  28.         mFieldElement = (VariableElement) element;  
  29.         //获取注解和值  
  30.         BindView bindView = mFieldElement.getAnnotation(BindView.class);  
  31.         mResId = bindView.value();  
  32.         if (mResId < 0) {  
  33.             throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",  
  34.                     BindView.class.getSimpleName(), mFieldElement.getSimpleName()));  
  35.         }  
  36.     }  
  37.   
  38.     public Name getFieldName() {  
  39.         return mFieldElement.getSimpleName();  
  40.     }  
  41.   
  42.     public int getResId() {  
  43.         return mResId;  
  44.     }  
  45.   
  46.     public TypeMirror getFieldType() {  
  47.         return mFieldElement.asType();  
  48.     }  
  49. }  

这里看到了很多的Element,在Xml解析时候就有Element这个概念。在Java源文件中同样有Element概念:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.example;        // PackageElement  
  2.   
  3. public class MyClass {      // TypeElement  
  4.   
  5.     private int a;          // VariableElement  
  6.   
  7.     private Foo other;      // VariableElement  
  8.   
  9.     public Foo () {}        // ExecuteableElement  
  10.   
  11.     public void setA (      // ExecuteableElement  
  12.                 int newA    // TypeElement  
  13.                 ) {  
  14.   
  15.     }  
  16. }  

接下来就是在处理器的process中解析注解了

每次解析前都要清空,因为process方法可能不止走一次。

拿到注解模型之后遍历调用生成Java代码

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
  3.         mAnnotatedClassMap.clear();  
  4.         try {  
  5.             processBindView(roundEnv);  
  6.         } catch (IllegalArgumentException e) {  
  7.             error(e.getMessage());  
  8.             return true;  
  9.         }  
  10.   
  11.         try {  
  12.             for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {  
  13.                 info("generating file for %s", annotatedClass.getFullClassName());  
  14.                 annotatedClass.generateFinder().writeTo(mFiler);  
  15.             }  
  16.         } catch (Exception e) {  
  17.             e.printStackTrace();  
  18.             error("Generate file failed,reason:%s", e.getMessage());  
  19.         }  
  20.         return true;  
  21.     }  
processBindView 和 getAnnotatedClass

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * 遍历目标RoundEnviroment 
  3.  * @param roundEnv 
  4.  */  
  5. private void processBindView(RoundEnvironment roundEnv) {  
  6.     for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {  
  7.         AnnotatedClass annotatedClass = getAnnotatedClass(element);  
  8.         BindViewField field = new BindViewField(element);  
  9.         annotatedClass.addField(field);  
  10.     }  
  11. }  
  12. /** 
  13.  * 如果在map中存在就直接用,不存在就new出来放在map里 
  14.  * @param element 
  15.  */  
  16. private AnnotatedClass getAnnotatedClass(Element element) {  
  17.     TypeElement encloseElement = (TypeElement) element.getEnclosingElement();  
  18.     String fullClassName = encloseElement.getQualifiedName().toString();  
  19.     AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);  
  20.     if (annotatedClass == null) {  
  21.         annotatedClass = new AnnotatedClass(encloseElement, mElementUtils);  
  22.         mAnnotatedClassMap.put(fullClassName, annotatedClass);  
  23.     }  
  24.     return annotatedClass;  
  25. }  

3.在生成Java之前 我们要在bindview-api 中创建一些类,配合 bindview-compiler 一起使用。

你在使用Butterknife的时候不是要在onCreate里掉用一下BindView.bind(this)吗,那这个玩意是干什么呢。试想一下,前面做的一大堆工作是为了生成自动绑定控件的Java代码,如果生成的Java代码不能和你要使用的地方关联起来,那也是没有用的,可以把BindView.bind(this)理解为调用了你生成的Java代码,而生成了代码中完成了一些控件的初始化工作,自然你的控件就变得可用了。

接口:Finder 定义findView方法

实现类:ActivityFinder Activity中使用,ViewFinder View中使用

接口:Injector inject方法将来是要创建在生成的Java文件中,用该方法中传递过来的参数进行控件的初始化。

辅助类:ViewInjector 调用和传递参数

这个代码我就不贴了,就一点点内容,一看就明白了。

4.在AnnotatedClass中生成Java代码

生成代码使用了一个很好用的库 Javapoet 。类,方法,都可以使用构建器构建出来,很好上手,再也不用拼接字符串了。哈哈哈哈~

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public JavaFile generateFinder() {  
  2.         //构建方法  
  3.         MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")  
  4.                 .addModifiers(Modifier.PUBLIC)//添加描述  
  5.                 .addAnnotation(Override.class)//添加注解  
  6.                 .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)//添加参数  
  7.                 .addParameter(TypeName.OBJECT, "source")//添加参数  
  8.                 .addParameter(TypeUtil.FINDER, "finder");//添加参数  
  9.   
  10.         for (BindViewField field : mFiled) {  
  11.             //添加一行  
  12.             injectMethodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()  
  13.                     , ClassName.get(field.getFieldType()), field.getResId());  
  14.         }  
  15.   
  16.         String packageName = getPackageName(mClassElement);  
  17.         String className = getClassName(mClassElement, packageName);  
  18.         ClassName bindClassName = ClassName.get(packageName, className);  
  19.         //构建类  
  20.         TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")//类名  
  21.                 .addModifiers(Modifier.PUBLIC)//添加描述  
  22.                 .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))//添加接口(类/接口,范型)  
  23.                 .addMethod(injectMethodBuilder.build())//添加方法  
  24.                 .build();  
  25.   
  26.         return JavaFile.builder(packageName, finderClass).build();  
  27.     }  
  28.   
  29.     public String getPackageName(TypeElement type) {  
  30.         return mElementUtils.getPackageOf(type).getQualifiedName().toString();  
  31.     }  
  32.   
  33.     private static String getClassName(TypeElement type, String packageName) {  
  34.         int packageLen = packageName.length() + 1;  
  35.         return type.getQualifiedName().toString().substring(packageLen).replace('.''$');  
  36.     }  

可以在代码里System.out调试注解处理器的代码。

还要注意的一点,项目之间的相互引用。

bindview-complier 引用 bindview-annotation

app 引用了剩下的三个module,在引用 bindview-complier 的时候用的apt的方式

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. apt project(':bindview-compiler')  

就写到这里吧,Demo 放在 Github上了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值