apt 与 JavaPoet 自动生成代码

前言

在你的工程中,是否有一些文件代码具有配置化,模板化的特点,这些代码不再有逻辑上的变动,只是随着业务的发展,重复的堆叠。当你在这个文件中新增一行配置时,内心是否心生抗拒,思考过这行配置是否可以不用人工来添加,让你从机械重复的劳动中解放出来呢?

本文通过介绍腾讯视频项目中,adapter创建View的例子,向大家介绍,如何通过自定义注解处理器自动生成代码,以及如何调试自定义注解处理器。首先,介绍一下我们工程中,Adapter是如何创建View的。

onCreateViewHolder方法中,将viewType传递给ONAViewTools.getONAView的静态方法,返回指定的View。

@Override
public RecyclerView.ViewHolder onCreateInnerViewHolder(ViewGroup viewGroup, int viewType) {
    View convertView;
    convertView = (View) ONAViewTools.getONAView(viewType, mContext);
    return new RecycleViewItemHolder(convertView);
}

在我们的工程中,频道页上所有的view,都是通过ONAViewTools这个工具类创建出来的。

ONAViewTools类:

public static IONAView getONAView(int viewType, Context context) {
    .........
    return createONAView(viewType, context);
}

这里省略了一些细节。

createONAView方法:

 public static IONAView createONAView(int viewType, Context context) {
        try {
            if (context != null) {
                switch (viewType) {
                    case EONAViewType._EnumONAMultPoster:
                        return new ONAMultPosterView(context);
                    case EONAViewType._EnumONAGalleryPoster:
                        return new ONAGalleryPosterView(context);
                        .......此处省略了很多条case:
                        return new ONASplitLineView(context);
                    case EONAViewType._EnumONAStarList:
                        return new ONAStarListView(context);
                    case EONAViewType._EnumONANewsItem:
                        return new ONANewsItemView(context);
                 }
            }
      }catch(Exception e){

       }
}

那么,当我们新增一种View的时候,套路已经清晰了。当我们新增一种ONAXXXView时,要经历以下几个步骤:

  1. 定义ViewType 常量XXX_View_Type。
  2. 新建ONAXXXView.java 文件,编写Ui代码。
  3. 在ONAViewTools.java文件的createONAView方法中新增一条配置。

现在,我们就开始说明,如何自动化的在ONAViewTools中新增配置。当然,你可能觉得,每次在ONAViewTools中手动新增一条配置也没花多少时间。确实,我这里只是拿来举例子,配合讲明白这篇文章的主题。并且本文将通过新工程的方式讲解,而不是基于腾讯视频的工程。

首先,介绍一下需要用到的基础知识。

android-apt

android-apt是Android Studio中一款用来辅助处理编译时注解的Gradle插件。不知注解为何物的同学可以先下去补补课。Github上非常著名的EventBus、ButterKnife、Retrofit等优秀开源库都使用了这个插件,它们都是基于编译时注解实现的框架。

Annotation Processing Tool

Annotation Processing Tool 是jdk5.0之后提供的用于编译期处理注解的api组件,简称apt。主要包括两大部分:

1、用于模型化Java 程序语言结构的模型化api,包括com.sun.mirror包下的mirror api,javax.lang.model包下的element api 及其他辅助工具类。

2、javax.annotation.processing包下用于编写注解处理器的注解处理api。

Element 和 TypeMirror

Element代表java源文件中的程序构建元素,例如包、类、方法等。Element接口有5个子类。

PackageElement

包程序元素

TypeElement

类、接口、注解、枚举元素

VariableElement

方法参数、成员变量、局部变量、枚举常量、异常参数

ExecutableElement

方法、构造函数、静态代码块

|TypeParameterElement 类、接口、方法、或构造方法的泛型参数|

TypeMirror 用于描述Java程序中元素的信息,即Element 的元信息。通过Element.asType()接口可以获取Element的TypeMirror。TypeMirror接口的继承结构相对比较复杂:

一些已知的TypeMirror的释意:

PrimitiveType

原始数据类型,boolean,byte,short int,long,float,char,double

ReferenceType

引用类型

ArrayType

数组类型

DeclaredType

声明的类型,例如类、接口、枚举、注解类型

AnnotationType

注解类型

ClassType

类类型

EnumType

枚举类型

InterfaceType

接口类型

TypeVariable

类型变量类型

VoidType

void 类型

WildcardType

通配符类型

当TypeMirror是DeclaredType或者TypeVariable时,TypeMirror可以转化成Element:

Element element = processingEviroment.getTypeUtils().asElement(typeMirror);

JavaPoet

