【Android APT】注解处理器 ( 根据注解生成 Java 代码 )

2023-03-29 12:49:27 浏览数 (2)

文章目录

  • 一、生成 Java 代码
  • 二、实现 IButterKnife 接口
  • 三、视图绑定主要操作
  • 四、完整注解处理器代码
  • 五、博客资源

Android APT 学习进阶路径 : 推荐按照顺序阅读 , 从零基础到开发简易 ButterKnife 注解框架的学习路径 ;

  • 【Java 注解】注解简介及作用
  • 【Java 注解】自定义注解 ( 注解属性定义与赋值 )
  • 【Java 注解】自定义注解 ( 元注解 )
  • 【Java 注解】自定义注解 ( 注解解析 )
  • 【Java 注解】自定义注解 ( 使用注解实现简单测试框架 )
  • 【Android APT】编译时技术 ( ButterKnife 原理分析 )
  • 【Android APT】编译时技术 ( 编译时注解 和 注解处理器 依赖库 )
  • 【Android APT】编译时技术 ( 开发编译时注解 )
  • 【Android APT】注解处理器 ( 注解标注 与 初始化方法 )
  • 【Android APT】注解处理器 ( 配置注解依赖、支持的注解类型、Java 版本支持 )
  • 【Android APT】注解处理器 ( Element 注解节点相关操作 )
  • 【Android APT】注解处理器 ( 生成代码并自动绑定控件 )

上一篇博客 【Android APT】注解处理器 ( Element 注解节点相关操作 )中 对 注解所标注的 节点 , 进行了获取及分析 , 将 VariableElement 类型的 注解节点 , 按照所在 Activity 进行了分组 ;

本篇博客开发 注解处理器 的 生成代码部分 ;

一、生成 Java 代码


上一篇博客 【Android APT】注解处理器 ( Element 注解节点相关操作 ) 中已经将 注解节点 , 按照 Activity 分组 , 放在了 HashMap<String, ArrayList<VariableElement>> elementMap 数据结构中 , 要生成的 .java 类的个数就是该 HashMap 键值对的个数 ;

目标是生成如下代码 :

代码语言:javascript复制
package kim.hsl.apt;

import android.view.View;

public class MainActivity_ViewBinder implements IButterKnife<kim.hsl.apt.MainActivity> {
    public void bind(kim.hsl.apt.MainActivity target) {
        target.hello = target.findViewById(2131230899);
    }
}

逐个遍历 HashMap<String, ArrayList<VariableElement>> elementMap 数据结构 , 要从该 HashMap 中获取上述要生成代码的相关信息 ;

代码语言:javascript复制
package kim.hsl.apt;

生成上述代码 , 需要获取包名 kim.hsl.apt , 根据 VariableElement 注解节点 , 获取 TypeElement 父节点 , 使用 ElementUtils 获取 TypeElement 节点对应的 PackageElement 包节点 , 调用该节点的 getQualifiedName 方法获取完整的包名信息 ;

代码语言:javascript复制
//获取对应类的包名
// 获取 VariableElement 的父节点 TypeElement
TypeElement typeElement = (TypeElement) variableElements.get(0).getEnclosingElement();

// 获取 Activity 名称
String activitySimpleName = typeElement.getSimpleName().toString();

// 获取包节点
PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(typeElement);

// 获取包名
String packageName = packageElement.getQualifiedName().toString();
代码语言:javascript复制
public class MainActivity_ViewBinder implements IButterKnife<kim.hsl.apt.MainActivity> {

生成上述代码 , 需要获取类名 , 以及完整的包名 和 类名 ; 调用 TypeElement 的 getSimpleName 方法 , 可以获取不带包名的类名 ;

代码语言:javascript复制
// 获取类名
String className = activitySimpleName   "_ViewBinder";
代码语言:javascript复制
    public void bind(kim.hsl.apt.MainActivity target) {
        target.hello = target.findViewById(2131230899);
    }
}

生成上述代码 , 其中 target.hello = target.findViewById(2131230899); 代码需要循环生成 , 该 Activity 中有多少变量添加了 @BindView 注解 , 就需要有几行上述代码 ;

代码语言:javascript复制
// public void bind(kim.hsl.apt.MainActivity target){
stringBuffer.append("public void bind("   packageName   "."   activitySimpleName   " target){n");

