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

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


什么是注解处理器?

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


处理器AbstractProcessor

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

  1. public class MyProcessor extends AbstractProcessor {
  2. @Override
  3. public synchronized void init(ProcessingEnvironment processingEnv) {
  4. super.init(processingEnv);
  5. }
  6. @Override
  7. public Set<String> getSupportedAnnotationTypes() {
  8. return null;
  9. }
  10. @Override
  11. public SourceVersion getSupportedSourceVersion() {
  12. return SourceVersion.latestSupported();
  13. }
  14. @Override
  15. public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
  16. return true;
  17. }
  18. }

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

  1. @SupportedSourceVersion(SourceVersion.latestSupported())
  2. @SupportedAnnotationTypes({
  3. // 合法注解全名的集合
  4. })
代替  getSupportedSourceVersion() 和 getSupportedAnnotationType() ,没毛病,还可以在注解处理离器中使用注解。


注册注解处理器

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

  1. -- myprcessor .jar
  2. ---- com
  3. ------ example
  4. -------- MyProcessor .class
  5. ---- META-INF
  6. ------ services
  7. -------- javax .annotation .processing .Processor

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

  1. com .example .myprocess .MyProcessorA
  2. com .example .myprocess .MyProcessorB

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

compile 'com.google.auto.service:auto-service:1.0-rc2'

一个注解搞定:

  1. @AutoService(Processor.class)
  2. public class MyProcessor extends AbstractProcessor {
  3. ...
  4. }

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

1.自定义注解

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

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

BufferKnife使用:

  1. public class MainActivity extends AppCompatActivity {
  2. @Bind(R.id.rxjava_demo)
  3. Button mRxJavaDemo;
  4. @Override
  5. protected void onCreate(Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. setContentView(R.layout.activity_main);
  8. ButterKnife.bind( this);
  9. mRxJavaDemo.setText( "Text");
  10. }
  11. }
然后我们编译一下,打开路径:/app/build/intermediates/classes/release/com/ming/rxdemo/MainActivity$$ViewBinder.class

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

  1. public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
  2. public MainActivity$$ViewBinder() {
  3. }
  4. public void bind(Finder finder, T target, Object source) {
  5. View view = (View)finder.findRequiredView(source, 2131492944, "field \'mRxJavaDemo\'");
  6. target.mRxJavaDemo = (Button)finder.castView(view, 2131492944, "field \'mRxJavaDemo\'");
  7. }
  8. public void unbind(T target) {
  9. target.mRxJavaDemo = null;
  10. }
  11. }

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

项目结构

  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。

  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 并注册,做基本的初始化工作。

  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. @Override
  20. public synchronized void init(ProcessingEnvironment processingEnv) {
  21. super.init(processingEnv);
  22. mElementUtils = processingEnv.getElementUtils();
  23. mMessager = processingEnv.getMessager();
  24. mFiler = processingEnv.getFiler();
  25. }
  26. @Override
  27. public Set<String> getSupportedAnnotationTypes() {
  28. Set<String> types = new LinkedHashSet<>();
  29. types.add(BindView.class.getCanonicalName()); //返回该注解处理器支持的注解集合
  30. return types;
  31. }
  32. @Override
  33. public SourceVersion getSupportedSourceVersion() {
  34. return SourceVersion.latestSupported();
  35. }
  36. @Override
  37. public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
  38. return true;
  39. }
  40. }

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

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

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

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

  1. package com.example; // PackageElement
  2. public class MyClass { // TypeElement
  3. private int a; // VariableElement
  4. private Foo other; // VariableElement
  5. public Foo () {} // ExecuteableElement
  6. public void setA ( // ExecuteableElement
  7. int newA // TypeElement
  8. ) {
  9. }
  10. }

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

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

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

  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. try {
  11. for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
  12. info( "generating file for %s", annotatedClass.getFullClassName());
  13. annotatedClass.generateFinder().writeTo(mFiler);
  14. }
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. error( "Generate file failed,reason:%s", e.getMessage());
  18. }
  19. return true;
  20. }
processBindView 和 getAnnotatedClass

  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 。类,方法,都可以使用构建器构建出来,很好上手,再也不用拼接字符串了。哈哈哈哈~

  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. for (BindViewField field : mFiled) {
  10. //添加一行
  11. injectMethodBuilder.addStatement( "host.$N=($T)finder.findView(source,$L)", field.getFieldName()
  12. , ClassName.get(field.getFieldType()), field.getResId());
  13. }
  14. String packageName = getPackageName(mClassElement);
  15. String className = getClassName(mClassElement, packageName);
  16. ClassName bindClassName = ClassName.get(packageName, className);
  17. //构建类
  18. TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector") //类名
  19. .addModifiers(Modifier.PUBLIC) //添加描述
  20. .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType()))) //添加接口(类/接口,范型)
  21. .addMethod(injectMethodBuilder.build()) //添加方法
  22. .build();
  23. return JavaFile.builder(packageName, finderClass).build();
  24. }
  25. public String getPackageName(TypeElement type) {
  26. return mElementUtils.getPackageOf(type).getQualifiedName().toString();
  27. }
  28. private static String getClassName(TypeElement type, String packageName) {
  29. int packageLen = packageName.length() + 1;
  30. return type.getQualifiedName().toString().substring(packageLen).replace( '.', '$');
  31. }

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

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

bindview-complier 引用 bindview-annotation

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

apt project(':bindview-compiler')

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值