注解及其三类应用场景

注解(Annotation)

注解又称为Java标注,是JDK5.0引入的一种注释机制,注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据,注解对他们注解的代码的操作没有直接的影响。注解本身没有意义,他需要结合反射和插桩等技术才有意义

声明注解
//使用@interface 关键字声明注解
public @interface FirstAnnotation{
  
}
元注解

能够应用标注在其他注解上的注解我们称之为元注解,在自定义注解时,我们需要使用的元注解有两个,分别是@Target和@Retention,这两个元注解通过传递参数,限制其标记的注解。

  • @Target 标记一个注解的应用范围,即该注解可以应用在哪些元素上

    • 可以应用于注解类型 ElementType.ANNOTATION_TYPE
    • 可以应用于构造函数 ElementType.CONSTRUCTOR
    • 可以应用于字段或属性 ElementType.FIELD
    • 可以应用于局部变量 ElementType.LOCAL_VARIABLE
    • 可以应用于方法 ElementType.METHOD
    • 可以应用于包 ElementType.PACKAGE
    • 可以应用于方法中的参数 ElementType.PARAMETER
    • 可以应用于类的任何元素 ElementType.TYPE
  • @Retention 标记了一个注解的存活时间

    • 标记的注解仅保留在源码级别中,即止保留在.java文件中,会被被编译器忽略 RetentionPolicy.SOURCE
    • 标记的注解在编译时由编译器保留,即可以保留在.class文件中,由编译器处理,但会被Java虚拟机忽略 RetentionPolicy.CLASS
    • 标记的注解由JVM保留,因此运行环境可以使用它 RetentionPolicy.RUNTIME

    从存在的过程长短来看,SOURCE < CLASS < RUNTIME,也就是说CLASS包含SOURCE,RUNTIME包含CLASS和SOURCE。

下面看一个例子
//@Target(ElementType.TYPE) //只能在类上使用该注解
@Target({ElementType.TYPE,ElementType.FIELD}) //允许在类、属性上使用该注解
@Retention(RetentionPolicy.SOURCE) //注解仅保留到源码
public @interface FirstAnnotation{
  
}
注解类型元素

为自定义的注解包含注解类型元素声明,他们看起来很像方法,可以定义可选的默认值。

@Target({ElementType.TYPE,ElementType.FIELD}) //允许在类、属性上使用该注解
@Retention(RetentionPolicy.SOURCE) //注解仅保留到源码
public @interface FirstAnnotation{
  String value(); //无默认值
  int age() default 1;// 有默认值
}
注解的应用场景

根据元注解@Retention定义的注解存储方式,注解有三种使用场景。

  • SOURCE:作用与源码级别的注解,可提供IDE语法检查、APT等场景使用,注解在编译器编译后会被丢弃
  • CLASS: 会保留在class文件中,可以用于字节码操作,如AspectJ、热修复Roubust。
  • RUNTIME:注解保留至运行期,可结合反射技术使用。
1、IDE语法检查

可自定义一个注解来检查传入方法的参数的合法性,比如以下实例中,用注解@pet限制了方法的参数只能是DOG和CAT,如果传入其他的参数,会出现警告。在开发中可使用已经存在的注解,比如@DrawableRes 等。该方法与枚举相比,可节省不少内容空间,因为枚举的本质是对象,会比常量多5到10倍的内存占用。

public class MyClass {

    public static void main(String[] args){
        MyClass myClass = new MyClass();

        myClass.havePet(DOG);
        //myClass.havePet(TIGER);//会出现参数非法警告

    }

    private static final int DOG = 0;
    private static final int CAT = 1;
    private static final int TIGER = 2;

    @IntDef({DOG,CAT}) //限定两个值
    @Target({ElementType.PARAMETER,ElementType.FIELD})
    @Retention(RetentionPolicy.SOURCE)
    @interface pet{

    }

    @pet
    private int myPet;

    /**
     * 用pet注解修饰后,该方法就只能接收由pet注解限定的两个参数值,
     * 如果传入其他的数值,会出现警告
     * @param pet
     */
    public void havePet(@pet int pet){
        myPet = pet;
    }

    //使用系统已经定义的注解
    /**
     * 只能传入drawable资源ID
     * @param id
     */
    public void haveDraw(@DrawableRes int id){

    }
}
2、APT注解处理器

APT的全程是Annotation Processor Tools,翻译为注解处理器,注解处理器是javac自带的一个工具,用来在编译期间扫描注解信息,并将信息交由注解处理器处理。

注解处理器是对注解应用最为广泛的场景,在Glide、EventBus3、Butterknifer、Tinker、Arouter等常用框架中都有注解处理器的身影。

3、字节码操作(字节码插桩)

定义为CLASS的注解,会被保留在class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解),此时完全符合此种注解的使用场景为字节码操作,如Aspect、热修复Roubust中应用才场景。所谓字节码操作,是直接修改class文件已达到修改代码执行逻辑的目的。

