Java Annotation 理解和运用

前言

在Android开发作业中接触到了很多开源框架使用了Java Annotation机制,我接触到的就有GreenRobot、Dagger2、AndFix等项目。

那么 Annotation机制到底是如何发挥作用的?下面将介绍Annotation的常见类型及基本语法。

从@Override认识注解

相信大部分同学对@Override一点都不陌生,在子类覆盖超类的方法时,Eclipse等IDE会在方法上自动生成这个注解。

那么来看一下这个注解的语法形式:

package java.lang;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

其中@interface 定义了Override是一个Annotation类型,或者叫元数据(meta-data)。
@Target和@Retetion是对Override的注解,称之为元注解(元数据的注解)。

@Target

再来看下@Target的定义:

package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

首先Target也是一个注解类型,在其内部定义了方法ElementType[] value();

细心观察就会发现这个方法的返回值就是@Target(ElementType.METHOD)中的ElementType.METHOD,也就是注解的属性,是一个ElementType枚举。

再来看ElementType的定义:

package java.lang.annotation;
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

这个枚举其实就是定义了注解的适用范围,在Override注解中,@Target的属性是ElementType.METHOD,所以Override这个注解只能用于注解方法。

而Target注解本身也有一个@Target元注解,这个@Target元注解属性是ElementType.ANNOTATION_TYPE,也就是说Target注解只能用作元数据(注解)的注解,所以叫它元注解。

@Retention

@Target声明了Override注解只能使用代码中的方法定义,@Retention注解则定义了注解的保留范围,如:在源代码、CLASS文件或运行时保留。

超出@Retention定义的属性,注解将被丢弃。

package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

如果像Override注解中的@Retention定义RetentionPolicy .SOURCE属性,那么生成CLASS文件时不会在方法上见到@Override。由于Override注解用于检测子类有无实现超类或接口的抽象方法,所以只在编译阶段检测语法是否正确就足够了。

@Documented

@Documented也属于元注解:

package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

如果一个注解定义了@Ducumented,在javadoc API文档时,被这个注解标记的元素在文档上也会出现该注解,例如:

//自定义一个注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.Main;
}

//使用自定义的注解
public class A {
    @Subscribe
    public void a(EventA a){
        System.out.println("tid "+Thread.currentThread().getId()+" run A.a() ,event name is "+a.name);
    }
}

在javadoc生成的文档中可见:

@Subscribe
public void a(EventA a)

而不在Subsribe注解上添加@Documented则不会在方法a(EventA a)上出现@Subscribe。

@Inherited

该元注解比较特殊,只对@Target为ElementType.TYPE(类、接口、枚举)有效,并且只支持类元素。

使用了@Inherited的注解修饰在一个class上,可以保证继承它的子类也拥有同样的注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

小结

通过常见的@Override我们认识了一个注解定义的语法,并且认识了四个基本元注解@Target、@Retention、@Documented、@Inherited以及它们的功能。
其中@Target用来指定注解可以修饰的源代码中的元素(构造器、方法、字段、包、参数、局部变量、类或接口),@Retention指定注解保留的范围(源代码、class文件、运行时),@Documented指定注解是否出现在javadoc生成的API文档中的具体的元素上,@Inherited指定注解是否可以被子类继承。

自定义注解

使用@interface可以定义一个注解,形如:

@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.Main;
}

在本例中threadMode()返回的是注解的一个属性,其返回值可以是所有基本类型、String、Class、enum、Annotation或以上类型的数组。

default关键字可以定义该属性的默认值,如果没有指定默认值在使用注解时必须显示指定属性值。

特别说明的是注解不支持像extends语法这样的继承。

自定义注解以及反射使用注解的例子

本例将仿照Eventbus使用注解的语法实现一个超简易版的Mybus。

//定义运行线程枚举
public enum ThreadMode {
    Posting,Main;
}

//定义注解用于反射识别观察者方法
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.Main;
}

//定义一个MyBus类用于注册、注销观察者或者发出通知给观察者,在发出通知时会遍历观察者集合,找出观察者中被@Subscribe 注解的方法,判断通知事件的类型与@Subscribe 注解方法的参数类型是否匹配,然后根据ThreadMode交给观察者在主线程或子线程处理。
public class MyBus {

    LinkedList<Object> mObjs = new LinkedList<Object>();

    
    public void register(Object o) {
        mObjs.add(o);
    }

    public void unregister(Object o) {
        mObjs.remove(o);
    }

    public void post(final Object event) {
        // System.out.println("post参数类型:"+event.getClass().getCanonicalName());
        Iterator<Object> iterator = mObjs.iterator();
        while (iterator.hasNext()) {
            final Object obj = iterator.next();
            Class<? extends Object> cl = obj.getClass();
            // System.out.println("遍历类 "+cl.getCanonicalName());
            for (final Method method : cl.getDeclaredMethods()) {
                Subscribe subscribe = method.getAnnotation(Subscribe.class);
                if (subscribe != null) {
                    // System.out.println("找到注解的方法 "+method.getName());
                    if (method.getParameterCount() == 1) {
                        Class pmClass = (method.getParameterTypes())[0];
                        // System.out.println(method.getName()+"的参数类型:"+pmClass.getCanonicalName());
                        if (pmClass.equals(event.getClass())) {
                            // 判断参数的类型是post参数类型的超类或接口或相等
                            // System.out.println(method.getName()+" 是合法的");
                            try {
                                if (subscribe.threadMode() == ThreadMode.Main) {
                                    method.invoke(obj, event);
                                }else{
                                    new Thread(){
                                        public void run() {
                                            try {
                                                method.invoke(obj, event);
                                            } catch (IllegalAccessException
                                                    | IllegalArgumentException
                                                    | InvocationTargetException e) {
                                                // TODO Auto-generated catch block
                                                e.printStackTrace();
                                            }
                                        };
                                    }.start();
                                }
                            } catch (IllegalAccessException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            } catch (IllegalArgumentException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }

}


//事件A
public class EventA {
    public String name;
}

//事件B
public class EventB {
    public String name;
}

//观察者A
public class A {
    @Subscribe
    public void a(EventA a){
        System.out.println("tid "+Thread.currentThread().getId()+" run A.a() ,event name is "+a.name);
    }
}

//观察者B
public class B {
    @Subscribe(threadMode=ThreadMode.Posting)
    public void b(EventB e){
        System.out.println("tid "+Thread.currentThread().getId()+" run B.b(),event name is "+e.name);
    }
}

//测试
public class Main {

    public static void main(String[] args) {
        A a= new A();
        B b=new B();
        MyBus bus = new MyBus();
        bus.register(a);
        bus.register(b);
        
        EventA eventA = new EventA();
        eventA.name="eventA";
        
        EventB eventB = new EventB();
        eventB.name = "eventB";
        bus.post(eventA);
        bus.post(eventB);
    }

}

测试结果:

tid 1 run A.a() ,event name is eventA
tid 10 run B.b(),event name is eventB

可以看到发出不同的事件A和B,最后根据观察者A和B中方法的注解找到方法处理,由注解中的ThreadMode属性指定在哪个线程执行处理方法。

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

推荐阅读更多精彩内容