for (VariableElement variableElement : variableElements){
    // 循环被注解的字段
    // 为每个 VariableElement 注解字段生成 target.findViewById(R.id.xxx); 代码
    // 获取成员变量名
    String variableName = variableElement.getSimpleName().toString();
    // 获取资源 id , 通过注解 , 获取注解属性 value
    int resourceId = variableElement.getAnnotation(BindView.class).value();
    // target.
    stringBuffer.append("target."   variableName   " = target.findViewById("   resourceId   ");n");
}

// }
stringBuffer.append("}n");
// }
stringBuffer.append("}n");

二、实现 IButterKnife 接口


该接口直接定义在主应用 , 上面的 注解处理器 本质上就是在 编译时 生成该接口的实现类 , 并实现了其中的 bind 方法 , 每个 Activity 界面都要 生成一个该接口的子类对象 , 在该 生成的 IButterKnife 子类中进行 组件的 findViewById 的视图绑定操作 ;

代码语言:javascript复制
package kim.hsl.apt;

public interface IButterKnife<T> {
    void bind(T target);
}

严谨一点的话 , 该接口一般是定义在 Android 依赖库 中 ;

三、视图绑定主要操作


在 Activity 界面中 , 调用

代码语言:javascript复制
ButterKnife.bind(this);

方法 , 即可实现视图绑定操作 , 实际上是通过 Activity 的类名 “MainActivity” , 获取到生成的类名 “MainActivity_ViewBinder” , 通过反射获取该类对象 ;

直接创建该对象 , 并调用对象的 bind 方法 , 即可完成视图绑定 ;

ButterKnife 及静态 bind 方法实现 :

代码语言:javascript复制
package kim.hsl.apt;

public class ButterKnife {

