Android APT(Java注解应用)

关于Java注解

一.APT技术

APT(Annotation Process Tool),注解处理器。用来在编译时扫描和处理注解,按照一定的规则,生成相应的java文件。

Android 目前比较流行的Dagger2, ButterKnife, EventBus3都采用了APT技术。

二.APT使用

1. AbstractProcesser类

Java语言自带的Override等注解,虚拟机会默认处理。
对于自定义注解,需要我们自己处理。java提供一个AbstractProcesser.java类,我们需要继承该类,实现自动的注解处理器,来处理自定义注解

public abstract class AbstractProcessor implements Processor {
  
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    /**
     * 构造器
     */
    protected AbstractProcessor() {}

    /**
     * 指定可以被处理器识别的选项
     *
     * 默认实现:
     * 如果处理器类使用{@SupportedOptions}注解,则返回一个不可修改的字符串set集 
     * 合,包含注解设置的属性值。如果未对类进行注解,则返回空集合   
     */
    public Set<String> getSupportedOptions() {
        SupportedOptions so = this.getClass().getAnnotation(SupportedOptions.class);
        if  (so == null)
            return Collections.emptySet();
        else
            return arrayToSet(so.value());
    }

    /**
     * 指定此处理器支持的注解类型
     * 
     * 默认实现:
     * 如果此处理器类使用了{@SupportedAnnotationTypes}注解,则返回一个不可修改的 
     * set字符串集合,包含注解设置的属性值。如果未对类进行注释,则返回空集合
     */
    public Set<String> getSupportedAnnotationTypes() {
            SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
            if  (sat == null) {
                if (isInitialized())
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                             "No SupportedAnnotationTypes annotation " +
                                                             "found on " + this.getClass().getName() +
                                                             ", returning an empty set.");
                return Collections.emptySet();
            }
            else
                return arrayToSet(sat.value());
        }

    /**
     * 如果处理器类使用{@SupportedSourceVersion}注解,则返回注释中设置的版本值。
     * 如果没有对类进行注解,则返回Java 6.0
     *
     * 指定此处理器支持的最新Java版本,通常返回SourceVersion.latestSupported()
     */
    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (isInitialized())
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                         "No SupportedSourceVersion annotation " +
                                                         "found on " + this.getClass().getName() +
                                                         ", returning " + sv + ".");
        } else
            sv = ssv.value();
        return sv;
    }


    /**
     * 初始化方法 ,只能初始化一次,否则会抛出IllegalStateException异常
     *
     *一般在这里获取我们需要的工具类
     * @param processingEnv 提供工具类Elements, Types和Filer
     */
    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }

    /**
     * 注解处理方法,可以在这里写扫描、评估和处理注解的代码,生成Java文件
     */
    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

    /**
     * Returns an empty iterable of completions.
     */
    public Iterable<? extends Completion> getCompletions(Element element,
                                                         AnnotationMirror annotation,
                                                         ExecutableElement member,
                                                         String userText) {
        return Collections.emptyList();
    }

    protected synchronized boolean isInitialized() {
        return initialized;
    }

    private static Set<String> arrayToSet(String[] array) {
        assert array != null;
        Set<String> set = new HashSet<String>(array.length);
        for (String s : array)
            set.add(s);
        return Collections.unmodifiableSet(set);
    }
}

2. 自定义注解

Java注解
这里自定义一个BindView,应用场景类似于ButterKnife框架的findView功能,用来找到View对象
注解定义如下:

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

3. 自定义注解处理器

这里需要注意需要创建Java library Moduleapply plugin: ‘java-library’不然找不到AbstractProcessor 类
依赖:

 implementation project(':annotation')

 implementation 'com.google.auto.service:auto-service:1.0-rc4'
 implementation 'com.squareup:javapoet:1.11.1'
  • :annotation:自定义注解的Module
  • auto-service依赖库:
    为什么引入auto-service
    在使用注解处理器需要先声明,步骤:
    1、需要在 processors 库的 main 目录下新建 resources 资源文件夹;
    2、在 resources文件夹下建立 META-INF/services 目录文件夹;
    3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
    4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;)
    引入auto-service库,使用@AutoService注解声明就可以自动完成APT的声明步骤。
  • javapoet square依赖库:提供的生成Java文件开源库 github地址 中文教程

自定义注解处理器
一般注解处理器逻辑:
1. 遍历得到源码中,需要解析的元素列表。
2. 判断元素是否可见和符合要求。
3. 组织数据结构得到输出类参数。
4. 输入生成java文件。
5. 错误处理。

自定义APT,需要了解一下Java Element

Processor处理过程中,会扫描全部Java源码,代码的每一个部分都是一个特定类型的Element,它们像是XML一层的层级机构。

