• Android自定义注解


    1.自定义注解

    注解Annotation是JDK5.0引入的一种注释机制。常见的@override就是一个注解。类、方法、变量、参数、包都可以被注解。注解对注解的代码没有直接影响,之所以产生作用,是对其解析后做了相应的处理。

    注解其实就是一个标注而已,后期通过这个标注拿到对应的函数、变量,然后做一些操作(通常是反射)。所以,注解之所以产生作用,是对其解析后做了相应的处理。注解仅仅只是个标记罢了。

    定义注解需要使用关键字:@interface,定义注解的格式为:

    public @interface xxx {

    }

    比如,定义一个注解,名字是MyAnnotation:

    @Documented

    @Target(ElementType.TYPE)

    @Retention(RetentionPolicy.RUNTIME)

    public @interface MyAnnotation {

    }

    定义好注解后,就可以通过@MyAnnotation在代码中使用它了:

    @MyAnnotation

    public void annotationTest() {

    }

    @interface、@Documented、@Target、@Retention都是用来修饰MyAnnotation的,它们的含义分别为:

    @interface定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是一个Annotation。定义注解时,@interface是必需的。

    @Documented表示注解是否出现在javadoc中,缺省则表示不出现在javadoc中。

    @Target用于指定Annotation的类型,如缺失则Annotation可用于任何地方,否则用于指定的地方。

    @Retention用于指定Annotation的策略。

     

    2.元注解

    @MyAnnotation的注解中又引入了其它注解,如 @Target、@Retention,作用在其它注解上的注解称为元注解。

    元注解有四个:@Target、@Retention、@Documented、@Inherited

    ①@Target(ElementType.TYPE),用于描述注解的定义范围(即:被描述的注解可以用在什么地方)。注解可被用于packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在注解类型的声明中使用了@Target可更加明晰其修饰的目标。

    package java.lang.annotation;

    public enum ElementType {

        TYPE, //应用于类的任何元素

        FIELD, //应用于属性字段、枚举常量

        METHOD, //应用于方法

        PARAMETER, //应用于方法的参数

        CONSTRUCTOR, //应用于构造函数

        LOCAL_VARIABLE, //应用于局部变量

        ANNOTATION_TYPE, //应用于注解类型,比如@Retention注解中使用该属性

        PACKAGE, //应用于包声明

        TYPE_PARAMETER, //应用于类型泛型,即泛型方法、泛型类、泛型接口(jdk1.8)

        TYPE_USE //应用于类型使用,可用于标注任意类型,除了class(jdk1.8)

    }

    如果想同时定义在成员变量和成员方法上,需要用大括号包起来,并且中间用逗号隔开:

    @Target({ElementType.FIELD, ElementType.METHOD})

    public @interface MyAnnotation {

    }

    ②@Retention(RetentionPolicy.RUNTIME),用于定义注解的生命周期,也可以理解为存储方式

    package java.lang.annotation;

    public enum RetentionPolicy {

        SOURCE, //标记的注解仅保留在源级别中,并被编译器忽略。即只在源码中可用,编译时丢弃,class字节码文件中不包含

        CLASS, //默认策略,标记的注解在编译时由编译器保留,但虚拟机会忽略。即在源码和字节码中可用——编译时保留,class字节码中存在,运行时丢弃,运行时无法获得

        RUNTIME //标记的注解由虚拟机保留,因此运行时环境可以使用它。即编译时保留,运行时保留,可通过反射获得(在源码、字节码、运行时均可用)

    }

    ③@Documented,标记注解是否出现在Javadoc 中,缺省则不出现在javadoc中。Documented是一个标记注解,没有成员。

    ④@Inherited,标记标注的Annotation是否具有继承性,类使用@Inherited修饰的注解,子类会继承该注解,注意接口或者父类的方法使用@Inherited修饰,子类不会继承这个注解。

     

    3.注解作用

    ①Annotation具有"让编译器进行编译检查的作用"。例如 @Override @Deprecated @SuppressWarnings 都具有编译检查作用。

    比如某个方法被@Override标注,则意味着该方法会覆盖父类中的同名方法,如方法被 @Override 修饰,父类中没有同名方法,则编译器报错。

    比如某个方法被@Deprecated标注,则意味着该方法不再被建议使用,如开发人员试图使用,编译器则会给出相应的提示信息。

    ②在编译和运行时解析和使用Annotation。

    获取和解析注解内容,做相应的逻辑处理。比如很多第三方框架的实现ButterKnift,EventBus等是基于此方法。

     

    4.给注解定义参数

    @MyAnnotation只是定义了一个注解,但是不能传任何信息,只是相当于一个标签,现在来给注解定义参数:

    @Target({ElementType.FIELD,ElementType.METHOD})

    @Retention(RetentionPolicy.RUNTIME)

    public @interface MyAnnotation {

        String name() default "张三"; //使用default定义默认值

        int age();

    }

    因为要用反射拿到注解参数,所以@Retention需要定义为RetentionPolicy.RUNTIME。

    然后开始传参:

    public class Test {

        //name有默认值,也可以不写

        @MyAnnotation(name = "李四",age = 18)

        private static int num;

        @MyAnnotation(name = "王五",age = 38)

        private static int age;

        public static void main(String[] args) throws InstantiationException, IllegalAccessException {

            System.out.println("赋值前: num: " + num+" age: "+age);

            //拿到类的字节码

            Class testClass = Test.class;

            //拿到所有成员变量

            for (Field declaredField : testClass.getDeclaredFields()) {

                //检测成员变量上是否有@MyAnnotation注解

                if (declaredField.isAnnotationPresent( MyAnnotation.class)) {

                    MyAnnotation annotation = declaredField.getAnnotation(MyAnnotation.class);

                    //获取到注解中的age的值

                    int age = annotation.age();

                    declaredField.set( testClass.newInstance(),age);

                }

            }

            System.out.println("赋值后: num: " + num+" age: "+age);

        }

    }

    运行结果:

    赋值前: num: 0 age: 0

    赋值后: num: 18 age: 38

     

    5.注解示例

    自定义一个注解,命名为MyAnnotation:

    @Retention(RetentionPolicy.RUNTIME)

    @interface MyAnnotation {

        String[] value() default "unknown";

    }

    使用MyAnnotation修饰testMethod方法:

    class TestAnnotation {

        @MyAnnotation(value = {"hello", "world"})

        public void testMethod() {

        }

    }

    运行时解析注解信息,做相应的逻辑处理:

    Class testAnnotationClass = TestAnnotation.class;

    Method method = testAnnotationClass.getMethod("testMethod");

    if (method.isAnnotationPresent( MyAnnotation.class)) { 

        MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);

        String[] values = myAnnotation.value();

        for (String str:values) {

            System.out.printf(str+", ");

       }

    }

    Annotation[] annotations = method.getAnnotations();

    for(Annotation annotation: annotations) {

        System.out.println(annotation);

    }

     

    4.运行时注解

    运⾏时注解是指在代码运⾏的过程中通过反射机制找到⾃定义的注解,然后做相应的事情。

    ⾃定义运⾏时注解⼤的⽅⾯分为两步:⼀个是申明注解、第⼆个是解析注解。

    ①申明注解

    申明注解步骤:

    1)通过@Retention(RetentionPolicy.RUNTIME)元注解确定注解是在运⾏的时候使⽤。需要注意的是,JVM 运行时通过反射注解处理的方式会对性能造成影响,日常开发中很少使用这样的方式。

    2)通过@Target确定注解是作⽤在什么上⾯的(变量、函数、类等)。

    3)确定注解需要的参数。

    以声明⼀个作⽤在变量上的BindView为例说明运⾏时注解:

    通常获取组件通过findViewById方式,如果引用组件比较多,需要写大量的findViewById重复代码。此时可以通过注解,实现Android依赖注入,定义一个注解@BindView:

    @Retention(RetentionPolicy.RUNTIME)

    @Target(ElementType.FIELD)

    public @interface BindView {

        int value() default 0;

    }

    ②注解解析

    运⾏时注解的解析简单的分为三个步骤:

    1)找到类对应的所有属性或者⽅法(⾄于是找类的属性还是⽅法就要看⾃定义的注解是定义⽅法上还是属性上了)。

    2)找到添加了注解的属性或者⽅法。

    3)做注解需要⾃定义的⼀些操作。

    接下来需要写代码通过反射的方式给变量赋值:

    @BindView(R.id.tv)//把id存到注解里

    private TextView tv;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        bind(this);//重要,调用此方法赋值变量

    }

    public void bind(final Activity activity){

        try {

            Class clazz = activity.getClass();

            Field[] fields = clazz.getDeclaredFields();//获取所有的变量

            for (Field field : fields){ //遍历所有的变量

                if (field.isAnnotationPresent( BindView.class)){  // 判断当前变量是否有BindView注解

                    BindView bindView = field.getAnnotation(BindView.class); //获取当前变量的BindView注解

                    int id = bindView.value();//把注解里的值取出来(就是上面的R.id.tv)

                    if (id != 0){

                        field.setAccessible(true);//暴力反射

                        field.set(activity,activity.findViewById( id)); //通过反射的方式赋值变量

                    }

                }

            }

        } catch (Exception e){

            e.printStackTrace();

        }

    }

    这样就通过注解完成了一个绑定View的操作。

     

    6.编译时注解

    编译时注解就是在编译的过程中⽤⼀个javac注解处理器扫描到⾃定义的注解,⽣成需要的⼀些⽂件(通常是java⽂件)。

    ⾃定义编译时注解的步骤:

    1)声明注解。

    2)编写注解处理器。

    3)⽣成⽂件(通常是JAVA⽂件)。

    ①声明注解

    编译时注解的声明和运⾏时注解的声明⼀样也是三步:

    1)通过@Retention(RetentionPolicy.TYPE)元注解确定注解是在编译的时候使⽤。

    2)通过@Target确定注解是作⽤在什么上⾯的(变量、函数、类等)。

    3)确定注解需要的参数。

    ⽐如⾃定义⼀个作⽤在类上的编译时注解Factory,并且这个注解是需要两个参数的,⼀个是Class类型,⼀个是String类型。

    @Target({ElementType.TYPE})

    @Retention(RetentionPolicy.CLASS)

    public @interface Factory {

        Class type();

        String id();

    }

    ②编写注解处理器

    和运⾏时注解的解析不⼀样,编译时注解的解析需要⾃⼰实现⼀个注解处理器。

    注解处理器(Annotation Processor)是javac的⼀个⼯具,它⽤来在编译时扫描和处理注解Annotation。⼀个注解的注解处理器,以Java代码(或者编译过的字节码)作为输⼊,⽣成⽂件(通常是.java⽂件)作为输出。⽽且这些⽣成的Java⽂件同开发者⼿动编写的Java源代码⼀样可以调⽤。(注意:不能修改已经存在的java⽂件代码)。

    注解处理器所做的⼯作,就是在代码编译的过程中,找到指定的注解。然后增加⾃⼰特定的逻辑做出相应的处理(通常是⽣成JAVA⽂件)。

    注解处理器的写法有固定套路的,两步:

    1)注册注解处理器(这个注解器就是第⼆步⾃定义的类)

    2)⾃定义注解处理器类继承AbstractProcessor

    1)注册注解处理器

    打包注解处理器的时候需要⼀个特殊的⽂件 javax.annotation.processing.Processor 在 META-INF/services 路径下。在javax.annotation.processing.Processor⽂件⾥⾯写上⾃定义注解处理器的全称(包加类的名字)如果有多个注解处理器换⾏写⼊就可以。

    google为了⽅便注册注解处理器,提供了⼀个注册处理器的库@AutoService(Processor.class)来简化操作。只需要在⾃定义的注解处理器类前⾯加上google的这个注解,在打包的时候就会⾃动⽣成javax.annotation.processing.Processor⽂件,写⼊相的信息。不需要⼿动去创建。当然了如果你想使⽤google的这个注解处理器的库,必须加上下⾯的依赖。

    compile 'com.google.auto.service:auto-service:1.0-rc3'

    ⽐如下⾯的这段代码就使⽤上了google提供的这个注解器处理库,会⾃动注册注解处理器。

    @AutoService(Processor.class)

    public class FactoryProcessor extends AbstractProcessor {

        ...

    }

    2)⾃定义注解处理器类

    ⾃定义注解处理器类⼀定要继承AbstractProcessor,否则找不到需要的注解。在这个类⾥⾯找到需要的注解,做出相应的处理。

    关于AbstractProcessor⾥⾯的⼀些函数也做⼀个简单的介绍。

    /**每个Annotation Processor必须有⼀个空的构造函数。编译期间,init()会⾃动被注解处理⼯具调⽤,并传⼊ProcessingEnvironment参数,通过该参数可以获取到很多有⽤的⼯具类(Element,Filer,Messager等)*/

    @Override

    public synchronized void init( ProcessingEnvironment processingEnvironment) {

        super.init(processingEnvironment);

    }

    /**⽤于指定⾃定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),注解(Annotation)指定必须是完整的包名+类名*/

    @Override

    public Set getSupportedAnnotationTypes() {

        return super.getSupportedAnnotationTypes();

    }

    /**Annotation Processor扫描出的结果会存储进roundEnvironment中,可以在这⾥获取到注解内容,编写你的操作逻辑。注意:process()函数中不能直接进⾏异常抛出,否则程序会异常崩溃*/

    @Override

    public boolean process(Set set, RoundEnvironment roundEnvironment) {

        return false;

    }

    注解处理器的核⼼是process()⽅法(需要重写AbstractProcessor类的该⽅法),⽽process()⽅法的核⼼是Element元素。Element 代表程序的元素,在注解处理过程中,编译器会扫描所有的Java源⽂件,并将源码中的每⼀个部分都看作特定类型的Element。它可以代表包、类、接⼝、⽅法、字段等多种元素种类。所有Element肯定是有好⼏个⼦类。

    ③⽣成⽂件

    ⽣成⽂件,通常是⽣成⼀个java⽂件。直接调⽤帮助类Filer的createSourceFile()函数就可以创建⼀个java⽂件。之后就是在这个java ⽂件⾥⾯写⼊需要的内容了。

     

    编译时注解的核心是APT(Annotation Processing Tools),原理是代码中设置注解,在编译时检查AbstractProcessor子类,并调用该类型的process函数,将所有添加注解的元素传递到process中,在process做相应逻辑处理。

  • 相关阅读:
    【Android】修改aar包里的jar包里的class文件内容
    scratch保护环境 2023年5月中国电子学会图形化编程 少儿编程 scratch编程等级考试一级真题和答案解析
    Web前端:JavaScript与Java和PHP的比较
    2023最新版JavaSE教程——第3天:流程控制语句之顺序语句与分支语句
    【离散数学】——刷题题库(范式)
    快应用接入帐号服务指导
    nginx主要作用三个(虚拟主机+反向代理+upsteam调度分发)
    基于STM32无人超市消费系统设计
    阿里二面:有一个 List 对象集合,如何优雅地返回给前端?
    Worthington核糖核酸测定详细攻略
  • 原文地址:https://blog.csdn.net/zenmela2011/article/details/126088276