注解及其三类应用场景

注解(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容