    /**
     * 在 Activity 中调用该方法, 绑定接口
     * @param target
     */
    public static void bind(Object target){
        String className = target.getClass().getName()   "_ViewBinder";

        try {
            // 通过反射得到 MainActivity_ViewBinder 类对象
            Class<?> clazz = Class.forName(className);

            // 调用生成的代码 MainActivity_ViewBinder 的 bind 方法
            if (IButterKnife.class.isAssignableFrom(clazz)){
                IButterKnife iButterKnife = (IButterKnife) clazz.newInstance();
                iButterKnife.bind(target);
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

四、完整注解处理器代码


代码语言:javascript复制
package kim.hsl.annotation_compiler;

import com.google.auto.service.AutoService;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import kim.hsl.annotation.BindView;

/**
 * 生成代码的注解处理器
 */
@AutoService(Processor.class)
public class Compiler extends AbstractProcessor {

    /**
     * 生成 Java 代码对象
     */
    private Filer mFiler;

    /**
     * 日志打印
     */
    private Messager mMessager;

    /**
     * 初始化注解处理器相关工作
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.mFiler = processingEnv.getFiler();
        this.mMessager = processingEnv.getMessager();
    }

    /**
     * 声明 注解处理器 要处理的注解类型
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportedAnnotationTypes = new HashSet<String>();
        // 将 BindView 全类名 kim.hsl.annotation.BinndView 放到 Set 集合中
        supportedAnnotationTypes.add(BindView.class.getCanonicalName());
        return supportedAnnotationTypes;
    }

    /**
     * 声明支持的 JDK 版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 通过 ProcessingEnvironment 类获取最新的 Java 版本并返回
        return processingEnv.getSourceVersion();
    }

    /**
     * 搜索 Android 代码中的 BindView 注解
     * 并生成相关代码
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        // 搜索 BindView , 将 BindView 注解放在什么元素上 , 得到的就是相应类型的元素
        // 根据 注解类型 获取 被该注解类型 标注的元素 , 元素可能是类 , 方法 , 字段 ;
        // 通过 getElementsAnnotatedWith 方法可以搜索到整个 Module 中所有使用了 BindView 注解的元素
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);

        // @BindView 注解标注的都是 Activity 中的成员字段,
        // 上述 elements 中的元素都是 VariableElement 类型的节点
        HashMap<String, ArrayList<VariableElement>> elementMap = new HashMap<>();

        // 遍历 elements 注解节点 , 为节点分组
        for (Element element : elements){
            // 将注解节点类型强转为 VariableElement 类型
            VariableElement ve = (VariableElement) element;

            // 获取该注解节点对应的成员变量类名
            // 先获取该注解节点的上一级节点 , 注解节点是 VariableElement , 成员字段节点
            // 上一级节点是就是 Activity 类节点对应的 类节点 TypeElement
            TypeElement te = (TypeElement) ve.getEnclosingElement();

            // 获取 Activity 的全类名
            String activityFullName = te.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.NOTE, "TypeElement : "   activityFullName   " , VariableElement : "   ve.getSimpleName());

            // 获取 elementMap 集合中的 Activity 的全类名对应的 VariableElement 节点集合
            // 如果是第一次获取 , 为空 ,
            // 如果之前已经获取了该 Activity 的全类名对应的 VariableElement 节点集合, 那么不为空
            ArrayList<VariableElement> variableElements = elementMap.get(activityFullName);
            if (variableElements == null){
                variableElements = new ArrayList<>();
                // 创建之后 , 将集合插入到 elementMap 集合中
                elementMap.put(activityFullName, variableElements);
            }
            // 将本节点插入到 HashSet<VariableElement> variableElements 集合中
            variableElements.add(ve);
        }

        // 生成代码
        // 遍历 HashMap<String, HashSet<VariableElement>> elementMap 集合
        // 获取 Key 的迭代器
        Iterator<String> iterator = elementMap.keySet().iterator();

        while (iterator.hasNext()){
            // 获取 Activity 全类名
            String key = iterator.next();

            // 获取 Activity 下被注解标注的 VariableElement 注解节点
            ArrayList<VariableElement> variableElements = elementMap.get(key);

            //获取对应类的包名
            // 获取 VariableElement 的父节点 TypeElement
            TypeElement typeElement = (TypeElement) variableElements.get(0).getEnclosingElement();

            // 获取 Activity 名称
            String activitySimpleName = typeElement.getSimpleName().toString();

            // 获取包节点
            PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(typeElement);
            // 获取包名
            String packageName = packageElement.getQualifiedName().toString();

            // 获取类名
            String className = activitySimpleName   "_ViewBinder";

            // 写出文件的字符流
            Writer writer = null;

            // 获取到包名后 , 开始生成 Java 代码
            try {

                mMessager.printMessage(Diagnostic.Kind.NOTE, "Create Java Class Name : "   packageName   "."   className);

                // 根据 包名.类名_ViewBinder 创建 Java 文件
                JavaFileObject javaFileObject = mFiler.createSourceFile(packageName   "."   className);

                // 生成 Java 代码
                writer = javaFileObject.openWriter();

                // 生成字符串文本缓冲区
                StringBuffer stringBuffer = new StringBuffer();
                // 逐行写入文本到缓冲区中

                // package kim.hsl.apt;
                stringBuffer.append("package "   packageName  ";n");

                // import android.view.View;
                stringBuffer.append("import android.view.View;n");

                // public class MainActivity_ViewBinding implements IButterKnife<kim.hsl.apt.MainActivity>{
                stringBuffer.append("public class "   className   " implements IButterKnife<"   packageName   "."   activitySimpleName  ">{n");
                //stringBuffer.append("public class "   className  "{n");

                // public void bind(kim.hsl.apt.MainActivity target){
                stringBuffer.append("public void bind("   packageName   "."   activitySimpleName   " target){n");

                for (VariableElement variableElement : variableElements){
                    // 循环被注解的字段
                    // 为每个 VariableElement 注解字段生成 target.findViewById(R.id.xxx); 代码

                    // 获取成员变量名
                    String variableName = variableElement.getSimpleName().toString();
                    // 获取资源 id , 通过注解 , 获取注解属性 value
                    int resourceId = variableElement.getAnnotation(BindView.class).value();

                    // target.
                    stringBuffer.append("target."   variableName   " = target.findViewById("   resourceId   ");n");
                }


                // }
                stringBuffer.append("}n");

                // }
                stringBuffer.append("}n");

                mMessager.printMessage(Diagnostic.Kind.NOTE, "stringBuffer.toString() : "   stringBuffer.toString());

                mMessager.printMessage(Diagnostic.Kind.NOTE, "writer : "   writer);

                        // 将字符串缓冲区的数据写出到 Java 文件中
                writer.write(stringBuffer.toString());

                mMessager.printMessage(Diagnostic.Kind.NOTE,"write finished");


            } catch (Exception e) {
                mMessager.printMessage(Diagnostic.Kind.NOTE,"IOException");
                e.printStackTrace();
            }finally {
                if (writer != null){
                    try {
                        mMessager.printMessage(Diagnostic.Kind.NOTE,"write closed");
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

        }

        mMessager.printMessage(Diagnostic.Kind.NOTE,"process finished");

        return false;
    }
}

五、博客资源


博客源码 :

  • GitHub : https://github.com/han1202012/APT
  • CSDN : https://download.csdn.net/download/han1202012/18917831

0 人点赞