Android APT 注解处理器(JavaPoet)

274 阅读5分钟

一、写在前面

大家在开发过程中,肯定都接触过注解,使用它可以很优雅地实现我们的功能,同时可以结合javaPoet(KotlinPoet)来解决一些重复代码的问题。

二、目标

这里我们拟定一个目标,比如我们在网络请求的时候,拿Retrofit举例,我们调用接口请求,直接以interface的形式定义好我们请求的格式,剩下的内部使用动态代理去实现了,当然动态代理是运行时的,会耗费更多的运行时资源,我们可不可以使用自定义注解的形式将它改成编译时呢?当然这里是一个目标,我们这里只介绍使用“自定义注解+javapoet”的使用,不追求达到跟Retrofit完全一样的功能。

三、实现逻辑

1、注解结构定义

我们知道一个网络请求涉及到很多东西,我们最常变动的就是业务参数,以及HEAD等,所以我们先定义两个个接口。

在此之前先说下两个注解,RetentionTarget, 一般自定义注解会加上这连个注解:

Retention

1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;

2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;

3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

Target

ElementType 这种枚举类型的常量提供了Java程序中可能出现注释的语法位置的简单分类。这些常量在java.lang.annotation.Target元注释中用于指定在何处写入给定类型的注释是合法的。注释可能出现的语法位置分为声明上下文(注释适用于声明)和类型上下文(注释适用于声明和表达式中使用的类型)。常量注释类型、构造函数、字段、局部变量、方法、包、参数、类型和类型参数与JLS 9.6.4.1中的声明上下文相对应。

public enum ElementType {
    /** 类、接口(包括注释类型)或枚举声明 */
    TYPE,
    
    /** 字段声明(包括枚举常量) */
    FIELD,
	
    /** 方法声明 */
    METHOD,

    /** 形式参数声明 */
    PARAMETER,

    /** 构造函数声明 */
    CONSTRUCTOR,

    /** 局部变量声明 */
    LOCAL_VARIABLE,

    /** 注释类型声明 */
    ANNOTATION_TYPE,

    /** 包 声明 */
    PACKAGE,

    /**
    * 类型参数声明
    *
    * @since 1.8
    */
    TYPE_PARAMETER,

    /**
    * 字体的使用
    *
    * @since 1.8
    */
    TYPE_USE
}

接下来我们自定义两个注解:

1、RequestFit

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestFit {
        String api();
        String version();

}    

这个RequestFit是用来标记哪些方法是是需要处理的,注解处理器可以根据这个注解来识别需要处理的方法,内部的方法可自定义,这里可以放一些固定参数。

2、RequestHead

@Target(METHOD)
@Retention(RUNTIME)
public @interface RequestHead {
  String[] value();
}

这个就是HEAD的自定义注解,当然,此注解可以不使用,一般说来接口还是业务参数居多。

2、请求结构定义

上述将注解的结构定义好了,接下来要定义interface,同时使用新定义的注解来描述我们的请求。

public interface ApiRequest {
    @RequestFit(api = "xxxxxaaa",version = "1.0")
    RequestParams getDetail(String name, HashMap dataParams);

    @RequestFit(api = "xxxxx",version = "1.0")
    RequestParams getListMessage(String age, HashMap dataParams);
}

这里我们先定一个ApiRequest,内部有两方法,表示有两个网络请求,我们这里只演示RequestFit,RequestHead就不演示了,使用跟RequestFit是一样的。

如上所示,以getDetail为例,我们使用了参数分别有String类型和HashMap类型,注解RequestFit表示当前是哪个api,以及对应版本号。

3、注解处理器实现

强调下,注解处理器需要单独写个Moudle,然后这个Moudle是一个java library。

导入需要的依赖

//处理器注册
implementation 'com.google.auto.service:auto-service:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'

implementation 'com.google.auto:auto-common:1.2.1'
annotationProcessor 'com.google.auto:auto-common:1.2.1'

//========注解必备(3)=========
//开源javapoet:https://github.com/square/javapoet/
//使用在线库 或者 拷贝源码库
//用于注解之后,进行的代码处理框架(比手动写效率高)
implementation 'com.squareup:javapoet:1.13.0'

这里说一下,APT其实就是基于SPI一个工具,是JDK留给开发者的一个在编译前处理注解的接口,AutoService框架的作用是自动生成SPI清单文件(META-INF/services下的文件)。不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现),但是既然有了,为啥不用呢

