高效的背后写手:APT

APT(Annotation Processing Tool)
是一种注解处理工具,他对源码文件进行检测找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须通过APT工具来进行处理。也就是说APT可以根据我们制定的规则来帮我们生成java文件。

java文件结构
java源文件是一种结构体语言,分为四种元素/节点,即所有的java文件都是由他们构成的,如下

package com.tianxia.apt     //PackageElement 包元素/包节点
public class Main{ // TypeElement类元素/类节点
   private int x;  // VariableElement 属性元素/属性节点
   private void Main(){ //ExecuteableElement 方法元素/方法节点
      ....
   }
}
节点意义
PackageElement表示一个包程序元素,提供对有关包及其成员的信息的访问
ExecuteableElement表示某个类或接口的方法、构造方法或初始化程序(静态或实例)
VariableElement表示一个字段、enum常量、方法或构造方法的参数、局部变量或异常参数
TypeElement表示一个类或接口程序元素。提供对有关类型及其成员的信息访问

需要掌握的API

方法
getEncloseedElements()返回该元素直接包含的子元素
getEnclosingElement()返回包含该元素的父元素,与上一个方法相反
getKind()返回element的类型,判断是那种element
getModifiers()获取修饰关键字,如 public static final等
getSimpleName()获取元素名,不带包名
getQualifiedName()获取全名,如果是类元素的话包含完整的包名路径
getParameters()获取方法的参数元素,每一个元素都是VariableElement
getReturnType()获取方法元素的返回类型
getConstantValue()如果属性变量被final修饰,则可以使用该方法获取他的值

了解了这些基本内容我们开始撸码
新建工程ButterKnifeDemo,我们以ButterKnife这个三方库为例来学习APT的用法,然后新建3个module即annotation、complier、library。其中annotation和complier为java Library,library为Android Library
在这里插入图片描述在这里插入图片描述然后对他们进行一些配置
1.annotation的build.gradle

// 中文乱码问题(错误: 编码GBK的不可映射字符)
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

2.complier的build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // As3.5 + gradle5.4.1-all
    compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

    // 帮助我们通过类调用的形式来生成Java代码
    implementation "com.squareup:javapoet:1.10.0"
    // 引入annotation,处理@BindView、@Onclick注解
    implementation project(':annotation')

}

// 中文乱码问题(错误: 编码GBK的不可映射字符)
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

3.app的build.gradle

    implementation project(":annotation")
    implementation project(":library")
    annotationProcessor project(":complier")

ok,同步一下。

在annotation module中定义两个注解

//SOURCE 注解仅在源码中保留,class文件中不存在
//CLASS 注解在源码和class文件中存在,但运行时不存在
//RUNTIME 注解在源码,class文件中存在且运行时可以通过反射机制获取
@Target({ElementType.FIELD})  //作用在属性之上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

@Target({ElementType.METHOD})  //作用在方法之上
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] value();
}

library moudle文件
/**
 * 核心类
 */
public class ButterKnife {
    public static void bind(Activity activity){
        // 拼接类名,如:MainActivity$ViewBinder
        String className = activity.getClass().getName() +"$ViewBinder";

        try {
            //加载上述拼接类
            Class<?> viewBinderClass = Class.forName(className);
            //接口 = 接口实现类
            ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
            //调用接口方法
            viewBinder.bind(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public abstract class DebouncingOnClickListener implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        //调用抽象方法
        doClick(v);
    }

    public abstract void doClick(View view);
}
public interface ViewBinder<T> {
    void bind(T target);
}
compiler module文件

先来定义一个常量类,在注解处理时会用到,需要注意的是包名路径要对应自己的工程

public class Constants {

    // 注解处理器中支持的注解类型
    public static final String BINDVIEW_ANNOTATION_TYPES = "com.tianshang.annotation.BindView";
    public static final String ONCLICK_ANNOTATION_TYPES = "com.tianshang.annotation.OnClick";

    // 布局、控件绑定实现接口
    public static final String VIEWBINDER = "com.tianshang.library.ViewBinder";

    public static final String CLICKLISTENER = "com.tianshang.library.DebouncingOnClickListener";

    public static final String VIEW = "android.view.View";

    // bind方法名
    public static final String BIND_METHOD_NAME = "bind";

    // bind方法的参数名target
    public static final String TARGET_PARAMETER_NAME = "target";
}

以及一个工具类

/**
 * 字符串、集合判空工具
 */
public final class EmptyUtils {
    public static boolean isEmpty(CharSequence cs) {
        return cs == null || cs.length() == 0;
    }

    public static boolean isEmpty(Collection<?> coll) {
        return coll == null || coll.isEmpty();
    }

    public static boolean isEmpty(final Map<?, ?> map) {
        return map == null || map.isEmpty();
    }

}

接下来到了这篇文章的核心之处,处理自定义注解并编写我们所需生成java文件的模板。

我们要生成的java模板是这样的
public class MainActivity_ViewBinding implements ViewBinder<com.tianshang.butterknifedemo.MainActivity> {
    public void bind(final com.tianshang.butterknifedemo.MainActivity target) {
        target.tv1.findViewById(R.id.tv_1);
        target.tv2.findViewById(R.id.tv_2);

        target.findViewById(R.id.tv_1).setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void onClick(View v) {
                target.click(view);
            }
        });

        target.findViewById(R.id.tv_2).setOnClickListener(new DebouncingOnClickListener() {
            @Override
            public void onClick(View v) {
                target.click(view);
            }
        });
    }
}

