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,

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

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

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

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

① 定义一个绑定注解的接口
public interface IViewBind {
    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 iViewBinder= (IViewBind) 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 {
@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 annoations, RoundEnvironment roundEnv) { }

    // 4
    @Override
    public Set 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 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 getSupportedAnnotationTypes() {
      Set annotations=new LinkedHashSet<>();
      annotations.add(BindView.class.getCanonicalName());
      return annotations;
  }

  @Override
  public boolean process(Set annotations, RoundEnvironment roundEnv) {
      Set elements = roundEnv.getElementsAnnotatedWith(BindView.class);
      Map> 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());
          }
          ArrayList bindViewInfos=bindViewMap.get(typeElement);
          // 添加list
          bindViewInfos.add(new BindViewInfo(id,element.getSimpleName().toString()));
      }
      produceClass(bindViewMap);
      return true;
  }


  private void produceClass(Map> hasMap){

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

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

  }

/**
   * 产生Java文件
   * @param typeElement
   * @param bindViewInfos
   */
  private void produceJavaClass(TypeElement typeElement, List 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 {\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()来初始化,而是通过注解完成,代码运行正常。
源码传送门

你可能感兴趣的:(Android注解,这几篇文章就够了(三)自己写个注解处理器)