JavaPoet是一组用来生成 .java文件的JAVA API。正如其名,当你创建.java文件时,你将不用再处理代码换行、缩进、引用导入等枯燥而又容易出错的工作,这一切JavaPoet都将能够很好地为你完成,你的工作将变得富有诗意。

TypeSpec、ParameterSpec、MethodSpec、CodeBlock、JavaFile都是JavaPoet提供的用于描述一个源文件元素的类。TypeSpec代表了一个接口、类、注解、枚举的定义,ParameterSpec代表一个成员变量、函数参数的定义,MethodSpec代表了方法的定义,CodelBlock用于描述一段代码块,JavaFile表示源文件本身。一个java文件正式通过以上几种类型的嵌套、组合,最终描述成一个完整的java源文件。JavaPoet为每种元素,都提供了相应的Builder类。

JavaPoet提供了一套自定义的字符串格式化规则。常用的有$L、$S、$T、$N:

格式化规则

表示

$L

字面量

$S

字符串

$T

类、接口

$N

变量

介绍完基础知识,下面我们通过新建工程的方式,一步步讲解:

新建AutoTypeBinding工程

1 .新建工程AutoTypeBinding。

2 .新建viewtypebinder model,选择java library,该model中,提供注解ViewType的定义:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ViewType {
    int value() default -1;
}

3 .新建apt_compiler model,选择java library,该model 中新建ViewTypeProcessor类,该类需继承annotation.processing.AbstractProcessor类。

在apt_compiler model 的build.gradle中,添加如下配置:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':viewtypebinder')
    compile 'com.squareup:javapoet:1.7.0'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

现在,你的工程结构应该如下图所示:

在ViewTypeProcessor中编写注解处理的代码:

@SupportedAnnotationTypes("com.example.ViewType")
@SupportedOptions({"fileName","failedView"})
public class ViewTypeProcessor extends AbstractProcessor {

    private Messager mMessager;
    private static final String GENERATED_FILE_NAME = "fileName";
    private static final String FAILED_VIEW = "failedView";
    private String mFileName;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("process");
        Collection<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(ViewType.class);
        List<TypeElement> types = ElementFilter.typesIn(annotatedElements);
        if(types == null || types.isEmpty()){
            return false;
        }
        mFileName = processingEnv.getOptions().get(GENERATED_FILE_NAME);
        if(mFileName == null || mFileName.isEmpty()){
            mMessager.printMessage(Diagnostic.Kind.WARNING, "No option generatedFileName passed to annotation processor");
            return false;
        }
        //解析failedView参数 -------- ①
        ClassName failedView = null;
        String failedViewName = processingEnv.getOptions().get(FAILED_VIEW);
        try {
            if(failedViewName != null && !failedViewName.isEmpty()){
                failedView = ClassName.bestGuess(failedViewName);
            }
        }catch (IllegalArgumentException e){
            mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(Locale.getDefault(), "Option is invalid: %s", failedViewName));
            throw  new AbortProcessViewTypeException(e);
        }
        TypeElement failedViewTypeElement = processingEnv.getElementUtils().getTypeElement(failedViewName);
        checkTypeVaild(failedViewTypeElement);

        ClassName className = null;
        try {
            className = ClassName.bestGuess(mFileName);
            System.out.println("Generate file name: " + mFileName);
        }catch (IllegalArgumentException e){
            mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(Locale.getDefault(), "Option is invalid: %s", mFileName));
            RuntimeException processException = new AbortProcessViewTypeException();
            processException.initCause(e);
            throw processException;
        }
        ClassName View = ClassName.bestGuess("android.view.View");
        ClassName Context = ClassName.bestGuess("android.content.Context");
        //构建类
        TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addJavadoc("This file is generated by apt, please do not modify!");
        ParameterSpec paramsContext = ParameterSpec.builder(Context, "context").build();
        ParameterSpec typeParamSpec = ParameterSpec.builder(TypeName.INT, "type").build();
        //构建方法 public static final createView(Context context, int type)
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("createView")
                .addModifiers(Modifier.FINAL, Modifier.PUBLIC, Modifier.STATIC)
                .returns(View)
                .addParameter(paramsContext)
                .addParameter(typeParamSpec);
        //如果指定了failedView,则如果创建View发生异常时,返回failedView
        if(failedView != null){
             methodBuilder.beginControlFlow("try");
        }
        //构建switch(type){
        //            }
        //代码块
        CodeBlock.Builder caseBlock = CodeBlock.builder().beginControlFlow("switch($N)", typeParamSpec);
        //循环遍历所有被注解元素,每项元素都会对应生成一条case 语句
        for (TypeElement type : types){
            ViewType viewType = type.getAnnotation(ViewType.class);
            if(viewType == null){
                continue;
            }
            checkTypeVaild(type);
            int value = viewType.value();
            ClassName viewName = ClassName.get(type);
            System.out.println("viewName = " +viewName.simpleName() + ", value = " + value);
            caseBlock.add("case $L:\n", value).indent().addStatement("return new $T($N)", viewName, paramsContext).unindent();
        }
        //没有匹配到的情况下,返回null
        caseBlock.add("default:\n").indent().addStatement("return null", View, paramsContext).unindent();
        caseBlock.endControlFlow();
        methodBuilder.addCode(caseBlock.build());
        if(failedView != null) {
            methodBuilder.nextControlFlow("catch($T t)", Throwable.class);
            methodBuilder.addStatement("return new $T($N)", failedView, paramsContext);
            methodBuilder.endControlFlow();
        }
        //构建文件
        JavaFile javaFile = JavaFile.builder(className.packageName(), typeBuilder.addMethod(methodBuilder.build()).build()).build();
        //写文件
        writeSourceFile(mFileName, javaFile.toString());
        return false;
    }