注解处理类

//用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
//允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({Constants.BINDVIEW_ANNOTATION_TYPES, Constants.ONCLICK_ANNOTATION_TYPES})
//指定jdk的编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//以上三行是固定写法

public class ButterKnifeProcess extends AbstractProcessor {  //处理类必须继承AbstractProcessor

    //操作Element工具类(类,函数,属性都是Element)
    private Elements elementUtils;
    //type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;
    //Messager 用来报告错误,警告和其他提示信息
    private Messager messager;
    //文件生成器 类、资源,Filter用来创建新的类,class文件以及辅助文件
    private Filer filer;
    //key:类节点,value:被@BindView注解的属性集合
    private Map<TypeElement, List<VariableElement>> tempBindViewMap = new HashMap<>();
    //key:类节点,value:被@OnClick注解的方法集合
    private Map<TypeElement, List<ExecutableElement>> tempOnClickMap = new HashMap<>();

    /**
     * 用于一些初始化的操作,通过processingEnvironment参数可以获取一些有用的工具类
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //初始化
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        typeUtils = processingEnvironment.getTypeUtils();
        messager.printMessage(Diagnostic.Kind.NOTE,
                "注解处理器初始化完成,开始处理注解------------------------------->");

    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //允许处理的注解的元素不为空(即有元素被@BindView或@OnClick标记)
        if (!EmptyUtils.isEmpty(set)) {
            //获取所有被@BindView注解的元素集合
            Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            Set<? extends Element> onClickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);

            if (!EmptyUtils.isEmpty(bindViewElements) || !EmptyUtils.isEmpty(onClickElements)) {
                //赋值临时map存储,用来存储被注解的属性集合
//                比如MainActivity中有两个控件
//                @BindView(R.id.tv_1)
//                TextView tv1;
//                @BindView(R.id.tv_2)
//                TextView tv2;
//                以这种方式存储下来
//                {"MainActivity_ViewBinding":列表{R.id.tv_1,R.id.tv_2}}
//                onClick同理
                valueOfMap(bindViewElements, onClickElements);
                // 生成类文件,如:
                try {
                    createJavaFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return true;
            }
        }
        return false;
    }

    /**
     * 这个方法实现过程按照12345的顺序实现,
     * @throws IOException
     */
    private void createJavaFile() throws IOException {
        //判断是否有需要生成的类文件
        if (!EmptyUtils.isEmpty(tempBindViewMap)) {

            //获取接口的类型
            TypeElement viewBinderType = elementUtils.getTypeElement(Constants.VIEWBINDER);
            TypeElement clickListenerType = elementUtils.getTypeElement(Constants.CLICKLISTENER);
            TypeElement viewType = elementUtils.getTypeElement(Constants.VIEW);

            //从下往上写(javaPoet的技巧)
            for (Map.Entry<TypeElement, List<VariableElement>> entry : tempBindViewMap.entrySet()) {

                //类名(TypeElement)
                ClassName className = ClassName.get(entry.getKey());
                //2.实现接口泛型(implements ViewBinder<MainActivity>)
                ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), ClassName.get(entry.getKey()));

                //4.方法体参数(final MainActivity target)
                ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), //参数类型(Mainactivity)
                        Constants.TARGET_PARAMETER_NAME) //参数名target
                        .addModifiers(Modifier.FINAL)
                        .build();

                //3.方法体: public void bind(final MainActivity target) {
                MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME) //bind方法
                        .addAnnotation(Override.class)  //方法注解
                        .addModifiers(Modifier.PUBLIC) //方法类型
                        .addParameter(parameterSpec);//方法创建完成

                //5.方法内容
                for (VariableElement fieldElement : entry.getValue()) {
                    //获取属性名
                    String fieldName = fieldElement.getSimpleName().toString();
                    //获取注解的值(如:R.id.tv_1)
                    int annotationValue = fieldElement.getAnnotation(BindView.class).value();
                    // target.tv1.findViewById(R.id.tv_1);
                    String methodContent = "$N." + fieldName + "=$N.findViewById($L)";
                    //加入方法内容
                    methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME, Constants.TARGET_PARAMETER_NAME, annotationValue);

                }

                if (!EmptyUtils.isEmpty(tempOnClickMap)) {
                    for (Map.Entry<TypeElement, List<ExecutableElement>> entry1 : tempOnClickMap.entrySet()) {
                        if (className.equals(ClassName.get(entry1.getKey()))) {
                            for (ExecutableElement executableElement : entry1.getValue()) {
                                String methodName = executableElement.getSimpleName().toString();
                                int[] value = executableElement.getAnnotation(OnClick.class).value();
                                for (int i = 0; i < value.length; i++) {
//                                    target.tv1.setOnClickListener(new DebouncingOnClickListener() {
//                                        @Override
//                                        public void doClick(View view) {
//                                            target.onClick(view);
//                                        }
//                                    });
                                    methodBuilder.beginControlFlow("$N.findViewById($L).setOnClickListener(new $T()",
                                            Constants.TARGET_PARAMETER_NAME,
                                            value[i],
                                            ClassName.get(clickListenerType))
                                            .beginControlFlow("public void doClick($T view)",
                                                    ClassName.get(viewType))
                                            .addStatement("$N." + methodName + ("(view)"), Constants.TARGET_PARAMETER_NAME)
                                            .endControlFlow()
                                            .endControlFlow(")")
                                            .build();
                                }
                            }
                        }
                    }
                }

                //1.生成必须是同包;(属性的修饰符是缺失的)
                JavaFile.builder(className.packageName(), //包名
                        TypeSpec.classBuilder(className.simpleName() + "$ViewBinder")  //类名
                                .addSuperinterface(typeName)  //实现ViewBinder接口
                                .addModifiers(Modifier.PUBLIC) //类的类型为public
                                .addMethod(methodBuilder.build())  //方法体
                                .build()) //类构件完成
                        .build()
                        .writeTo(filer);
            }

        }
    }

    private void valueOfMap(Set<? extends Element> bindViewElements, Set<? extends Element> onClickElements) {

        if (!EmptyUtils.isEmpty(bindViewElements)) {
            for (Element element : bindViewElements) {
                messager.printMessage(Diagnostic.Kind.NOTE, "@BindView >>> " + element.getSimpleName());
                if (element.getKind() == ElementKind.FIELD) {
                    VariableElement fieldElement = (VariableElement) element;
                    //属性节点的上层是类节点
                    TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
                    if (tempBindViewMap.containsKey(enClosingElement)) {
                        tempBindViewMap.get(enClosingElement).add(fieldElement);
                    } else {
                        List<VariableElement> fields = new ArrayList<>();
                        fields.add(fieldElement);
                        tempBindViewMap.put(enClosingElement, fields);
                    }
                }
            }
        }

        if (!EmptyUtils.isEmpty(onClickElements)) {
            for (Element element : onClickElements) {
                messager.printMessage(Diagnostic.Kind.NOTE, "@OnClick >>> " + element.getSimpleName());
                if (element.getKind() == ElementKind.METHOD) {
                    ExecutableElement executableElement = (ExecutableElement) element;
                    //属性节点的上层是类节点
                    TypeElement enClosingElement = (TypeElement) element.getEnclosingElement();
                    if (tempOnClickMap.containsKey(enClosingElement)) {
                        tempOnClickMap.get(enClosingElement).add(executableElement);
                    } else {
                        List<ExecutableElement> executes = new ArrayList<>();
                        executes.add(executableElement);
                        tempOnClickMap.put(enClosingElement, executes);
                    }
                }
            }
        }

    }
}

