组件化技术要点之AnnotationProcessor

概述

Annotation Proccessor注解处理器在android 开源项目中应用广泛,比如大神JakeWharton的ButterKnife。它通过在编译期扫描注解,生成模板类,运行时通过反射调用生成的模板类,以解耦项目,减少模板代码。在项目进行组件化时,页面路由是一个比较核心的问题,从ARouter到JIMU都采用了Annotation Proccessor来实现页面路由。

下边将通过一个简单的自定义Annotation Proccessor例子,来帮你快速掌握这项技能。

AbstractProcessor 及常用套路

AbstractProcessor

AbstractProcessor 主要重写4个函数:

  1. ***getSupportedAnnotationTypes()***:获取要处理的注解的全类名集合
  2. ***getSupportedSourceVersion()***:支持的JDK版本,一般返回SourceVersion.latestSupported()即可
  3. ***init(ProcessingEnvironment processingEnvironment)***: 通过传入的ProcessingEnvironment参数,获取一些工具类
  4. ***process(Set set, RoundEnvironment roundEnvironment)***:处理注解的入口,也是最重要的方法

另外,注解处理器要起作用,其jar包需要符合一定的结构,这样系统才能识别,为了方便使用一般会在注解处理器类上添加@AutoService({Processor.class})注解,这个注解的作用就是自动生成对应的结构。 这个注解是google的auto-service里边的。

常用套路

自定义注解处理器时,从项目工程结构是上来说,一般会分为三个部分:

  1. xxx-annotation:java-library工程,存放自定义的注解类
  2. xxx-anno-processor:java-library工程,生成源代码,一般会依赖google的auto-service和square的javapoet
  3. xxx: android-library工程,1和2配合只是生成了模板代码,还没有运行时的执行入口,这个moudle的主要功能就是提供执行入口。

例子

这个例子主要实现类似butterKnife的自动findViewById操作功能,名字叫BindKnife。

1.创建注解工程java-library工程:bindknife-annotation

注解类:

package com.github.eklir.androidproccessor.bindknife.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface AutoBind {
    int value();
}

常量类:

package com.github.eklir.androidproccessor.bindknife.constants;

public class BindKnifeConstants {
    //生成的模板类的后缀
    public static final String BIND_SUFFIX = "$$BindKnife$$AutoBind";
}

gradle配置:

apply plugin: 'java-library'

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

sourceCompatibility = "8"
targetCompatibility = "8"

2.创建注解处理器java-library工程:bindknife-anno-processor

package com.github.eklir.androidprocessor.bindknife.proccessor;

import com.github.eklir.androidproccessor.bindknife.annotation.AutoBind;
import com.github.eklir.androidproccessor.bindknife.constants.BindKnifeConstants;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;


