JAVA高级(10)—— 注解

Annotation其实就是代码里的特殊标记,可以在编译,类加载,运行的时候被读取,并执行相应的处理。在不改变原有逻辑的情况下,在源文件嵌入一些补充信息。一条重要原则:仅仅使用注解来标识程序元素,对程序不会造成任何影响,要让注解起作用,必须为这些注解提供注解处理工具。

一、基本Annotation

  • @Override 只能修饰方法,强制子类必须覆盖父类的方法
  • @Deprecated 表示某个程序元素(类、方法、接口等)已经过时,若其他程序使用该元素,将会给出警告
  • @SuppressWarnings 被修饰的程序元素取消显示指定的警告,会同时作用于该程序元素下的所有的子元素

二、自定义Annotation

1、定义Annotation

定义新的Annotation,使用@interface关键字,与定义一个接口非常相似。所有的注解都继承了Annotation接口。

public @interface Testable{
}

使用Annotation的语法,非常类似于public,final等修饰符(一般单独放一行),通常用于修饰程序中的类 、接口、变量、方法等

public class MyClass {
    @Testable
    public void info() {
    }
}

Annotation中还可以带成员变量,用无参数的方法来声明。方法名定义了成员变量的名字,返回值定义了成员变量的类型。语法与定义接口的语法非常相似。若没有定义默认值,则必须赋值。

public @interface Testable {
    String name();
    int age() default 19;//已经设置默认值,使用时可以不赋值
}

//使用
public class MyClass {
    @Testable(name = "xiangyaohui")
    public void info() {
    }
}

标记Annotation:没有成员变量,仅使用自身的存在与否,来为我们提供信息。
元数据Annotation:包含成员变量的Annotation,因为他们可以接受更多的元数据。

2、提取Annotation信息

AnnotatedElement接口中的三个方法

- T getAnnotation(Class aClass) //返回该类程序元素上,指定类型的注解,若该类型的注解不存在,返回null
- Annotation[] getAnnotations()  //返回程序元素存在的所有注解
- boolean isAnnotationPresent(Class aClass)  //判定该程序元素上是否包含指定类型的注解

AnnotatedElement接口的实现类:
Package
Class
Field
Contrustor
Method

2.1、例子一

仅仅是一个标记Annotation,没有成员变量

public @interface Testable {
}

public class MyClass {
    @Testable
    public void m1() {
    }

    public void m2() {
    }
}