process方法通过遍历所有被@ViewType注解的View,生成一个由mFileName指定的java文件,该文件中包含一个静态方法public static final View createView(Context context, int type)的方法 ,该方法体由一个switch语句根具type的值创建并返回不同类型的View。

apt提供了@SupportedAnnotationTypes、@SupportedSourceVersion、@SupportOptions三个注解分别用来注明该Processor文件支持的注解类型,支持的java版本,和支持的输入参数。你也可以通过覆写AbstractProcessor的getSupportedAnnotationTypes(),getSupportedSourceVersion(),getSupportOptions()方法来指定。可以看到,我们通过@SupportedAnnotationTypes注解描述了ViewTypeProcessor需要处理的注解类是 com.example.ViewType,该注解处理器需要指定的输入参数有fileName和failedView,fileName指定了生成java文件的名称,failedView指定了当创建View发生异常时,需要返回一个默认View,在debug模式下,这很有用,比如,你在listView上看到了一个failedView,表明该位置position创建View失败了。

在void init(ProcessingEnviroment processingEnv)方法中,为了方便我们向控制台输出日志,我们将Messager保存起来。apt工具初始化processor时,会回调init方法, processingEnv是apt向processor传递的编译环境参数,processingEnv向processor提供了访问apt编译环境的工具集,比如,通过processingEvn.getMessager()可以获得向控制台报告错误、警告、提示的工具,通过processingEvn.getFiler()可以获得创建java源文件的工具。

接下来,我们来看process方法。process方法可能会被apt工具多次调用,,apt初始化的时候,会调用一次process方法。在第一次调用时,apt编译器会将整个工程作为输入,收集到所有被ViewType注解的元素,然后同过process方法的参数annotations传递给process方法处理。如果在某轮process处理中,process生成了新的java文件,则apt编译器会将新生成的java文件作为输入,然后收集到新的被注解的元素,直到不再产生新的文件后,process循环调用结束。注意,当没有新的文件生成后,process还会被再调用一次,此次输入是空的。

round

input

output

1

整个项目

A.java

2

A.java

none

3

none

none

在代码①处,我们解析输入参数FAILED_VIEW,如过FAILED_VIEW被指定,则会尝试通过ClassName这个类的bestGuess方法,这个方法接受一个字符串failedViewName,返回一个ClassName failedView,failedView完整的描述了failedViewName代表的类名称、包名称等信息。如果failedViewName格式不合法,bestGuess会抛出IllegalArgumentException。

下面看这两行代码:

	TypeElement failedViewTypeElement = processingEnv.getElementUtils().getTypeElement(failedViewName);
	checkTypeVaild(failedViewTypeElement);

通过processingEvn的getElementUtis()方法获取操作程序元素Element的工具类Elements,并通过getTypeElement()方法返回由failedViewName指定的TypeElement 元素。checkTypeVaild()会校验这个元素是否合法,如果不合法,chechTypeVaild会抛出异常,终止process处理。

    private void checkTypeVaild(TypeElement type) {
        if(type.getKind() != ElementKind.CLASS){
            reportErrorWithAbort("The annotation @ViewType only applies to classes", type);
        }

        if(!ancestorIsView(type)){
           reportErrorWithAbort("The annotation @ViewType only applies to Views", type);
        }
        if(!isVisible(type)){
            reportErrorWithAbort("cannot resolve symbol " + type.getQualifiedName(), type);
        }
    }