@AutoService({Processor.class})
public class BindProccessor extends AbstractProcessor {
    private static final String TAG = "BindKnife";
    private Map<TypeElement, List<Element>> parentAndChildren = new HashMap<>();
    private Filer filer;
    private Elements elementUtils;
    private Messager messager;
    private Types typeUtils;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> retVal = new HashSet<>();
        retVal.add("com.github.eklir.androidproccessor.bindknife.annotation.AutoBind");
        return retVal;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        typeUtils = processingEnvironment.getTypeUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> autoBindElements = roundEnvironment.getElementsAnnotatedWith(AutoBind.class);
        if (autoBindElements == null || autoBindElements.isEmpty()) {
            return false;
        }
        try {
            categories(autoBindElements);
            generateHelper();
        } catch (Exception e) {
            messager.printMessage(Diagnostic.Kind.ERROR, "generateHelper failed:"+ e.getMessage());
        }
        return true;
    }

    private void generateHelper() throws IOException {
        if (!parentAndChildren.isEmpty()) {
            Set<Map.Entry<TypeElement, List<Element>>> entries = parentAndChildren.entrySet();
            for (Map.Entry<TypeElement, List<Element>> entry : entries) {
                //class
                TypeElement parent = entry.getKey();
                //filed
                List<Element> fileds = entry.getValue();
                String parentName = parent.getQualifiedName().toString();
                int lastDotIndex = parentName.lastIndexOf('.');
                String packageName = parentName.substring(0, lastDotIndex);
                //定义方法的参数
                ParameterSpec paramsSpec = ParameterSpec.builder(TypeName.OBJECT, "target").build();
                //定义方法体
                MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(TypeName.VOID)
                        .addParameter(paramsSpec)
                        .addStatement("$T act = ($T) target", ClassName.get(parent), ClassName.get(parent));
                //对所有的字段赋值
                for (Element filed : fileds) {
                    TypeMirror typeMirror = filed.asType();
                    String fieldName = filed.getSimpleName().toString();
                    AutoBind annotation = filed.getAnnotation(AutoBind.class);
                    injectMethodBuilder.addStatement("act." + fieldName + " = ($T)act.findViewById(" + annotation.value() + ")", ClassName.get(typeMirror));
                }
                MethodSpec injectMethodSpec = injectMethodBuilder.build();
                //定义类文件
                TypeSpec helper = TypeSpec.classBuilder(parent.getSimpleName() + BindKnifeConstants.BIND_SUFFIX)
                        .addSuperinterface(ClassName.get("com.github.eklir.android.bindknife", "IKnife"))
                        .addMethod(injectMethodSpec)
                        .build();
                //生成类文件
                JavaFile.builder(packageName, helper).build().writeTo(filer);
            }
        }

    }

    /**
     * 按类对注解的字段分组
     *
     * @param autoBindElements
     * @throws IllegalAccessException
     */
    private void categories(Set<? extends Element> autoBindElements) throws IllegalAccessException {
        for (Element element : autoBindElements) {
            //字段的封装类型是类;
            TypeElement parent = (TypeElement) element.getEnclosingElement();
            if (element.getModifiers().contains(Modifier.PRIVATE)) {
                throw new IllegalAccessException("AutoBind 注解的字段不能为private!");
            }
            if (parentAndChildren.containsKey(parent)) {
                parentAndChildren.get(parent).add(element);
            } else {
                List<Element> eleList = new ArrayList<>();
                eleList.add(element);
                parentAndChildren.put(parent, eleList);
            }
        }
    }
}

gradle配置

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    implementation 'com.squareup:javapoet:1.11.1'
    implementation project(':bindknife_annotation')
}

tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

sourceCompatibility = "8"
targetCompatibility = "8"

3.创建调用入口android-library工程:bindknife

这个工程主要定义了两个类,一个是调用的入口BindKnife,一个为了方便注解操而定义的IKnife接口

IKnife

package com.github.eklir.android.bindknife;

public interface IKnife {
    void inject(Object target);
}

BindKnife

package com.github.eklir.android.bindknife;

import android.app.Activity;

import com.github.eklir.androidproccessor.bindknife.constants.BindKnifeConstants;

public class BindKnife {

    public static void inject(Activity activity) {
        String canonicalName = activity.getClass().getCanonicalName();
        //拼出模板类的全类名
        String bindClassName = canonicalName + BindKnifeConstants.BIND_SUFFIX;
        try {
            Class<?> bindClazz = Class.forName(bindClassName);
            IKnife knife = (IKnife) bindClazz.newInstance();
            knife.inject(activity);
        } catch (ClassNotFoundException e) {
            throw  new RuntimeException(String.format("AutoBind helper class [%s] not found",bindClassName));
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

gradle 配置:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:28.0.0'
    api project(':bindknife_annotation')
}

4.在主工程中使用

package com.github.eklir.androidprocessor;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;

import com.github.eklir.android.bindknife.BindKnife;
import com.github.eklir.androidproccessor.bindknife.annotation.AutoBind;

public class MainActivity extends Activity {

    @AutoBind(R.id.tvTest)
    TextView textView;
    @AutoBind(R.id.ivTest)
    ImageView ivTest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //执行入口调用
        BindKnife.inject(this);
        textView.setText("我是注入的哈哈哈");
        ivTest.setImageResource(R.mipmap.ic_launcher);
    }
}

gradle配置:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.github.eklir.androidprocessor"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation project(':bindknife')
    annotationProcessor project(':bindknife_anno_proccessor')
}

测试说明

测试时,建议在terminal中输入

gradlew clean assembleDebug

命令来测试,如果有错误,会在命令行里提示。执行命令后,生成的java源文件在 app\build\generated\source\apt\debug\xxx目录下,查看java源码是否正确,来调整调用javapoet的api。

demo地址

bindknife

推荐阅读

  • Java中的注解(Annotation)处理器解析
  • Android 打造编译时注解解析框架 这只是一个开始

你可能感兴趣的:(android)