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, 我这里就不过多赘述了

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

Demo

笔者在网上找了一个别人的demo,供大家参考:点击打开链接

参考文章:

http://programmaticallyspeaking.com/playing-with-java-annotation-processing.html

http://www.cnblogs.com/avenwu/p/4173899.html

时间: 2024-10-21 10:01:51

Android进阶之自定义注解的相关文章

Android进阶篇-自定义图片伸缩控件具体实例_Android

ZoomImageView.java: 复制代码 代码如下: /** * @author gongchaobin *  *  自定义可伸缩的ImageView */public class ZoomImageView extends View{    /** 画笔类  **/    private Paint mPaint;     private Runnable mRefresh = null;    /** 缩放手势监听类  **/    private ScaleGestureDetec

我的Android进阶之旅------&amp;gt;Android如何通过自定义SeekBar来实现视频播放进度条

首先来看一下效果图,如下所示: 其中进度条如下: 接下来说一说我的思路,上面的进度拖动条有自定义的Thumb,在Thumb正上方有一个PopupWindow窗口,窗口里面显示当前的播放时间.在SeekBar右边有一个文本框显示当前播放时间/总时间. step1.先来看一看PopupWindow的布局文件,seek_popu.xml,效果如下图所示: <?xml version="1.0" encoding="utf-8"?> <RelativeLa

我的Android进阶之旅------&amp;gt;经典的大牛博客推荐(排名不分先后)!!

今天看到一篇文章,收藏了很多大牛的博客,在这里分享一下 谦虚的天下 柳志超博客 Android中文Wiki AndroidStudio-NDK开发-移动开发团队 谦虚的天下 - 博客园 gundumw100博客 - android进阶分类文章列表 - ITeye技术网站 CSDN博文精选:Android系列开发博客资源汇总 - CSDN.NET - CSDN资讯 Android笔记本--半年来的研究笔记,导航. - 思想实践地 - CSDN博客 [魏祝林]Android中级教程 - Androi

我的Android进阶之旅】GitHub 上排名前 100 的 Android 开源库进行简单的介绍

GitHub Android Libraries Top 100 简介 本文转载于:https://github.com/Freelander/Android_Data/blob/master/Android-Librarys-Top-100.md 本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍, 至于排名完全是根据 GitHub 搜索 Java 语言选择 (Best Match) 得到的结果, 然后过滤了跟 Android 不相关的项目, 所以排名并

Android编程实现自定义手势的方法详解_Android

本文实例讲述了Android编程实现自定义手势的方法.分享给大家供大家参考,具体如下: 之前介绍过如何在Android程序中使用手势,主要是系统默认提供的几个手势,这次介绍一下如何自定义手势,以及如何对其进行管理. 先介绍一下Android系统对手势的管理,Android系统允许应用程序把用户的手势以文件的形式保存以前,以后要使用这些手势只需要加载这个手势库文件即可,同时Android系统还提供了诸如手势识别.查找及删除等的函数接口,具体如下: 一.加载手势库文件: staticGestureL

我的Android进阶之旅------&amp;gt;解决Jackson等第三方转换Json的开发包在开启混淆后转换的实体类数据都是null的bug

1.错误描述 今天测试人员提了一个bug,说使用我们的app出现了闪退的bug,后来通过debug断点调试,发现我们的app转换服务器发送过来的json数据后,都是为null.而之前已经提测快一个月的功能,一直都是稳定的,为什么现在会报java.lang.NullPointerException. 2.错误原因 原来我提测了一个月的APP版本一直没有打开混淆开关,而出问题的这个APP版本在即将要发布出去的时候打开了混淆开关.这样的话,我那些要通过转换json数据为bean实体类,因为没有在pro

我的Android进阶之旅------&amp;gt;Android疯狂连连看游戏的实现之开发游戏界面(二)

连连看的游戏界面十分简单,大致可以分为两个区域: 游戏主界面区 控制按钮和数据显示区 1.开发界面布局 本程序使用一个RelativeLayout作为整体的界面布局元素,界面布局上面是一个自定义组件,下面是一个水平排列的LinearLayout. 下面是本程序的布局文件:/res/layout/main.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android=

我的Android进阶之旅------&amp;gt; Android为TextView组件中显示的文本添加背景色

通过上一篇文章 我的Android进阶之旅------> Android在TextView中显示图片方法 (地址:http://blog.csdn.net/ouyang_peng/article/details/46916963)      我们学会了在TextView中显示图片的方法,现在我们来学习如何为TextView组件中显示的文本添加背景色.要求完成的样子如图所示: 首先来学习使用BackgroundColorSpan对象设置文字背景色,代码如下: TextView textView=(

我的Android进阶之旅------&amp;gt;如何将Activity变为半透明的对话框?

              我的Android进阶之旅------>如何将Activity变为半透明的对话框?可以从两个方面来考虑:对话框和半透明. 在定义Activity时指定Theme.Dialog主题就可以将Activity设置为对话框风格. 通过修改Theme.Dialog主题的android:windowBackground属性值可以改变Activity的背景图像.如果背景图像使用半透明的图像,则Activity就好变成半透明的对话框.为了修改android:windowBackgro