checkTypeVaild所接受的元素必须是类,而且必须继承自android.view.View,并且必需相对生成的java文件可见,也就是生成的java文件必须对type所表示的类具有访问权限。

元素type是否继承自View:

    private boolean ancestorIsView(TypeElement type) {
        while (true){
            TypeMirror parentMirror = type.getSuperclass();
            if(parentMirror.getKind() == TypeKind.NONE){
                return false;
            }
            TypeElement parentElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentMirror);
            if(parentElement.getQualifiedName().contentEquals("android.view.View")){
                return true;
            }
            type = parentElement;
        }
    }

元素是否可见:

    private boolean isVisible(TypeElement type){
        String myPackage = ClassName.bestGuess(mFileName).packageName();
        boolean isNestClass = type.getEnclosingElement().getKind() == ElementKind.CLASS;
        Set<Modifier> modifiers = type.getModifiers();
        if(isNestClass && !modifiers.contains(Modifier.STATIC)){
            return false;
        }
        if(modifiers.contains(Modifier.PUBLIC)){
            return true;
        }
        if(ClassName.get(type).packageName().contentEquals(myPackage)){
            return true;
        }
        return false;
    }

type.getEnclosingElement()返回包裹type的最里层元素,如果该元素恰巧是一个类,那么type就是一个内部类。type.getModifiers()返回该元素的访问权限修饰符,Modifiers是java反射包中提供的类,定义了PRIVATE,PUBLIC等常量,分别对应private、public修饰符。如果type是一个内部类,则其必须是一个静态类。其次,如果type是一个public类,则可以访问,否则,看type是否和mFileName指定的java文件是否在同一个包下。

TypeSpec、ParameterSpec、MethodSpec、CodeBlock、JavaFile都是JavaPoet提供的用于描述一个源文件元素的类。TypeSpec代表了一个接口、类、注解、枚举的定义,ParameterSpec代表一个成员变量、函数参数的定义,MethodSpec代表了方法的定义,CodelBlock用于描述一段代码块,JavaFile表示源文件本身。一个java文件正式通过以上几种类型的嵌套、组合,最终描述成一个完整的java源文件。JavaPoet为每种元素,都提供了相应的Builder类。

        JavaFile javaFile = JavaFile.builder(className.packageName(), typeBuilder.addMethod(methodBuilder.build()).build()).build();
        writeSourceFile(mFileName, javaFile.toString());

当javaFile构建好后,则通过writeSourceFile()方法,生成源文件。

    private void writeSourceFile(String fileName, String s) {
        try {
            JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(fileName);
            Writer writer =fileObject.openWriter();
            try {
                writer.write(s);
            }finally {
                try {
                    writer.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }catch (IOException e){
            mMessager.printMessage(Diagnostic.Kind.WARNING, "could not write class " + fileName);
        }
    }

通过processingEnv.getFiler()会返回一个Filer接口,Filer的createSrouceFile方法可以创建java文件对象fileObject,这样,我们用可以将javaFile生成的字符串写到文件中去了。为什么这里需要通过processingEnv提供的Filer接口来写文件呢,我们完全可以通过自己new File()的方式创建文件呀?答案是确实可以,但是这样,apt就无法感知有新的源文件创建了。

注册处理器

ViewTypeProcessor文件代码编写完后,还有一件非常重要的事情,就是注册ViewTypeProcessor,这样javac编译器才能在编译的时候找到正确的注解处理器处理注解。

  1. 在apt-compiler model的src/main 下新建resources/META-INF/services目录
  2. 在META-INF下继续新建javax.annotation.processing.Processor文件

3 .在文件中新增一下语句:

com.example.ViewTypeProcessor

以上配置过程也可通过引入插件自动完成。AutoService是google提供已一款可以自动生成jar包配置的插件。首先在apt-compiler build.gradle文件下,添加如下红框中依赖:

然后再ViewTypeProcesspor上新增如下注解:

应用

现在,我们来编写我们的主工程,来测试我们的apt_compiler处理器。工程结构如下所示:

MyNameView.java定义如下:

@ViewType(MyViewTypes.MY_NAME_VIEW)
public class MyNameView extends TextView {
    public MyNameView(Context context) {
        this(context, null, 0);
    }

    public MyNameView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyNameView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        setText("吴涛");
        setTextColor(Color.BLACK);
        setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
        setGravity(Gravity.CENTER);
    }

}

FailedView.java

public class FailedView extends View {

    public FailedView(Context context) {
        super(context);
    }

    public FailedView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FailedView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

app builg.gradle:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'  //引入android-apt插件

android {
  .................

}

apt {
    arguments {
        fileName "com.example.wutao.autotypebinding.ViewTypeTools"  //注解处理器生成的java文件名
        failedView "com.example.wutao.autotypebinding.view.FailedView"      //指定异常View
    }
}

dependencies {
    ........
    ........
    provided project(":viewtypebinder") 
    apt project(':apt_compiler')
}

编译后,成功在app/build/generated/source/apt/debug/com/example/wutao/autotypebinding/目录下生成ViewTypeTools.java文件:

package com.example.wutao.autotypebinding;

import android.content.Context;
import android.view.View;
import com.example.wutao.autotypebinding.view.FailedView;
import com.example.wutao.autotypebinding.view.MyNameView;
import java.lang.Throwable;

/**
 * This file is generated by apt, please do not modify! */
public final class ViewTypeTools {
  public static final View createView(Context context, int type) {
    try {
      switch(type) {
        case 11:
          return new MyNameView(context);
        default:
          return null;
      }
    } catch(Throwable t) {
      return new FailedView(context);
    }
  }
}

现在,我们可以在MainActivity.java中引用该类了:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View view = ViewTypeTools.createView(this, MyViewTypes.MY_NAME_VIEW);
        ViewGroup root = findViewById(R.id.root);
        root.addView(view);
    }
}