MyProcessor

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    /**
     * Types是一个用来处理TypeMirror的工具
     */
    private Types typesUtils;
    /**
     * Elements是一个用来处理Element的工具
     */
    private Elements elements;
    /**
     * 生成java源码
     */
    private Filer filer;
    /**
     * Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。
     * 它不是注解处理器开发者的日志工具,
     * 而是用来写一些信息给使用此注解器的第三方开发者的
     */
    private Messager messager;

    private Locale locale;
    private SourceVersion sourceVersion;
    private Map<String, String> optMap;


    //===============核心设置==================
    private Map<String, MyAnnotationClass> mAnnotatedClassMap;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        //===============核心设置==================
        typesUtils = processingEnv.getTypeUtils();
        filer = processingEnv.getFiler();

        //ProcessingEnvironment可以获取的对象
        elements = processingEnv.getElementUtils();
        messager = processingEnv.getMessager();
        locale = processingEnv.getLocale();
        sourceVersion = processingEnv.getSourceVersion();
        optMap = processingEnv.getOptions();
        //
        mAnnotatedClassMap = new TreeMap<>();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();

        //创建自定义注解处理类
        try {
            processMyView(roundEnv);
        } catch (Exception e) {
            System.out.println("异常:" + e.toString());
        }

        //将自定义注解处理类,写入文件
        for (MyAnnotationClass annotationClass : mAnnotatedClassMap.values()) {
            try {
                annotationClass.generateFiler().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    /**
     * 创建处理类,处理自定义注解
     *
     * @param roundEnv
     */
    private void processMyView(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(RequestFit.class)) {
            MyAnnotationClass annotationClass = createMyAnnotationClass(element);
            MySendField bindViewField = new MySendField(element);
            annotationClass.addField(bindViewField);

            System.out.println("processMyView annotatedClass: " + annotationClass);
            System.out.println("processMyView MySendField: " + bindViewField);
        }

    }

    /**
     * 获取每一个Element的处理类,并将生成的处理类保存到map中
     *
     * @param element
     * @return
     */
    private MyAnnotationClass createMyAnnotationClass(Element element) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        String fullName = typeElement.getQualifiedName().toString();
        System.out.println("getAnnotatedClass typeElement: " + typeElement);
        MyAnnotationClass annotationClass = mAnnotatedClassMap.get(fullName);
        //如果集合中不存在,则添加到集合中
        if (annotationClass == null) {
            //创建注解处理类
            annotationClass = new MyAnnotationClass(typeElement, elements);
            mAnnotatedClassMap.put(fullName, annotationClass);
        }
        return annotationClass;
    }


    /**
     * (3)必须重写的方法,
     * <p>
     * 处理器想要处理的自定义注解
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    /**
     * <p>
     * 将自定义的注解添加到set列表中
     * <p>
     * 给(3)getSupportedAnnotationTypes使用
     *
     * @return
     */
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(RequestFit.class);
        return annotations;
    }


    /**
     * (4)必须重写的方法:
     * <p>
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
     *
     * @return 使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


}

MyAnnotationClass

public class MyAnnotationClass {
    private ArrayList<MySendField> mFields;//自定义注解的处理集合
    private TypeElement mTypeElement;
    private Elements mElements;

    private ArrayList<MethodSpec> bindViewBuidlers;//自定义注解的处理集合

    private ArrayList<MethodSpec> netRequestBuidlers;//自定义注解的处理集合


    public MyAnnotationClass(TypeElement typeElement, Elements elements) {
        mFields = new ArrayList<>();
        bindViewBuidlers = new ArrayList<>();
        netRequestBuidlers = new ArrayList<>();
        this.mTypeElement = typeElement;
        this.mElements = elements;
    }

    /**
     * 保存有自定义注解的处理
     *
     * @param field
     */
    public void addField(MySendField field) {
        mFields.add(field);
    }


    /**
     * 核心
     * 利用开源javaPoet生成对应的.java代码
     *
     * @return
     */
    public JavaFile generateFiler() {
        try {
            //添加网络请求方法的处理解析
            for (MySendField field : mFields) {
                String dataType = field.getDataType();
                System.out.println("创建方法: " + field.getMethodName().toString()+"  "+ dataType);

                List<ParameterSpec> specList = field.getMethodParams();
                    //(1)生成java网络请求方法:
                    MethodSpec.Builder bindViewBuidler = MethodSpec.methodBuilder(field.getMethodName().toString())
                            .addModifiers(Modifier.PUBLIC)//public
                            .addAnnotation(Override.class)//接口的复写方法
                            .addParameters(specList)
                            .returns(TypeUtil.PROVIDER);

                    bindViewBuidler.addStatement("$T params =new $T()"
                            , ClassName.get(HashMap.class)
                            , ClassName.get(HashMap.class));

                    bindViewBuidler.addStatement("params.put("API",$S)"
                            , field.getApi());

                    bindViewBuidler.addStatement("params.put("VERSION",$S)"
                            , field.getVersion());

                    for (ParameterSpec spec : specList) {
                        bindViewBuidler.addStatement("params.put("$N"," + spec.name + ")"
                                , spec.name);
                    }

                    bindViewBuidler.addStatement("$T requestParams =new $T()"
                            , TypeUtil.PROVIDER
                            , TypeUtil.PROVIDER);

                    bindViewBuidler.addStatement("requestParams.setParams(params)"
                            , field.getApi());

                    bindViewBuidler.addStatement("return requestParams");
                    bindViewBuidlers.add(bindViewBuidler.build());    
            }

            System.out.println("创建方法完事: " + "");

            //(3)生成java的类文件(.java的文件)
            TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "Impl")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(TypeName.get(mTypeElement.asType()))//类实现的接口名
                    .addMethods(bindViewBuidlers)
                    .build();
            //添加包名
            String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
            //JavaFile
            JavaFile result = JavaFile.builder(packageName, injectClass).build();


        //将打印也写入
            result.writeTo(System.out);

            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }


    /**
     * 反射调用对应的接口,需要的接口位置,包名:com.test.lib
     */
    private static class TypeUtil {
        static final ClassName PROVIDER = ClassName.get("com.test.testrequestfit", "RequestParams");
    }



}

