Android进阶之自定义注解

Android进阶之自定义注解

本篇文章内容包括:

如果使用过ButterKnife, EventBus, Retrofit, Dagger等框架, 你对注解一定不会陌生. 但是注解背后究竟有什么魔法, 可以做这么不可思议的事情.

什么是注解

先来看看Java文档中的定义

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

注解是一种元数据, 可以添加到java代码中. 类、方法、变量、参数、包都可以被注解,注解对注解的代码没有直接影响.

首先, 明确一点: 注解并没有什么魔法, 之所以产生作用, 是对其解析后做了相应的处理. 注解仅仅只是个标记罢了.

定义注解用的关键字是@interface

元注解

java内置的注解有Override, Deprecated, SuppressWarnings等, 作用相信大家都知道.

现在查看Override注解的源码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

发现Override注解上面有两个注解, 这就是元注解. 元注解就是用来定义注解的注解.其作用就是定义注解的作用范围, 使用在什么元素上等等, 下面来详细介绍.

元注解共有四种@Retention, @Target, @Inherited, @Documented

  • @Retention 保留的范围,默认值为CLASS. 可选值有三种

    • SOURCE, 只在源码中可用
    • CLASS, 在源码和字节码中可用
    • RUNTIME, 在源码,字节码,运行时均可用
  • @Target 可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未标注则表示可修饰所有

  • @Inherited 是否可以被继承,默认为false

  • @Documented 是否会保存到 Javadoc 文档中

其中, @Retention是定义保留策略, 直接决定了我们用何种方式解析. SOUCE级别的注解是用来标记的, 比如Override, SuppressWarnings. 我们真正使用的类型是CLASS(编译时)和RUNTIME(运行时)

自定义注解

举个栗子, 结合例子讲解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TestAnnotation {
    String value();
    String[] value2() default "value2";
}

元注解的的意义参考上面的讲解, 不再重复, 这里看注解值的写法:

类型 参数名() default 默认值;

其中默认值是可选的, 可以定义, 也可以不定义.

处理运行时注解

Retention的值为RUNTIME时, 注解会保留到运行时, 因此使用反射来解析注解.

使用的注解就是上一步的@TestAnnotation, 解析示例如下:

public class Demo {

    @TestAnnotation("Hello Annotation!")
    private String testAnnotation;

    public static void main(String[] args) {
        try {
            // 获取要解析的类
            Class cls = Class.forName("myAnnotation.Demo");
            // 拿到所有Field
            Field[] declaredFields = cls.getDeclaredFields();
            for(Field field : declaredFields){
                // 获取Field上的注解
                TestAnnotation annotation = field.getAnnotation(TestAnnotation.class);
                if(annotation != null){
                    // 获取注解值
                    String value = annotation.value();
                    System.out.println(value);
                }

            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

此处只演示了解析成员变量上的注解, 其他类型与此类似.

解析编译时注解

解析编译时注解需要继承AbstractProcessor类, 实现其抽象方法

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)

该方法返回ture表示该注解已经被处理, 后续不会再有其他处理器处理; 返回false表示仍可被其他处理器处理.

处理示例:

// 指定要解析的注解
@SupportedAnnotationTypes("myAnnotation.TestAnnotation")
// 指定JDK版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcesser extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement te : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
                TestAnnotation testAnnotation = element.getAnnotation(TestAnnotation.class);
                // do something
            }
        }
        return true;
    }
}

这里先大致介绍是怎么个套路, 接下来说具体实践过程.

Android中使用编译时注解

注解是个什么东西我们已经知道了, 也知道了如何解析注解. 我们下一步的目标是如ButterKnife一般自动生成代码.

接下来的操作基于InteliJ IDEA(开发注解及其解析类, 打出jar包)和Android Studio(实测使用情况)

note: AS的Android开发环境中没有AbstractProcessor类, 而我新建了Java Module后遇到了各种各样的花式错误(后面的报错之路会叙述), 无奈只能在IDEA中开发并打出jar包

开发注解库

在IDEA中新建java项目, 并开启maven支持. 如果新建项目的页面没有maven选项, 建好项目后右键项目目录->"Add Framwork Support...", 选择maven.

自定义编译时注解

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
public @interface TestAnnotation {
    String value() default "Hello Annotation";
}

解析编译时注解

// 支持的注解类型, 此处要填写全类名
@SupportedAnnotationTypes("myannotation.TestAnnotation")
// JDK版本, 我用的是java7
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcessor extends AbstractProcessor {
    // 类名的前缀后缀
    public static final String SUFFIX = "AutoGenerate";
    public static final String PREFIX = "My_";
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        for (TypeElement te : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
                // 准备在gradle的控制台打印信息
                Messager messager = processingEnv.getMessager();
                // 打印
                messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString());
                messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getSimpleName());
                messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getEnclosingElement().toString());

                // 获取注解
                TestAnnotation annotation = e.getAnnotation(TestAnnotation.class);

                // 获取元素名并将其首字母大写
                String name = e.getSimpleName().toString();
                char c = Character.toUpperCase(name.charAt(0));
                name = String.valueOf(c+name.substring(1));

                // 包裹注解元素的元素, 也就是其父元素, 比如注解了成员变量或者成员函数, 其上层就是该类
                Element enclosingElement = e.getEnclosingElement();
                // 获取父元素的全类名, 用来生成包名
                String enclosingQualifiedName;
                if(enclosingElement instanceof PackageElement){
                    enclosingQualifiedName = ((PackageElement)enclosingElement).getQualifiedName().toString();
                }else {
                    enclosingQualifiedName = ((TypeElement)enclosingElement).getQualifiedName().toString();
                }
                try {
                    // 生成的包名
                    String genaratePackageName = enclosingQualifiedName.substring(0, enclosingQualifiedName.lastIndexOf('.'));
                    // 生成的类名
                    String genarateClassName = PREFIX + enclosingElement.getSimpleName() + SUFFIX;

                    // 创建Java文件
                    JavaFileObject f = processingEnv.getFiler().createSourceFile(genarateClassName);
                    // 在控制台输出文件路径
                    messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + f.toUri());
                    Writer w = f.openWriter();
                    try {
                        PrintWriter pw = new PrintWriter(w);
                        pw.println("package " + genaratePackageName + ";");
                        pw.println("\npublic class " + genarateClassName + " { ");
                        pw.println("\n    /** 打印值 */");
                        pw.println("    public static void print" + name + "() {");
                        pw.println("        // 注解的父元素: " + enclosingElement.toString());
                        pw.println("        System.out.println(\"代码生成的路径: "+f.toUri()+"\");");
                        pw.println("        System.out.println(\"注解的元素: "+e.toString()+"\");");
                        pw.println("        System.out.println(\"注解的值: "+annotation.value()+"\");");
                        pw.println("    }");
                        pw.println("}");
                        pw.flush();
                    } finally {
                        w.close();
                    }
                } catch (IOException x) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            x.toString());
                }
            }
        }
        return true;
    }
}