如何调试?

也许在我们开发注解处理器的时候,还需要单步调试,以便我们寻找注解处理器的漏洞。下面就向大家介绍,如何调试我们刚才开发的ViewTypeProcessor注解处理器。

1、在process方法中的合适位置下断点:

下断点的方法与平常调试android代码并无区别。

2、在项目的根目录下的gradle.properties文件中,新增如下配置:

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
org.gradle.daemon=true

3、新建remote debugger:

注意新建remoteDebuger的名称一定要是AnnotationProcessor 。

4:、Debug AnnotationProcessor:

现在,我们看到断点已经生效:

5、执行CompilerDebugWithJavac任务,命中断点:

结语

本文通过Adapter中使用工具类创建View的例子,一步一步讲解了如何通过自定义注解处理器,如何使用javaPoet提供的api,以及如何使用android-apt插件,以自动化的方式来生成工具类文件代码,从而提高编码效率。另外,本文还讲解了如何配置虚拟机参数,来调试逻辑稍复杂的自定义注解处理器。

现在有越来越多的开源项目在使用apt,apt的强大之处可见一斑,因此作为一个android开发者,我们有必要去了解这门技术。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码匠的流水账

spring security动态配置url权限

对于使用spring security来说,存在一种需求,就是动态去配置url的权限,即在运行时去配置url对应的访问角色。这里简单介绍一下。

652
来自专栏青枫的专栏

day52_BOS项目_04

第一步:导入pinyin4j-2.5.0.jar包,拷贝PinYin4jUtils.java工具类至utils包中 第二步:测试类代码如下:

502
来自专栏刘望舒

自定义注解和解析器实现ButterKnife

相信绝大部分的Android开发者都曾使用过ButterKnife, 利用ButterKnife开发者可以快速的实现实体view与xml的绑定,此外还能绑定各种...

3456
来自专栏lzj_learn_note

阿里ARouter使用及源码解析(一)

在app的开发中,页面之间的相互跳转是最基本常用的功能。在Android中的跳转一般通过显式intent和隐式intent两种方式实现的,而Android的原生...

542
来自专栏haifeiWu与他朋友们的专栏

美团外卖开源路由框架 WMRouter 源码分析

上周四美团外卖技术团队开源了一个 Android Router 的框架: WMRouter,博客详细介绍了用法以及设计方案,还不熟悉的同学可以先去看一下。本篇博...

822
来自专栏IT杂记

Mapreduce 任务提交源码分析1

提交过程 一般我们mapreduce任务是通过如下命令进行提交的 $HADOOP_HOME/bin/hadoop jar $MR_JAR $MAIN_CLASS...

2086
来自专栏用户2442861的专栏

Spring+Mybatis+SpringMVC后台与前台分页展示实例(附工程)

      林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka

592
来自专栏MelonTeam专栏

ViewPager与Fragment那些事儿

本文会讲解: 1.viewPager与Fragment使用过程中,偶现页面混乱问题的可能原因以及解决方案。 2.notifyDataSetChange方法在v...

1868
来自专栏Android常用基础

Rxjava2-小白入门(三)

继续上篇的Rxjava2的入门实例,把剩下的运用Rxjava的实例讲下,首先要说名下本文会用到Rxbinding的知识,他相当于Rxjava的辅助工具,在引入他...

812
来自专栏待你如初见

Java爬虫及分布式部署

import java.util.concurrent.ExecutorService;

1145

扫码关注云+社区