MYSendField

public class MYSendField {
    private ExecutableElement mVariableElement;
    private String api;
    private String version;

    private String dataType;

    public MYSendField(Element element) {
        //排错
        if (element.getKind() != ElementKind.METHOD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
                    RequestFit.class.getSimpleName()));
        }
        System.out.println("MySendField before mVariableElement: " + element);
        mVariableElement = (ExecutableElement) element;
        System.out.println("MySendField after mVariableElement: " + mVariableElement);
        //获取自定义注解
        RequestFit requestFit = mVariableElement.getAnnotation(RequestFit.class);

        RequestDataType requestDataType = mVariableElement.getAnnotation(RequestDataType.class);
        if(null !=requestDataType) {
            dataType = requestDataType.type();
        }

        //拿到控件的id
        api = requestFit.api();
        version = requestFit.version();
        System.out.println("BindViewField mResId: " + api);
        if (null == api) {
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", RequestFit.class.getSimpleName(),
                            mVariableElement.getSimpleName()));
        }
    }

    public String getDataType() {
        return dataType;
    }

    public Name getMethodName() {
        return mVariableElement.getSimpleName();
    }

    public ExecutableElement getmVariableElement() {
        return mVariableElement;
    }

    public List<ParameterSpec> getMethodParams() {
        List<ParameterSpec> specList = new ArrayList<>();
            for (VariableElement element: mVariableElement.getParameters()) {
                specList.add(ParameterSpec.builder(TypeName.get(element.asType()), element.getSimpleName().toString())
                        .addModifiers(element.getModifiers())
                        .build());
            }
    }

    public String getVersion() {
        return version;
    }

    public String getApi() {
        return api;
    }

    /**
     * 获取变量类型
     *
     * @return
     */
    TypeMirror getFieldType() {
        return mVariableElement.asType();
    }
}

上述类中最主要逻辑在MyProcessorprocessMyView方法中,就是通过getElementsAnnotatedWith来获取我们自定义注解RequestFit标记的方法,然后根据typeElement.getQualifiedName() 来归属生成的类,因为我们也可以定义多个interface。Map对应的value是一个MyAnnotationClass对象,这个对象内部维护一个MySendField列表,每个MySendField对应一个注解标记的方法,即每个注解的Element都被收集好了,最后在MyProcessor的process方法中逐个annotationClass.generateFiler().writeTo(filer) 使用javaPoet生成java文件,写入本地。

细节分析

这里注重说下javapoet的使用流程,上述说了MyAnnotationClass中存放了MySendField的列表,接下来都是根据这个来生成对应代码。

1、定义生成的方法

 MethodSpec.Builder bindViewBuidler 
     = MethodSpec.methodBuilder(field.getMethodName().toString())
    	.addModifiers(Modifier.PUBLIC)//public
        .addAnnotation(Override.class)//接口的复写方法
        .addParameters(specList)
        .returns(TypeUtil.PROVIDER);

  static final ClassName PROVIDER = ClassName.get("com.test.testrequestfit", "RequestParams");