public class TestProcessor {
    public static void process(String clazz) throws ClassNotFoundException {
        for (Method m : Class.forName(clazz).getMethods()) {
            //如果包含Testable标记注解
            if (m.isAnnotationPresent(Testable.class)) {
                try {
                    //调用m方法
                    m.invoke(null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

其实注解很简单,就是为源码添加一些特殊的标记,这些标记可以通过反射获取,获取到之后做出相应的处理。

2.2、例子二

public @interface ClickListenerFor {
    //用于保存监听器实现类
    String listener();
}
public class ClickListenerProcessor {
    public static void process(Object obj) {
        try {
            Class clazz = obj.getClass();
            for (Field field : clazz.getFields()) {
                field.setAccessible(true);
                ClickListenerFor c = field.getAnnotation(ClickListenerFor.class);
                if (c != null) {
                    //获取元数据
                    Class listenerClazz = Class.forName(c.listener());//调用方法
                    View.OnClickListener click = (View.OnClickListener) listenerClazz.newInstance();
                    //获取field 实际对应的对象
                    Button button = (Button) field.get(obj);
                    //添加事件
                    button.setOnClickListener(click);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
public class MainActivity extends Activity {
    @ClickListenerFor(listener = "OkClickListener")
    private Button okButton;
    @ClickListenerFor(listener = "CancelClickListener")
    private Button cancelButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button1 = findViewById(R.id.button1);
        Button button2 = findViewById(R.id.button2);
        ClickListenerProcessor.process(this);
    }

    class OkClickListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            //点击了确定按钮
        }
    }

    class CancelClickListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            //点击了取消按钮
        }
    }
}

三、元注解

负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,被用来对其它 annotation类型作说明

@Retention

表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPolicy):

  • SOURCE:在源文件中有效(即源文件保留)
  • CLASS:在class文件中有效(即class保留)
  • RUNTIME:在运行时有效(即运行时保留)

@Target

说明了Annotation所修饰的对象范围
取值(ElementType):

  • ANNOTATION_TYPE:只能修饰Annotation
  • PACKAGE:只能修饰包定义
  • TYPE:只能修饰类、接口(包括注解类型) 或枚举
  • FIELD:只能修饰成员变量
  • CONSTRUCTOR:修饰构造器
  • METHOD:只能修饰方法的定义
  • PARAMETER:用于修饰参数
  • LOCAL_VARIABLE:只能修饰局部变量

注意:定义的Annotation中仅有一个value的成员变量,可以在括号里直接指定值,不需要name=value的形式

@Documented

用于描述被修饰Annotation将被javadoc工具提取到文档,Documented是一个标记注解,没有成员。

@Inherited

被修饰的Annotation具有继承性
如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

四、使用APT来处理Annotation

1、什么是APT

在现阶段的Android开发中,注解越来越流行起来,比如ButterKnife,Retrofit,Dragger,EventBus等等都选择使用注解来配置。按照处理时期,注解又分为两种类型,一种是运行时注解,另一种是编译时注解,运行时注解由于性能问题被一些人所诟病(需要反射)。

编译时注解的核心依赖APT(Annotation Processing Tools)实现,原理是在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译期进行相应的处理,例如,根据注解生成新的Java类,这也就是EventBus,Retrofit,Dragger等开源库的基本原理。

Java API已经提供了扫描源码并解析注解的框架,可以继承AbstractProcessor类来提供实现自己的解析注解逻辑。

2、一个例子

2.1、创建名为processor的module

首先使用Android Studio创建一个Android的project。然后开始创建一个名为processor的java library。
点击file->new->new module



创建一个非Android的library,注意一定要选择Java Library



2.2、兼容性配置

由于Android目前不是完全支持Java 8的语言特性,会导致编译出错。这里将项目的源和目标兼容性值保留为 Java 7。 打开app模块下的build.gradle,在android标签下添加 compile options

compileOptions {
  sourceCompatibility JavaVersion.VERSION_1_7
  targetCompatibility JavaVersion.VERSION_1_7
}

然后打开processor library的build.gradle



添加

sourceCompatibility = 1.7
targetCompatibility = 1.7
2.3、创建Annotation

在processor模块下创建一个注解类



命名为CustomAnnotation



具体内容如下
2.4、创建注解处理器

Processor继承自AbstractProcessor类,@SupportedAnnotationTypes中填写待处理的注解全称,@SupportedSourceVersion表示处理的JAVA版本。

@SupportedAnnotationTypes(“<待处理注解类路径>”)
@SupportedSourceVersion(SourceVersion.RELEASE_7)

(在注解类上右键选择copy reference即可快速的获得类的全称)

实现process方法

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        StringBuilder builder = new StringBuilder()
                .append("package com.yuntao.annotationprocessor.generated;\n\n")
                .append("public class GeneratedClass {\n\n") // open class
                .append("\tpublic String getMessage() {\n") // open method
                .append("\t\treturn \"");
        // for each javax.lang.model.element.Element annotated with the CustomAnnotation
        for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
            String objectType = element.getSimpleName().toString();
            // this is appending to the return statement
            builder.append(objectType).append(" says hello!\\n");
        }
        builder.append("\";\n") // end return
                .append("\t}\n") // close method
                .append("}\n"); // close class
        try { // write the file
            JavaFileObject source = processingEnv.getFiler().createSourceFile("com.yuntao.annotationprocessor.generated.GeneratedClass");
            Writer writer = source.openWriter();
            writer.write(builder.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            // Note: calling e.printStackTrace() will print IO errors
            // that occur from the file already existing after its first run, this is normal
        }
        return true;
    }
2.5、创建resource

创建好注解处理器后,我们需要告诉编译器在编译的时候使用哪个注解处理器,这里就需要创建javax.annotation.processing.Processor文件

在processor模块下,main目录中创建一个resources文件夹,然后下边在创建META-INF/services,最后里边一个javax.annotation.processing.Processor文件,如下


在此文件中写入注解处理器的类全称com.yuntao.annotationprocessor.processor.CustomAnnotationProcessor

2.6、使用注解并生成一个JAVA类

在app模块下的MainActivity中使用注解


Rebuild工程之后(在app模块中,必须有使用这个自定义注解),此处会调用process方法生成了一个Java类文件,该类包含一个getMessage方法,该方法会返回一个字符串,其中字符串包含被@CustomAnnotation修饰的类的名称。这里主要在process方法中获取注解修饰类的名称。生成的代码如下。

目录如下:app/build/generated/source/apt/debug/GeneratedClass.java,这里便于演示我们只是简单的生成了一个Java源文件,当然如果要生成更复杂的文件,可以利用第三方工具,例如javapoet

2.7、添加android-apt

在project下的build.gradle中添加apt插件,添加依赖classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8


然后在app中的build.gradle添加apply plugin: 'com.neenbedankt.android-apt

使用APT来处理annotation的流程
1)定义注解(如@automain)
2)定义注解处理器,自定义需要生成代码
3)使用处理器
4)APT自动完成如下工作

APT流程
2.8、设置build的依赖

这一节主要讲的是把processor模块中的注解,注解处理器编译生成一个jar,然后把这个jar包复制到app模块下。然后让app依赖引用这个jar。
首先配置app的build.gradle依赖项,添加jar依赖,看最后一行。

dependencies {
  compile fileTree(include: ['*.jar'], dir: 'libs')
  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
  })
  compile 'com.android.support:appcompat-v7:25.0.1'
  testCompile 'junit:junit:4.12'
  compile files('libs/processor.jar')
}

然后我们编写一个gradle task,把生成的jar文件复制到app/libs目录中。

task processorTask(type: Exec) {
  commandLine 'cp', '../processor/build/libs/processor.jar', 'libs/'
}

最后我们创建task的依赖顺序,app:preBuild依赖我们写的processorTask, processorTask依赖 :processor:build:
意思就是processor build完成后把jar复制到app,然后在执行app:preBuild。

processorTask.dependsOn(':processor:build')
preBuild.dependsOn(processorTask)

最后配置完的build.gradle文件如下。



执行编译,编译完成后在app/libs目录下可以查看到生成的jar


2.9、使用注解

我们在MainActivity的类与onCreate方法上使用@CustomAnnotation



现在加上注解之后还没有生成我们需要的java文件,需要rebuild下才会生成,可以在Android Studio中选择Build>Rebuild或者在终端执行
然后我们可以在下述位置查看到生成的Java文件
app/build/generated/source/apt/debug/package/GeneratedClass.java



参考文献

Android中使用AbstractProcessor在编译时生成代码

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