阅读 364

Android 注解(Annotation)的自定义和解析方式

注解的本质

编程中经常会使用到注解,比如 Override, 找到 Annotation 这个接口,然后 Control + H 可以发现 Override 继承自 Annotation。

查看 Annotation的文档得知所有的注解类型都继承自此接口。

所以注解的本质很简单:注解就是一个继承了 Annotation 的接口。

注解的作用

注解就是用来标记 SourceCode 中的 Class,Field,Method,给某一段代码标注额外的信息,可以理解为程序员偷懒的方式。 比如 findViewById 写起来很麻烦,就用 butterknife 来偷个懒:

 @BindView(R.id.user) EditText username;
复制代码

那么既然标记了就需要在背后进行解析,否则只能当注释看,具体的解析方式后面再讲。

自定义一个注解

自定义一个叫做 Name 的注解,它有一个方法 value ,用来设置一个 String 值:

@Target(value = {ElementType.FIELD, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Name {
    String value();
}
复制代码

我们会发现注解上面也有注解,比如上面的 @Target,@Retention,这种用来描述注解的注解被称为元注解。

元注解

  • @Target:描述注解作用的目标,比如作用于字段还是方法,比如下面几个常用的类型:

    • ElementType.TYPE:作用于类和接口
    • ElementType.FIELD:作用于字段
    • ElementType.METHOD:作用于方法
  • @Retention:注解的生命周期:

    • RetentionPolicy.SOURCE:编译期可见,不会写入 class 文件,通常用于 apt 编译时自动生成代码
    • RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件,运行时不可见
    • RetentionPolicy.RUNTIME:永久保存,运行时可以反射获取

其他元注解 @Inherited, @Documented很简单,在此不再讨论。

解析自定义注解

根据注解的生命周期大致可以把解析方式分为两类,比如对于 RUNTIME 标识的可以通过代码在运行时通过反射进行解析,对于 SOURCE 标识的可以通过 apt(Annotation Processing Tool)在编译期进行解析。

运行时反射解析注解

  1. 通过反射获取某个被注解的类,方法,字段
  2. 获取注解对象上的注解信息
public class Test {
    public static void main(String[] args){
        testAnnotation();
    }
    @Name(value = "Mr.test")
    public static void testAnnotation(){
        try {
            Class clazz = Test.class;
            // 1. 获取被注解的方法
            Method method = clazz.getMethod("testAnnotation",null);
            // 2.获取 method 上的注解及其 value
            Name name = method.getAnnotation(Name.class);
            System.out.println(name.value());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}
复制代码

编译期解析注解

APT 是 javac 用来处理注解的工具,它可以在编译期扫描所有的注解,获取注解的值以及被注解的 Element,然后自定义一些操作,比如可以检查类是否有有无参构造函数,这个功能可以参考这里;你也可以在编译期根据注解生成一些模板代码,就像 butterknife 所做的事情,下面就以这个功能为例。 在编译期解析注解的步骤如下:

  • 自定义注解类
  • 实现一个 AbstractProcessor 的子类 BindViewProcessor
  • 让编译器识别自定义的 BindViewProcessor
自定义注解类

新建一个 JavaModule,命名为 annotation-lib。 自定义一个 BindView 类,它作用在 Field 上,并且只会在编译期存在,它有一个 value 方法,用来获取被注解 View 的 id:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    int value();
}
复制代码
自定义一个 AbstractProcessor 的子类

新建一个 JavaModule,命名为 annotation-processor,它的 build.gradle 中需要依赖 annotation-lib:

dependencies {
    implementation project(':annotation-lib')
}
复制代码

自定义 BindViewProcessor 如下:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtil;
    private Map<String, ClassCreatorProxy> proxyMap = new HashMap<>();
    private ProcessingEnvironment processingEnv;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        System.out.println("-------BindViewProcessor init");
        elementUtil = processingEnvironment.getElementUtils();
        processingEnv = processingEnvironment;
    }
    /**
     * 指定 java 版本,这个可以用注解 @SupportedSourceVersion(SourceVersion.RELEASE_8) 来代替
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    /**
     * 设置这个处理器是处理什么类型注解的,这个可以用注解 @SupportedAnnotationTypes(com.wangzhen.annotation_complier.StudentProcessor) 来代替
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        return annotations;
    }
    /**
     * @param set              请求处理的注解类型
     * @param roundEnvironment
     * @return true:表示当前注解已经处理;false:可能需要后续的 processor 来处理
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("-------BindViewProcessor process");
        // todo 解析注解,生成 java 文件
        return true;
    }
}
复制代码

需要实现的 4 个方法代码中已经添加了注释,这里就不在多说了,但是 init 方法有一个 ProcessingEnvironment 参数,process 方法中有一个 RoundEnvironment 参数,这两个参数又是干什么的呢?

ProcessingEnvironment
package javax.annotation.processing;
public interface ProcessingEnvironment {
    // 返回用来报告错误、警报和其他通知的 Messager,可以用来打印日志。
    Messager getMessager();
    // 返回用来创建新源、类或辅助文件的 Filer。
    Filer getFiler();
    // 返回用来对 Element 进行操作的实用工具方法。
    Elements getElementUtils();
    // 用来对类型进行操作的实用工具方法。
    Types getTypeUtils();
    
    ...
}
复制代码

getFiler 好理解,就是返回一个 Filer,它支持创建新文件:

public interface Filer {
// 创建一个新的源文件,并返回一个对象以允许写入它。
    JavaFileObject createSourceFile(CharSequence var1, Element... var2) throws IOException;
// 创建一个新的类文件,并返回一个对象以允许写入它。

    JavaFileObject createClassFile(CharSequence var1, Element... var2) throws IOException;
// 创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象。
    FileObject createResource(Location var1, CharSequence var2, CharSequence var3, Element... var4) throws IOException;
//  返回一个用于读取现有资源的对象。
    FileObject getResource(Location var1, CharSequence var2, CharSequence var3) throws IOException;
}

复制代码

比如我们将使用它的 createSourceFile 创建一个 java 文件。

Elements 用来对 Element 进行操作,那么 Element 又是什么呢? Element 表示一个程序元素,比如包、类或者方法。 它有几个常见的子类:

Element
    |--- PackageElement // 包元素,可以获得包名
    |--- TypeElement // 表示一个类或接口
    |--- VariableElement //表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
复制代码

可以看出 Element 其实就是包含了我们所写代码的一些信息,它常用的方法如下:

package javax.lang.model.element;
public interface Element extends AnnotatedConstruct {
    // 返回此元素直接封装(非严格意义上)的元素,比如如果是一个 VariableElement,获得是它所在的类。
    List<? extends Element> getEnclosedElements();
   
    // 获取此元素上的注解
    <A extends Annotation> A getAnnotation(Class<A> var1);
 
 ...
}
复制代码

Elements 常见的方法如下:

package javax.lang.model.util;

public interface Elements {
    // 返回所给元素的包元素
    PackageElement getPackageOf(Element var1);
    // 返回类型元素的所有成员,不管是继承的还是直接声明的。
    List<? extends Element> getAllMembers(TypeElement var1);
    ...
}
复制代码

所以我们可以通过 Elements 获取所有包元素,这样就获取到了新建 java 文件的包名,可以通过 Filer 创建一个 java 文件,现在缺少的是获取被 BindView 注释的 Element 了,它可以通过 RoundEnvironment 来获得。

RoundEnvironment

RoundEnvironment 常见的方法如下:

package javax.annotation.processing;

public interface RoundEnvironment {
   // 返回使用给定类型注解的元素。
    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
    // 返回使用给定类型注解的元素。
    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
    ...
}

复制代码
在 process 方法中拼写字符串然后生成文件
@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("-------BindViewProcessor process");
        proxyMap.clear();
        // 1.获取所有有 BindView 注解的 Element,由于 BindView 是作用在 Field 上的,所以这些 Element 一定是 Field
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        // 2.遍历所有被注解的 Element
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            // 3.获得它所在的类
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            ClassCreatorProxy classCreatorProxy = proxyMap.get(fullClassName);
            if (classCreatorProxy == null) {
                classCreatorProxy = new ClassCreatorProxy(elementUtil, classElement);
                proxyMap.put(fullClassName, classCreatorProxy);
            }
            // 4.获得它注解中的信息
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            classCreatorProxy.putElement(id, variableElement);
            // 5.创建一个 java 文件
            createJavaFile();
        }
        return true;
    }

    private void createJavaFile(){
        for(String key:proxyMap.keySet()){
            ClassCreatorProxy proxyInfo = proxyMap.get(key);
            try {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());
                Writer writer = jfo.openWriter();
                // 6.拼写 java 文件中的代码字符串
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
                System.out.println("-------BindViewProcessor create java file success");
            } catch (IOException e) {
                System.out.println("-------BindViewProcessor create java file failed");
                e.printStackTrace();
            }
        }
    }
复制代码

ClassCreatorProxy 只是用来拼写代码字符串:

package com.wangzhen.annotation_processor;

public class ClassCreatorProxy {
    private String bindingClassName;
    private String packageName;
    private TypeElement typeElement;
    private Map<Integer, VariableElement> variableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elements, TypeElement classElement) {
        this.typeElement = classElement;
        PackageElement packageElement = elements.getPackageOf(typeElement);
        packageName = packageElement.getQualifiedName().toString();
        bindingClassName = typeElement.getSimpleName() + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element) {
        variableElementMap.put(id, element);
    }

    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        // 拼写包名
        builder.append("package ").append(packageName).append(";\n\n")
        // 拼写依赖
            .append("import com.wangzhen.annotation_lib.*;").append("\n\n")
            // 拼写类结构
            .append("public class ").append(bindingClassName).append(" {\n");
        generateMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }
    // 拼写方法
    private void generateMethods(StringBuilder builder) {
        builder.append("public void bind(" + typeElement.getQualifiedName() + " host ) {\n");
        for (int id : variableElementMap.keySet()) {
            VariableElement element = variableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            builder.append("host." + name).append(" = ");
            builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));\n");
        }
        builder.append("  }\n");
    }

    public String getProxyClassFullName() {
        return packageName + "." + bindingClassName;
    }

    public TypeElement getTypeElement() {
        return typeElement;
    }
}
复制代码
让编译器识别自定义的 BindViewProcessor

虽然代码写好了,但是还需要编译器在编译期间执行我们的代码,主要包含两步:

  • 向编译器声明我们的 BindViewProcessor 最简单的方式是使用 google 提供的 AutoService:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...
}
复制代码

使用 AutoService 需要在 annotaiton-processor module 下导入依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    implementation project(':annotation-lib')
}
复制代码
  • 在主 module app 中添加依赖 annotation-processor
dependencies {
    ...
    implementation project(':annotation-lib')
    // 需要使用 annotationProcessor
    annotationProcessor project(':annotation-processor')
}
复制代码

使用

在 app 下的 MainActivity 使用 BindView,然后使用反射调用我们生成的 MainActivity_ViewBinding.java:

public class MainActivity extends AppCompatActivity {
    @BindView(value = R.id.tv_test)
    TextView tvTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bind();
        tvTest.setText("bind success");
    }

    private void bind() {
        try {
            Class bindViewClazz = Class.forName(this.getClass().getName() + "_ViewBinding");
            Method method = bindViewClazz.getMethod("bind", this.getClass());
            method.invoke(bindViewClazz.newInstance(), this);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}
复制代码

现在我们编译一下项目,编译器会在下面的路径中生成我们所拼写的 java 文件:

app
|- build
|-- generated
|--- source
|---- apt
|----- debug
|------ 你的包名
|------- MainActivity_ViewBinding.java
复制代码

MainActivity_ViewBinding.java 内容如下:

package com.wangzhen.annotationjavatest;

import com.wangzhen.annotation_lib.*;

public class MainActivity_ViewBinding {
    public void bind(com.wangzhen.annotationjavatest.MainActivity host) {
        host.tvTest = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165359));
    }
}
复制代码

通过 javapoet 生成代码

手动拼写字符串易出错且麻烦,可以用 javapoet 像诗人一样书写 java source code:

public TypeSpec generateJavaCodeByPoet(){

        TypeSpec bindingClass = TypeSpec.classBuilder(bindingClassName)
            .addModifiers(Modifier.PUBLIC)
            .addMethod(generateMethodByPoet())
            .build();
        return bindingClass;
    }

    private MethodSpec generateMethodByPoet(){
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
            .addModifiers(Modifier.PUBLIC)
            .returns(void.class)
            .addParameter(ClassName.bestGuess(typeElement.getQualifiedName().toString()),"host");

        for (int id : variableElementMap.keySet()) {
            VariableElement element = variableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            String codeString = "host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));";
            methodBuilder.addCode(codeString);
        }
        return methodBuilder.build();
    }
复制代码

代码

github

参考

【Android】APT(编译时生成代码)