最后在MainActivity中调用

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_1)
    TextView tv1;
    @BindView(R.id.tv_2)
    TextView tv2;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        tv1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }

    @OnClick({R.id.tv_1,R.id.tv_2})
    void onClick(View view){
        Toast.makeText(MainActivity.this,"dianji",Toast.LENGTH_LONG).show();
    }
}

代码撸完后,make project,不出意外的话根据模板生成的java文件会出现在这个路径
在这里插入图片描述
总的来说APT的思路还是很清晰的写法比较固定,难点在于javapoet的api使用上,大家可以查看javapoet官网,掌握的秘诀就是多写多练,毕竟java文件结构还是很固定的。
最后是这个demo的地址,大家给赏个star吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用AngularJs编写的简单 益智游戏(附源代码)  这是一个简单的 javascript 项目。这是一个拼图游戏,也包含一个填字游戏。这个游戏玩起来很棒。有两个不同的版本可以玩这个游戏。你也可以玩填字游戏。 关于游戏 这款游戏的玩法很简单。如上所述,它包含拼图和填字游戏。您可以通过移动图像来玩滑动拼图。您还可以选择要在滑动面板中拥有的列数和网格数。 另一个是填字游戏。在这里你只需要找到浏览器左侧提到的那些单词。 要运行此游戏,您需要在系统上安装浏览器。下载并在代码编辑器中打开此项目。然后有一个 index.html 文件可供您修改。在命令提示符中运行该文件,或者您可以直接运行索引文件。使用 Google Chrome 或 FireFox 可获得更好的用户体验。此外,这是一款多人游戏,双方玩家都是人类。 这个游戏包含很多 JavaScript 验证。这个游戏很有趣,如果你能用一点 CSS 修改它,那就更好了。 总的来说,这个项目使用了很多 javascript 和 javascript 库。如果你可以添加一些具有不同颜色选项的级别,那么你一定可以利用其库来提高你的 javascript 技能。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值