编译时注解器初探(一)

编译时注解器初探(一)

注解处理器 (Annotation Processor)

编译时注解和运行时注解定义的方式是完全一样的,不同的是它们对于注解的处理方式, 运行时注解是在程序运行时通过反射获取注解然后处理的,编译时注解是程序在编译期间通过注解处理器处理的。
编译时注解的处理过程是递归的 ,先扫描原文件,再扫描生成的文件,直到所有文件中都没有待处理的注解才会结束。其中扫描注解并不需要我们处理,我们需要关心的就是如何处理注解以及如何将处理后的结果写入到文件中

Processor就是用于处理编译器注解的类。通过继承AbstractProcessor就可以自定义处理注解。

init为初始化方法,这个方法会被注解处理工具调用,并传入一个ProcessingEnvironment变量,这个变量非常重要,它会提供一些非常使用的工具如Elements, Filer, Messager,Types等。

Elements:一个用来处理Element的工具类
Types:一个用来处理TypeMirror的工具类);
Filer:正如这个名字所示,使用Filer你可以创建文件。
在注解处理过程中,我们扫描所有的Java源文件。源代码的每一个部分都是一个特定类型的
Messager提供给注解处理器一个报告错误、警告以及提示信息的途径
Element。换句话说:Element代表程序的元素,例如包、类或者方法。每个Element代表一个静态的、语言级别的构件。在下面的例子中,我们通过注释来说明这个:

package com.example;    // PackageElement
public class Foo {        // TypeElement
    private int a;      // VariableElement
    private Foo other;  // VariableElement
    public Foo () {}    // ExecuteableElement
    public void setA (  // ExecuteableElement
                     int newA   // VariableElement
                     ) {}
}

你必须换个角度来看源代码,它只是结构化的文本,他不是可运行的。你可以想象它就像你将要去解析的XML文件一样(或者是编译器中抽象的语法树)。就像XML解释器一样,有一些类似DOM的元素。你可以从一个元素导航到它的父或者子元素上。

process:这个方法是Processor中最重要的方法,所有关于注解的处理和文件的生成都是在这个方法中完成的。这个方法也会被注解处理工具调用。它有两个参数,第一个Set set包含所有待处理的的注解,需要注意的是,如果你定义了一个注解但是没有在代码中使用它,这样是不会加到set中的。第二个参数roundEnvironment表示当前注解所处的环境,通过这个参数可以查询到当前这一轮注解处理的信息。第一个参数我们通常用不到它,最常用的是roundEnvironment中的getElementsAnnotatedWith方法,这个方法可以返回被特定注解标注的所有元素。process方法还有一个boolean类型的返回值,当返回值为true的时候表示这个Processor处理的注解不会再被后续的Processor处理。如果返回false,则表示这些注解还会被后续的Processor处理,类似拦截器模式。

getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上.你也可以使用注解来代替getSupportedAnnotationTypes()

创建一个java依赖库

下面用一个例子实践一下
现在我们创建一个类ButterKnifeProcessor 继承 AbstractProcessor。

package com.example.apt_api;
@SupportedAnnotationTypes("com.example.apt_api.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ButterKnifeProcessor extends AbstractProcessor{
}


@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
    if (set == null || set.isEmpty()){
        return false;
    }
    elementPackage.clear();
    //1 找到被注解的元素
    Set bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    //2 收集注解元素的信息
    collecteData(bindViewElement);
    //3 动态生成代码
    generateCode();
    return true;
}

在main目录下创建一个resources目录,resources目录下创建一个META-INF目录,META-INF目录下创建一个services目录,services目录下创建一个文件javax.annotation.processing.Processor,目录结构如下

--main
  --resources
    --META-INF
      --services
        --javax.annotation.processing.Processor

里面内容就是注解类的全路径,如下

com.example.apt_api.ButterKnifeProcessor

创建一个注解,用于标识需要处理的字段

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

假设要注解的类如下

package com.lyw.demo;
public class MainActivity extends Activity {
    @BindView(R.id.drawer_layout)
     DrawerLayout mDrawerLayout;
    @BindView(R.id.list_slidermenu)
     ListView mDrawerList;
}

