首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >apt 与 JavaPoet 自动生成代码

apt 与 JavaPoet 自动生成代码

原创
作者头像
吴涛
修改2017-10-26 09:41:52
4.4K0
修改2017-10-26 09:41:52
举报
文章被收录于专栏:吴涛的专栏吴涛的专栏

前言

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

本文通过介绍腾讯视频项目中,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接口的继承结构相对比较复杂:

[1508900053643_8070_1508900092231.png]
[1508900053643_8070_1508900092231.png]

一些已知的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"

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

[1508900338448_4534_1508900377021.png]
[1508900338448_4534_1508900377021.png]

在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文件

[1508900586779_2197_1508900625254.png]
[1508900586779_2197_1508900625254.png]

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

com.example.ViewTypeProcessor

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

[1508900608968_8531_1508900647441.png]
[1508900608968_8531_1508900647441.png]

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

[1508900620097_4858_1508900658572.png]
[1508900620097_4858_1508900658572.png]

应用

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

[1508900634163_2970_1508900672814.png]
[1508900634163_2970_1508900672814.png]

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方法中的合适位置下断点:

[1508900707886_4916_1508900746545.png]
[1508900707886_4916_1508900746545.png]

下断点的方法与平常调试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:

[1508900731628_7034_1508900770128.png]
[1508900731628_7034_1508900770128.png]
[1508900743976_6437_1508900782462.png]
[1508900743976_6437_1508900782462.png]

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

4:、Debug AnnotationProcessor:

[1508900753173_3782_1508900791654.png]
[1508900753173_3782_1508900791654.png]

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

[1508900762648_5113_1508900801144.png]
[1508900762648_5113_1508900801144.png]

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

[1508900773301_478_1508900811828.png]
[1508900773301_478_1508900811828.png]

结语

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • android-apt
  • Annotation Processing Tool
  • Element 和 TypeMirror
  • JavaPoet
  • 新建AutoTypeBinding工程
  • 注册处理器
  • 应用
  • 如何调试?
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档