自定义BindView注解处理器:BinderProcessor类

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

    private Elements mElementUtils;
    private HashMap<String, BinderClassCreator> mCreatorMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。
        //processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息
        //processingEnvironment.getFiler(); 文件工具
        //processingEnvironment.getMessager(); 错误处理工具
        mElementUtils = processingEnv.getElementUtils();
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //大部分class而已getName、getCanonicalNam这两个方法没有什么不同的。
        //但是对于array或内部类等就不一样了。
        //getName返回的是[[Ljava.lang.String之类的表现形式,
        //getCanonicalName返回的就是跟我们声明类似的形式。
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
        //因为兼容的原因,特别是针对Android平台,建议使用重载getSupportedAnnotationTypes()方法替代默认使用注解实现
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //扫描整个工程   找出含有BindView注解的元素
        Set<? extends Element> elements =
                roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //遍历元素
        for (Element element : elements) {
            //BindView限定了只能属性使用,这里强转为VariableElement
            VariableElement variableElement = (VariableElement) element;
            //返回此元素直接封装(非严格意义上)的元素。
            //类或接口被认为用于封装它直接声明的字段、方法、构造方法和成员类型
            //这里就是获取封装属性元素的类元素
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //获取简单类名
            String fullClassName = classElement.getQualifiedName().toString();
            BinderClassCreator creator = mCreatorMap.get(fullClassName);
            if (creator == null) {
                creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement),
                        classElement);
                mCreatorMap.put(fullClassName, creator);

            }
            //获取元素注解
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            //注解值
            int id = bindAnnotation.value();
            creator.putElement(id, variableElement);
        }
        for (String key : mCreatorMap.keySet()) {
            BinderClassCreator binderClassCreator = mCreatorMap.get(key);
            //通过javapoet构建生成Java类文件
            JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(),
                    binderClassCreator.generateJavaCode()).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return false;
    }
}

BinderClassCreator类 用于生成Java类代码,具体代码如下:

public class BinderClassCreator {

    public static final String ParamName = "view";

    private TypeElement mTypeElement;
    private String mPackageName;
    private String mBinderClassName;
    private Map<Integer, VariableElement> mVariableElements = new HashMap<>();

    /**
     * @param packageElement 包元素
     * @param classElement   类元素
     */
    public BinderClassCreator(PackageElement packageElement, TypeElement classElement) {
        this.mTypeElement = classElement;
        mPackageName = packageElement.getQualifiedName().toString();
        mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";
    }

    public void putElement(int id, VariableElement variableElement) {
        mVariableElements.put(id, variableElement);
    }

    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBinderClassName)
                //public 修饰类
                .addModifiers(Modifier.PUBLIC)
                //添加类的方法
                .addMethod(generateMethod())
                //构建Java类
                .build();

    }

    private MethodSpec generateMethod() {
        //获取所有注解的类的类名
        ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        //构建方法--方法名
        return MethodSpec.methodBuilder("bindView")
                //public方法
                .addModifiers(Modifier.PUBLIC)
                //返回void
                .returns(void.class)
                //方法传参(参数全类名,参数名)
                .addParameter(className, ParamName)
                //方法代码
                .addCode(generateMethodCode())
                .build();
    }

    private String generateMethodCode() {
        StringBuilder code = new StringBuilder();
        for (int id : mVariableElements.keySet()) {
            VariableElement variableElement = mVariableElements.get(id);
            //使用注解的属性的名称
            String name = variableElement.getSimpleName().toString();
            //使用注解的属性的类型
            String type = variableElement.asType().toString();
            //view.name = (type)view.findViewById(id)
            String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName +
                    ".findViewById(" + id + ");\n";
            code.append(findViewCode);

        }
        return code.toString();
    }

    public String getPackageName() {
        return mPackageName;
    }
}

4. APT生成代码使用

注解处理器完成了,怎么使用呢?需要通过反射来调用注解处理器生成代码
通过上面的注解处理器可知道,注解生成的类包名和使用注解的类的包名一致,类名为使用注解的类名__ViewBinding,方法名为bindView,传参为使用注解的类对象。所以可以如下通过反射调用

BinderViewTools类

public class BinderViewTools {
    public static void init(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class<?> bindClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method bind = bindClass.getMethod("bindView", clazz);
            bind.invoke(bindClass.newInstance(),activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5. APP Module中使用注解

依赖:

  • gradle>=2.2 使用annotationProcessor替代android-apt依赖注解处理器并进行工作
    (关于annotationProcessor替代android-apt:能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件,且打包时不会包含处理器代码)
    在Moudle的gradle中配置
annotationProcessor project(':apt_lib')
implementation project(':annotation')
  • gradle<2.2
    在project的gradle和Moudle的gradle中分别做如下配置
buildscript {
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  
    }
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    apt project(':apt_lib')
}

在Activity使用注解
MainActivity类

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textView)
    TextView mTextView;

    @BindView(R.id.imageView)
    ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BinderViewTools.init(this);
        mTextView.setVisibility(View.VISIBLE);
        mImageView.setVisibility(View.VISIBLE);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="获取View成功"
        android:visibility="gone"/>

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView"
        android:layout_centerInParent="true"
        android:src="@mipmap/ic_launcher"
        android:visibility="gone"/>
</RelativeLayout>

两个View在XML中默认隐藏,使用BindView注解,然后调用BinderViewTools.init(this);方法调用注解处理器生成的代码来获取View,最后设置View显示

注解处理器生成代码 MainActivity_ViewBinding类

public class MainActivity_ViewBinding {
  public void bindView(MainActivity view) {
    view.mTextView=(android.widget.TextView)view.findViewById(2131165295);
    view.mImageView=(android.widget.ImageView)view.findViewById(2131165240);
  }
}

运行效果


可以看到注解处理器正常生成代码,并且最终通过反射成功调用,View对象获取成功。

参考文章
1.[Android] APT
2.Android注解快速入门和实用解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容