假设想在有一个模块,其中一部分方法需要登陆后才可以操作,其余的方法不需要登陆就可操作,如果挨个方法做判断,太繁琐,此时我们就可以借助AOP(面向切面编程)的思想将模块中的所有功能进行划分,需要登陆和不需要登陆两部分,即两个切面,定义注解来区分这两个切面。除此之后,还可以进行运行期权限判断等。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login{

}

@Login
public void jumpA(){
    ///以下是登陆后才可调用的操作
}

public void jumpB(){
    //以下是不需要登陆就可以调用的操作
    
}

在以上代码中,编译生成的字节码中jumpA方法会被修改为如下内容:

@Login
public void jumpA(){
    if(this.isLogin){ ///如果已经登陆
        ///以下是登陆后才可调用的操作
    }else{
        ///如果未登录,跳转到登陆界面
    }
}
4、RUNTIME 注解保留到运行期,可以通过反射来获取和应用注解中的所有信息

反射是Java被称为动态语言的关键,反射是运行状态中,对于任何一个类,都能够知道这个类的所有属性和方法,对于任何一个对象,都能够调用他的任意属性和方法,并且能够改变他的属性。

Java 的反射机制提供了以下功能

  • 在运行时构造任意一个类的对象
  • 在运行时获取或者修改任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法和属性

Class

反射始于Class,Class是一个了类,封装了当前对象所对应的类的信息,一个类中的属性、方法和构造器等,Class就是用来描述类的类。一个类可以有多个对象,但是一个类在JVM中只会有一个Class实例。

获得Class对象的三种方式:

  • 通过类名获取 类名.class
  • 通过对象获取 对象名.getClass()
  • 通过全类名获取 Class.forName(全类名) 或者 ClassLoader.loadClass(全类名)

判断是否为某个类的实例

我们可以通过instanceof关键字来判断是否为某个类的实例,同时我们也可以借助反射中的Class对象的isInstance()方法来判断是否为某个类的实例,他是一个native方法。

public boolean isAssignableFrom(Class<?> cls)

通过反射创建对象的两种方式

  • 使用Class对象的newInstance()方法来创建Class对象对应类的实例

    Class<?> c = String.class;
    Object str = c.newInstance();
    
  • 先通过Class对象获取指定的构造器对象,再调用构造器对象的newInstance方法来创建实例。这种方法可以用指定的构造器来构造类的实例。

    //获取String所对应的Class对象
    Class<?> c = String.class;
    //获取String类带一个String参数的构造器
    Constructor constructor = c.getConstructor(String.class);
    //根据构造器创建实例
    Object obj = constructor.newInstance("test");
    

反射获取构造器信息

Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的public构造函数(包括父类) 
                                                                     Constructor[] getConstructors() -- 获得类的所有公共构造函数 
                                                                     Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(包括私有) 
                                                                     Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)

反射获取类的成员变量信息

Field getField(String name) -- 获得命名的公共字段 
Field[] getFields() -- 获得类的所有公共字段 
Field getDeclaredField(String name) -- 获得类声明的命名的字段 
Field[] getDeclaredFields() -- 获得类声明的所有字段

反射获取类的方法信息

Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法 
Method[] getMethods() -- 获得类的所有公共方法 
Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法 
Method[] getDeclaredMethods() -- 获得类声明的所有方法

获取类的方法后,可以使用invoke()来调用这个方法。

利用反射创建数组

数组在Java里比较特殊的一种类型,它可以赋值给一个Object Reference,其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,他的原型是:

public static Object newInstance(Class<?> componentType, int length);

基于注解和反射完成View 的注入

思路

定义注解,标识需要借助反射完成初始化的View变量,所以注解的Target为Field,Retention为RUNTIME。在实现初始化的方法中,获取Activity的Class对象,反射获取所有的变量,然后遍历变量,并且筛选出被定义的注解修饰的变量,如果被匹配的注解标识,获取注解并得到View的ID,此后有两种方式完成初始化,一是直接调用传入的Activity的对象的findViewById方法完成View的初始化,二是反射获取findViewById方法,然后借助invoke调用完成初始化。以下是关键代码:

public class InjectUtils {
    private static final String TAG = "InjectUtils";
    /**
     * 利用反射获取Activity的所有成员,筛选出被规定的注解修饰的变量,获取ID值,然后调用findViewById即可实例化View
     * @param activity
     */
    public static void injectView(Activity activity){
        Class<?> clazz = activity.getClass();
        //获取所有的Fields
        Field[] fields = clazz.getDeclaredFields();
        //遍历变量
        for (Field field : fields) {
            //判断变量是否被InjectView注解修饰
            if(field.isAnnotationPresent(InjectView.class)){
                InjectView injectView = field.getAnnotation(InjectView.class);
                int value = injectView.value();
                Log.d(TAG,"value = "  + value);
                try {
                    //方法1
//                    Method findViewById = clazz.getMethod("findViewById",int.class);
//                    findViewById.setAccessible(true);
//                    Object view = findViewById.invoke(activity,value);
//                    field.setAccessible(true);
//                    field.set(activity,view);
                    //方法2
                    Object view = activity.findViewById(value);
                    field.setAccessible(true);
                    field.set(activity,view);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

定义的注解为:

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

https://github.com/lxj-helloworld/annotation

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