Android注解,这几篇文章就够了(三)自己写个注解处理器

一 前言

前面两篇文章,注解处理器理解注解,对注解有了一个初步认识,第二篇文章末尾也提到了,注解不是代码的一部分,当开发者使用了Annotation注解以后,注解不会自己起作用,必须提供相应的代码来处理这些信息。
这篇文章,我们就写一个简单的注解处理器,作用是类似于ButterKnife查找id。
源码传送门

二 项目结构

整个项目采用如下所示的结构:


    1. BindViewAnnotation,Java Library,存放我们定义的注解。
    1. bindviewapi,Android Library,声明注解框架使用的api,本例子中,我们要实现的是查找view控件,并将控件和xml中的绑定。
    1. BindViewCompiler,Java Library,注解处理器,根据注解生成处理打代码。

新建这几个Library的过程不再陈述,特别注意的是,建BindViewCompiler Java Library时,在build.gradle下要加入以下代码:

// 用于生成Java文件的库
    implementation 'com.squareup:javapoet:1.11.1'
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

这些代码后面会提及。

三 正式开始吧

(一)在BindViewAnnotation新建注解

前面说过了,我们要实现的功能是查找xml文件的id功能,注解歹意如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)

public @interface BindView {
    int value();
}

使用注解,在Activity中使用注解,在xml中定义一个button,

<Button
        android:id="@+id/btn_bind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="BindButton"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

在Activity中使用我们定义的注解。

@BindView(R.id.btn_bind)
 public Button mButton;

前面说了,注解不会自动起作用,如果我们直接运行代码,会直接报错的,提示Button没有定义,所以我们要写代码来处理这个注解的信息。

(二) 声明注解框架用到的api

① 定义一个绑定注解的接口
public interface IViewBind<T> {
    void bind(T t);
}
② 向外提供的绑定方法,这里使用静态方法来管理。
public class ViewBinder {

    public static void bind(Activity activity){

        try {

            // 1
            Class clazz=Class.forName(activity.getClass().getCanonicalName()+"$$ViewBinder");
           // 2
            IViewBind<Activity> iViewBinder= (IViewBind<Activity>) clazz.newInstance();
           // 3
            iViewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            Log.d("hbj--exp",e.getException()+"");
            Log.d("hbj--cause",e.getCause()+"");
            Log.d("hbj--mess",e.getMessage()+"");
            Log.d("hbj--class",e.getClass()+"");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

// 1 处获取生成的类的名字,生成类的规则代码在后面写,生成的规则是 类名$$ViewBinder,例如在MainActivity中需要使用,生成的文件名字就是MainActivity$$ViewBinder.java。
// 2 获取生成的类的实例。
// 3 完成绑定。
可能对第二条和第三条不是很好理解,现在贴出生成的java文件的源码,结合生成的文件,应该就好理解了吧。

public class MainActivity$$ViewBinder< T extends MainActivity> implements IViewBind<T> {
@Override
public void bind(T activity) {
activity.mButton=activity.findViewById(2131165250);
}
}

(三) 根据注解生成代码

现在只剩根据注解生成代码。
创建一个自定义的Annotation Processor,继承自AbstractProcessor。

  // 1
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

   // 2
    @Override
    public synchronized void init(ProcessingEnvironment env){
    }
  
   // 3
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment roundEnv) { }

    // 4
    @Override
    public Set<String> getSupportedAnnotationTypes() { 
    }

    // 5
    @Override
    public SourceVersion getSupportedSourceVersion() {
    }
}

// 1处 @AutoService(Processor.class),向javac注册我们自定义的注解处理器, 这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
AutoService主要是生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解的话,需要通过以下方法进行手动配置进行手动注册:
1.在main下创建resources目录,然后创建META-INF/services 目录,最后在此目录下创建文件:javax.annotation.processing.Processor,目录如下,



文件里的内容是一些列的自定义注解处理器完整有效的类名集合,以换行符切割,这里就自定义了一个注解处理器,


注:但是有可能会出现使用@AutoService()无法动态生成入口文件的,这个问题可以如下解决:

这个要从google auto service 和META_INF,谷歌的 auto service 也是一种 Annotation Processor,它能自动生成 META-INF 目录以及相关文件,避免手工创建该文件,手工创建的方法,上文有,手工创建有可能失误。使用 auto service 中的 @AutoService(Processor.class) 注解修饰 Annotation Processor 类就可以在编译过程中自动生成文件。

如果要进入的话,还要注意要引入两个配置:

   implementation 'com.google.auto.service:auto-service:1.0-rc6'
   annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

这两个重复写的原因是:annotationProcessor这个是新版 gradle 提供的 java plugin 内置的配置项,在gradle 5.+ 中将 Annotation Processor 从编译期 classpath 中去除了,javac 也就无法发现 Annotation Processor。此处如果按照 gradle 4.+ 的写法,只写一个 implementation 是无法使用 auto service 的 Annotation Processor 的。必须要使用 annotationProcessor 来配置 Annotation Processor 使其生效。

// 2处 init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。

// 3处 public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素。
// 4处 getSupportedAnnotationTypes(),这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。

// 5处 getSupportedSourceVersion();用来指定你使用的Java版本。
下面给出BindViewProcessor的完整代码:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

  // 文件相关辅助类
  private Filer mFiler;
  // 日志相关辅助类
  private Messager mMessager;


  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);

      mFiler = processingEnv.getFiler();
      mMessager = processingEnv.getMessager();
  }


  @Override
  public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
      Set<String> annotations=new LinkedHashSet<>();
      annotations.add(BindView.class.getCanonicalName());
      return annotations;
  }

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
      Map<TypeElement,ArrayList<BindViewInfo>> bindViewMap=new HashMap<>();
      for (Element element : elements) {
          // 因为BindView只作用于Filed,判断注解是否是属性,不是的话直接结束
          if (element.getKind() != ElementKind.FIELD) {
              mMessager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " is not filed,can not use @BindView");
              return false;
          }
          // 获取注解元数据
          int id = element.getAnnotation(BindView.class).value();
          // 获取属性的类
          TypeElement typeElement= (TypeElement) element.getEnclosingElement();

          if (!bindViewMap.containsKey(typeElement)){
              bindViewMap.put(typeElement,new ArrayList<BindViewInfo>());
          }
          ArrayList<BindViewInfo> bindViewInfos=bindViewMap.get(typeElement);
          // 添加list
          bindViewInfos.add(new BindViewInfo(id,element.getSimpleName().toString()));
      }
      produceClass(bindViewMap);
      return true;
  }


  private void produceClass(Map<TypeElement,ArrayList<BindViewInfo>> hasMap){

      if (hasMap == null || hasMap.isEmpty()){
          return;
      }

      Set<TypeElement> typeElements=hasMap.keySet();
      for (TypeElement typeElement:typeElements){
          produceJavaClass(typeElement,hasMap.get(typeElement));
      }

  }

/**
   * 产生Java文件
   * @param typeElement
   * @param bindViewInfos
   */
  private void produceJavaClass(TypeElement typeElement, List<BindViewInfo> bindViewInfos){

      try {
          StringBuffer stringBuffer=new StringBuffer();
          stringBuffer.append("package ");
          stringBuffer.append(getPackageName(typeElement.getQualifiedName().toString())+";\n");
          stringBuffer.append("import com.jackson.bindviewapi.IViewBind;\n");
          stringBuffer.append("public class "+typeElement.getSimpleName()+"$$ViewBinder< T extends "+typeElement.getSimpleName()+"> implements IViewBind<T> {\n");
          stringBuffer.append("@Override\n");
          stringBuffer.append("public void bind(T activity) {\n");

          for (BindViewInfo bindViewInfo:bindViewInfos){
              stringBuffer.append("activity."+bindViewInfo.name+"=activity.findViewById("+bindViewInfo.id+");\n");
          }
          stringBuffer.append("}\n}");
          JavaFileObject javaFileObject=mFiler.createSourceFile(typeElement.getQualifiedName().toString()+"$$ViewBinder");
          Writer writer=javaFileObject.openWriter();
          writer.write(stringBuffer.toString());
          writer.close();
      } catch (IOException e) {
          e.printStackTrace();
      }
  }

  private String getPackageName(String className){
      if (className==null || className.equals("")){
          return "";
      }

      return className.substring(0,className.lastIndexOf("."));
  }
}

重新编译项目,在app/build/source/apt/debug/com.jackson.annotationdemo下就会找到生成的文件,如下图



生成文件的代码,前面已经给出,可以对照着生成java文件的代码,来看BindViewProcessor生成java文件的代码规则。

(四) 使用

在MainActivity中使用,代码如下:

 @BindView(R.id.btn_bind)
    public Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBinder.bind(this);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this,"注解测试",Toast.LENGTH_SHORT).show();
            }
        });
    }

可以看到,在MainActivity中,并没有mButton的findViewById()来初始化,而是通过注解完成,代码运行正常。
源码传送门

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容