看似代码很长, 其实很好理解. 只做了两件事, 1.解析注解并获取需要的值 2.使用JavaFileObject类生成java代码.

向JVM声明解析器

我们的解析器虽然定义好了, 但是jvm并不知道, 也不会调用, 因此我们需要声明.

如图所示

在java的同级目录新建resources目录, 新建META-INF/services/javax.annotation.processing.Processor文件, 文件中填写你自定义的Processor全类名

然后打出jar包以待使用(打包方式自行百度)

Android中使用

使用apt插件

项目根目录gradle中buildscriptdependencies添加

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

module目录的gradle中, 添加

apply plugin: 'android-apt'

代码中调用

将之前打出的jar包导入项目中, 在MainActivity中写个测试方法

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    test();
}

@TestAnnotation("hehe")
public void test(){
}

运行一遍项目之后, 代码就会自动生成.

以下是生成的代码, 在路径yourmodule/build/generated/source/apt/debug/yourpackagename中:

public class My_MainActivityAutoGenerate { 

    /** 打印值 */
    public static void printTest() {
        // 注解的父元素: com.example.pan.androidtestdemo.MainActivity
        System.out.println("代码生成的路径: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/My_MainActivityAutoGenerate.java");
        System.out.println("注解的元素: test()");
        System.out.println("注解的值: hehe");
    }
}

然后在test方法中调用自动生成的方法

@TestAnnotation("hehe")
public void test(){
    My_MainActivityAutoGenerate.printTest();
}

会看到以下打印结果:

代码生成的路径: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/com/example/pan/androidtestdemo/MainActivityAutoGenerate.java
注解的元素: test()
注解的值: hehe

报错之路

开始时, 我在Android Studio的Java Library中编写解析类, 然后在Android Module依赖Java库, 然后报下面这个错误

For more information see https://docs.gradle.org/current/userguide/build_environment.html
Error:Error converting bytecode to dex:
Cause: Dex cannot parse version 52 byte code.
This is caused by library dependencies that have been compiled using Java 8 or above.
If you are using the 'java' gradle plugin in a library submodule add 
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
to that submodule's build.gradle file.

我tm本来就是Java8啊, 一番Google, 需要开启手动开启才能支持java8, 步骤如下:

android {
    compileSdkVersion 23
    // 开启Java8, buildTools版本必须24以上
    buildToolsVersion "24"
    ...
    defaultConfig {
        ...
        // Java8需要jack工具链支持
        jackOptions{
            enabled true
        }

    }
    ...
    // 指定编译版本
    compileOptions{
        targetCompatibility = '1.8'
        sourceCompatibility = '1.8'
    }
}

然而...又报了这个错误

Error: Could not find the property 'options' on the task' : app: compileDebugJavaWithJack '.

来自JakeWharton大神的回复, jack编译器目前并不支持apt插件https://github.com/JakeWharton/butterknife/issues/571

摔! 不用java8报错, 用了又尼玛报. 自动生成代码是必须要用apt插件的. 那就只能用java7在IDEA里开发了.

时至今日(2016年06月23日), Google并没有解决这个问题, 目前jack编译器还处于预览版, 相信以后会解决吧

总结

有了本文所述的注解知识, 对Dagger,ButterKnife等框架就不难理解了. 如果在时间精力允许的情况下, 我们也完全可以自定义个注解框架.

本文中自动生成代码的部分十分简单, 也隐含bug: 在for循环中创建了文件, 如果一个类中使用了两次该注解, 第二次是无法创建新文件的. 真正的实际项目中, 肯定是将需要的信息保存起来, 之后统一创建java类.

更进一步的应用大家可以查看其他注解框架的源码, 调试注解大家可以查看这篇文章如何debug自定义AbstractProcessor, 我这里就不过多赘述了

水平有限, 如有错误欢迎指正.

参考链接

公共技术点之 Java 注解 Annotation

Annotation实战【自定义AbstractProcessor】

android 中运用apt自定义一个AbstractProcessor

打造一个 Android 的注解库

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,543评论 25 707
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李头阅读 6,206评论 4 31
  • 给我一只手 让我跟你走 给我一朵花 一颗向日葵 让我种下整片蓝天 给你最明亮、最干净、最纯粹的 眼神与期待 彩色泥...
    寐不成若阅读 226评论 6 2
  • 汨罗江畔一跃兮,屈子纵身成魂。 仿离骚以自比兮,继而九歌天问。 感夏日之炎炎兮,如旧绪绕彤云。 天青色等烟雨兮,粽...
    许士健阅读 202评论 0 2