那么我们在注释1 里面获取到的元素就是 mDrawerLayout和mDrawerList,收集到这个元素之后,我们还得知道它在哪个类里面,通过collecteData方法就可以获取

  private void collecteData(Set bindViewElement) {
        Iterator iterator = bindViewElement.iterator();
        while (iterator.hasNext()){
            Element element = iterator.next();
            //获取当前元素的类型
            TypeMirror typeMirror = element.asType();
            //字符串VIEW_TYPE = "android.view.View"
            TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType();
            System.out.println("element.toString():"+element.toString()+","+typeMirror.toString());
            //判断是否View或者View子类型
            if (typeUtils.isSubtype(typeMirror,viewTypeMirror)|| typeUtils.isSameType(typeMirror,viewTypeMirror)){
                //获取外层类元素,也就是com.lyw.demo.MainActivity
                TypeElement parent = (TypeElement) element.getEnclosingElement();
                System.out.println("parent.....................:"+parent.toString());
                List  list = elementPackage.get(parent);
                if (list == null){
                    list = new ArrayList<>();
                    //存入集合,后面生成代码需要
                    elementPackage.put(parent,list);
                }
                list.add(element);
            }else{
                System.out.println("view 应该标注在View上");
                throw  new RuntimeException("view 应该标注在View上");
            }
        }
    }

通过collecteData方法,我们把哪个类下面的用枚举BindView修饰的字段存入了elementPackage集合
下面看生成代码的方法,生成代码用的javapoet库,要熟悉JavaPoet语法可以参考此文章,javapoet语法

 private void generateCode() {
        Set>> entries = elementPackage.entrySet();
        Iterator>> iterator = entries.iterator();
        while (iterator.hasNext()){
            Map.Entry> entry = iterator.next();
            TypeElement parent = entry.getKey();
            List elements = entry.getValue();
            //获取当前所在类的包名 也就是导入包package 后面的值 
            String packageName = elementUtils.getPackageOf(parent).asType().toString();
            System.out.println("packageName:"+packageName);
            //获取当前所在类的名字
            String activityName = parent.getQualifiedName().toString().substring(packageName.length()+1);
            //合成我们要生成类的名字,也就是MainActivity_ViewBinding
            ClassName bindingClassName = ClassName.get(packageName,activityName+"_ViewBinding");
            //生成一个方法
            MethodSpec methodSpec = generateBindViewMethod(parent,elements,bindingClassName);
            System.out.println("bindingClassName:"+bindingClassName);
            try {
                //生成对应的类文件
                JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName)
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(methodSpec)
                        .build()).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

generateBindViewMethod生成一个方法

    private MethodSpec generateBindViewMethod(TypeElement parent, List elements,ClassName bindingClassName) {
        System.out.println("generateBindViewMethod:"+elements.toString());
        //获取包名
        String packageName = elementUtils.getPackageOf(parent).asType().toString();
        //获取类名
        String activityName = parent.getQualifiedName().toString().substring(packageName.length()+1);


        ClassName param = ClassName.get(packageName, activityName);
        ParameterSpec.Builder builder = ParameterSpec.builder(param,"target");

        //生成构造方法
        MethodSpec.Builder bindViewMathod = MethodSpec.constructorBuilder();
        bindViewMathod.addParameter(builder.build());
        bindViewMathod.addModifiers(Modifier.PUBLIC);
        //添加方法体
        bindViewMathod.addStatement("$T temp = ($T)target",parent,parent);
        for (Element element:elements){
            int id = element.getAnnotation(BindView.class).value();
            bindViewMathod.addStatement("temp.$N = temp.findViewById($L)",element.getSimpleName(),id);
        }
        return bindViewMathod.build();
    }

编译后在app/build/generated/source/apt/debug/com/bluetooth.demo目录下有如下类

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(MainActivity target) {
    MainActivity temp = (MainActivity)target;
    temp.mDrawerLayout = temp.findViewById(2131230848);
    temp.mDrawerList = temp.findViewById(2131230993);
  }
}

再写个类统一供外部调用

public class ButterKnife {
    public static void bind(Object target){
       String clazzName =  target.getClass().getName()+"_ViewBinding";
        try {
            Class clazz = Class.forName(clazzName);
            Constructor constructor = clazz.getConstructor(target.getClass());
           Object o =  constructor.newInstance(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

可以看到使用生成的那个类时,我们还是要通过反射去调用。并不是说就完全没有了反射。当前可以把那个constructor这个对象缓存起来,这样后续调用就直接用缓存的值。
假设我们使用运行时注解,逻辑如下。

public class ButterKnife {
    public static void bind(Object target){
       // 1 反射获取BindView修饰的字段
      // 2 反射设置字段的值
    }
}

虽然代码相对来说比较简单,但是由于反射使用过多,性能会有损耗。编译时注解虽然操作起来麻烦,但是相对于运行时注解来说,反射的地方减少了很多。

参考文章

Java注解之编译时注解
Java 注解处理器 (Annotation Processor)
JavaPoet语法

你可能感兴趣的:(编译时注解器初探(一))