field为循环的一个MySendField对象, 我们定义了一个RequestParams, 用于存放所有参数,返回,然后用公共的请求方法来调用。

2、填充参数

//创建Hash对象
bindViewBuidler.addStatement("$T params =new $T()", ClassName.get(HashMap.class), ClassName.get(HashMap.class));
//填充RequestFit注解的api
bindViewBuidler.addStatement("params.put("API",$S)", field.getApi());
//填充RequestFit注解的version
bindViewBuidler.addStatement("params.put("VERSION",$S)", field.getVersion());

//循环填充 注解标记方法的参数
for (ParameterSpec spec : specList) {
    bindViewBuidler.addStatement("params.put("$N"," + spec.name + ")", spec.name);
}

// 创建RequestParams对象
bindViewBuidler.addStatement("$T requestParams =new $T()", TypeUtil.PROVIDER, TypeUtil.PROVIDER);
//将上述的参数填充进requestParams对象中
bindViewBuidler.addStatement("requestParams.setParams(params)", field.getApi());
//返回这个对象
bindViewBuidler.addStatement("return requestParams");

这里就是根据每个MySendField, 生成填充参数的过程,简单说下各种Spec

JavaPoet的常用类

类名备注
TypeSpec用于生成类、接口、枚举对象的类
MethodSpec用于生成方法对象的类
ParameterSpec用于生成参数对象的类
AnnotationSpec用于生成注解对象的类
FieldSpec用于配置生成成员变量的类

在JavaPoet中,format中存在三种特定的占位符:

备注
$T在JavaPoet代指的是TypeName,该模板主要将Class抽象出来,用传入的TypeName指向的Class来代替
$N代指的是一个名称,例如调用的方法名称,变量名称,这一类存在意思的名称
$S和String.format中%s一样,字符串的模板,将指定的字符串替换到S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用L

来源链接:juejin.cn/post/700072…

上述specList就是根据给个注解标记得方法对应的ExecutableElement,通过getParameters()方法,取到VariableElement

for (VariableElement element: mVariableElement.getParameters()) {
    specList.add(ParameterSpec.builder(TypeName.get(element.asType()), element.getSimpleName().toString())
                 .addModifiers(element.getModifiers())
                 .build());           
}

就可以得到该标记方法所有的参数对应的ParameterSpec列表。

关于java的Element,有以下类型:

名称描述
VariableElement变量元素,表示字段、 enum常量、方法或构造函数参数、局部变量、资源变量或异常参数
ExecutableElement可执行元素,表示类或接口的方法、构造函数或初始值设定项(静态或实例),包括注解类型元素
TypeElement类型元素,表示一个类或接口程序元素。 提供对有关类型及其成员的信息的访问。 请注意,枚举类型是一种类,注解类型是一种接口
PackageElement包元素,表示包程序元素。 提供对有关包及其成员的信息的访问
Parameterizable可参数化的,具有类型参数的元素的混合接口
TypeParameterElement类型参数化元素,表示泛型类、接口、方法或构造函数元素的形式类型参数
QualifiedNameable限定可命名的

我们上面用到了ExecutableElement,就是对应被标记的方法,对应每个参数就是一个VariableElement。

3、生成对应的Java类

//(3)生成java的类文件(.java的文件)
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "Impl")
    .addModifiers(Modifier.PUBLIC)
    //                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))//类实现的接口名
    .addSuperinterface(TypeName.get(mTypeElement.asType()))//类实现的接口名
    .addMethods(bindViewBuidlers)
    .build();
//添加包名
String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
//JavaFile
JavaFile result = JavaFile.builder(packageName, injectClass).build();


//将打印也写入
result.writeTo(System.out);

就是生成一个mTypeElement名+"Impl"的类,该类实现了这个mTypeElement对应的接口,添加包名,并写入。

四、结语

对比一下编写的文件以及生成的文件

ApiRequest.java

ApiRequestImpl.java

可以看到这个已经成功了,接下来就是在请求网络接口调用时,填入上述生成的对应方法,当然还需要写一套请求的逻辑,将参数与okHttp关联起来。本文中例子是用来生成重复代码的。后续网络请求库的封装就不叙述了。总之完成后,每次请求代码调用类似于 :

NetWorkUtils.request(ApiRequestImpl.getDetail("xxx",{"age":"11"}),new RequestCallBack(){
    @Override
    void success(MyResponse response){
        
    }

    @Override
    void error(Response response){
        
    }
},MyResponse.class);