深入理解Java注解——注解基础

Posted yuxiyu!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解Java注解——注解基础相关的知识,希望对你有一定的参考价值。

一直以来对Java注解的理解都不是特别深刻,但是在多年的软件开发生涯中接触了不少注解相关的东西,所以有必要深入理解一下Java注解知识,通过本篇博客记录学习Java注解的一些知识点。

深入理解Java注解(二)——JavaPoet使用
深入理解Java注解(三)——编译时注解实战

什么是Java注解

举个例子,在Java开发中,我们会使用@Override标记一个被子类复写的方法,使用@Deprecated标记一个方法或者一个类表示方法或类已被弃用,不再推荐使用。这里的@Override @Deprecated就是Java注解,查看@Override源码如下:

package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

下面对以上代码做如下说明:

  1. 使用@interface声明一个注解,比如上面的代码中使用public @interface Override声明了@Override这个注解。

  2. @Override这个注解上又使用了@Target@Retention做了注解,这种针对注解的注解称为元注解。

  3. @Target表示注解作用的目标,它的取值是一个ElementType数组,ElementType是一个枚举类型:

    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
    }
    

    关于@Taget的取值,使用比较多的分别是TYPE FIELD METHODTYPE表示注解作用在类、接口或枚举类上;FIELD表示注解作用在类的某个成员变量上,包括枚举类中的常量;METHOD则表示注解作用在方法上。其他类型的取值作用范围可以查看源码中对应的注释。需要注意的是,@Target的取值是一个ElementType数组,这表示使用@Target这个元注解去注解其他注解时,可以有多个取值,比如这种方式:@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})

  4. @Retention表示注解的保留范围,它的取值是一个RetentionPolicy枚举类型,其源码如下:

    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
    }
    

    SOURCE表示注解保留到源码阶段,一旦源码被编译成class文件,则注解就不存在了;CLASS表示注解保留到字节码阶段,当源码被编译成字节码时,字节码中依然有注解,但是一旦字节码被虚拟机解释执行,则注解不存在;RUNTIME表示注解保留到运行时,即虚拟机解释执行字节码时注解依然存在。

通过以上的代码及解释,对注解有个大概的了解了,我个人理解是:注解是使用@interface标记的一个类(实际上注解类就是一个继承了Annotation接口的接口),它可以作用在类、方法或变量上,作为一种修饰。

但是,只作为修饰是不够的,注解的强大之处在于使用很少的代码实现非常强大的功能,在后续篇幅中会详细记录。

自定义注解

上面的代码中主要是使用JDK中提供的@Target@Deprecated元注解做例子说明注解是个啥,实际开发中我们可以自定义自己的注解,比如下面的代码定义了一个自定义的注解:

// 表示自定义注解作用在类、成员变量和方法上
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
// 表示注解保留到程序运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String name() default "";
}

上面的自定义注解代码中,比较难以理解的是花括号中的String name() default "";这句,上面说了实际上使用@interface定义的注解就是一个继承了Annotation接口的接口。

在Java中,定义接口时可以在接口中定义方法但不能有实现,可以在接口中定义某个成员变量,这个变量默认是public static的,但是注解中的这段String name() default "";即不像是声明一个变量也不像是声明一个方法,而且还有default ""这种不常见的写法。

为了理解这种写法,还是先来看看上面写的自定义注解怎么使用吧,以下代码在一个类和类的成员变量及类的成员方法上使用了自定义注解:

@MyAnnotation(name = "zhangsan")
public class Person {
    @MyAnnotation
    String name;
    
    @MyAnnotation
    public void sayHello() {
        System.out.println("hello, this is " + name);
    }
}

由于@MyAnnotation注解被元注解@Target修饰了:@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}),所以在上面的Person类中,可以在类及类的成员变量和成员方法上都使用@MyAnnotation注解,而且在使用该注解时,可以指定name的值如:@MyAnnotation(name = "zhangsan"),也可以不指定name的值,没有指定name的值时,使用的就是定义注解时的默认值了。

这里再回到定义自定义注解时的这种写法:String name() default "";,这种写法我觉得可以理解成是在接口中定义了一个方法,方法名为name,返回值为String,且方法的默认返回值为""

注意事项:

在注解中定义的方法,其返回值是有要求的,注解中的方法并非支持所有类型的返回值,只支持如下几种数据类型:
(1)8种基本数据类型;
(2)String、枚举、Class;
(3)其他注解类型
(4)以上类型的一维数组

下面的代码展示了目前可以定义在注解中的返回值类型:

// 表示自定义注解作用在类、成员变量和方法上
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
// 表示注解保留到程序运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String name() default "";
    Target target();
    int age();
    float score() default 1f;
    double[] lengths() default {10, 20};
    Class<StringBuilder> cls();
    // StringBuilder sb(); // 非法,IDE会提示"Invalid type 'StringBuilder' for annotation member"
}

另外,如果一个自定义注解中只定义了一个方法,且方法名为value,比如下面的代码:

public @interface MyAnnotation {
    int value();
}

那么在使用该注解时,可以不指定value直接在圆括号内写方法返回值,比如:

@MyAnnotation(10)
public void sayHello() {
    System.out.println("hello, this is " + name);
}

但是如果注解中定义的方法名不为value或者定义的方法不止一个,那就必须在使用注解时按这种格式写@MyAnnotation(xxx = XXX):

@MyAnnotation(value = 10)
public void sayHello() {
    System.out.println("hello, this is " + name);
}

请注意,到目前为止,还只是说明了自定义注解怎么定义,怎么使用在一个类上面,但是上面的示例代码中,在Person类中使用自定义注解还并不能产生任何效果,关于注解的解析,在后文会做更详细的说明,主要分为编译时注解和运行时注解。

注解的解析

注解定义好了之后必须修饰在方法、类或者某个变量上,但是这些仅仅是第一步,最关键的其实是注解的解析,必须通过某些方法解析出注解上携带的数据,才能让注解发挥作用,对注解的解析主要分两种,一种是编译时注解,主要在代码编译过程中解析注解并生成新代码,从而达到某个功能;另一种是运行时注解,通过反射将注解中的信息提取出来。实际开发过程中,以及某些知名的开源库(如ButterKnife)中都是使用的编译时注解这种方式,下面就记录下这两种不同的解析注解的方式。

运行时注解

运行时注解主要通过Java的反射机制,在程序运行过程中解析出注解中携带的信息,下面的代码展示了使用反射解析注解的方法:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
    String value() default "";
}

@MyAnnotation("zhangsan")
class Person {
    @MyAnnotation("haha")
    public void sayHello() {
        System.out.println("hello");
    }
}

public class Test {
    public static void main(String[] args) {
        MyAnnotation myAnnotation = Person.class.getAnnotation(MyAnnotation.class);
        System.out.println(myAnnotation.value()); // 输出"zhangsan"

        try {
            Method m = Person.class.getDeclaredMethod("sayHello");
            MyAnnotation annotation = m.getAnnotation(MyAnnotation.class);
            System.out.println(annotation.value()); // 输出"haha"
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

使用反射解析注解中的信息这种方法并不常用,主要是因为反射效率低,而且定义注解时需要设置保留策略为RetentionPolicy.RUNTIME即注解保留到代码运行时,若将上面的策略改为SOURCE或者CLASS后再次运行程序,会发现代码会抛出空指针异常,这是因为使用SOURCE或者CLASS这种策略时,代码在运行过程中已经没有注解了,通过Person.class.getAnnotation(MyAnnotation.class);获取注解得到的是null对象。运行时注解的方式由于效率低,用得不多,就不花过多篇幅去记录了,主要的精力还是放到编译时注解这种方式上。

编译时注解

编译时注解是程序在编译过程中解析注解信息的,这种解析注解的方式较运行时注解通过反射解析更为麻烦一些,但是优点是没有反射带来的性能损耗,android界大名鼎鼎的ButterKinfe库就是使用编译时注解实现的,其可以通过@BindView(id = R.id.xxx)这种简单的注解修饰达到省去写大量findViewById()这种没有营养的代码,下面就来看看编译时注解是怎么实现的吧。

本篇中使用Android Studio创建一个Android工程用于演示如何使用编译时注解。

  1. 使用Android Studio创建一个空的Android工程。

  2. 在项目上右键,创建一个新的Java Library命名为annotation,注意下图左侧,选择Java or Kotlin Library即可,在这个Lib中我们会创建一个自定义的注解,命名为MyAnnotation。
    在这里插入图片描述
    由于我已经创建了annotation这个Module,所以上图中会有提示"Module annotation already exists"

  3. 在上面创建的MyAnnotation类中编辑如下代码:

    package com.example.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface MyAnnotation {
        String value() default "";
    }
    
  4. 按照第二步的方法再创建一个名为processor的Java Library,该Lib的作用主要是创建自定义的注解处理器,如下图所示:
    在这里插入图片描述
    在创建的processor module下编辑build.gradle文件,添加对annotation库的依赖,如下代码所示:

    plugins {
        id 'java-library'
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
    
    dependencies {
        implementation project(':annotation')
    }
    

    再编辑MyProcessor类,其代码如下:

    import com.example.annotation.MyAnnotation;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.TypeElement;
    import javax.tools.Diagnostic;
    
    public class MyProcessor extends AbstractProcessor {
    
        // 打印日志用
        private Messager messager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
            messager.printMessage(Diagnostic.Kind.NOTE, "---------->init");
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            // 返回支持的注解类型
            Set<String> types = new HashSet<>();
            types.add(MyAnnotation.class.getCanonicalName());
            return types;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            // 返回支持的源代码版本
            return SourceVersion.latestSupported();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            // 注解处理主要在该方法中
            messager.printMessage(Diagnostic.Kind.NOTE, "---------->process");
            return true;
        }
    }
    
  5. 在项目的app module下依赖annotation和processor,编辑app module下的build.gradle文件,添加对这两个Lib的依赖:

    dependencies {
    	...
    
        implementation project(':annotation')
        annotationProcessor project(':processor')
    }
    

    注意对processor的依赖一定要使用annotationProcessor而不是implementation,只有使用annotationProcessor才能让注解处理器生效。

  6. 注册注解处理器。注册注解处理器有两种方法,第一种是直接在processor module的src/main目录下创建resources/META-INF/services目录,然后在该目录下新建一个文件,文件名固定为javax.annotation.processing.Processor,文件内容则是MyProcessor类的全路径,比如该例子中的路径是:com.example.processor.MyProcessor,如果你的项目中有多个Processor类,依次换行添加到javax.annotation.processing.Processor这个文件中即可。另一种注册注解处理器的方法是使用Google的AutoService,其github地址为:https://github.com/google/auto/tree/master/service

  7. 在Android Studio中构建项目,点击菜单栏上的类似锤子的图标后,在Build视图中可以看到上面MyProcessor类中输出的日志信息:
    在这里插入图片描述
    出现以上日志打印证明自定义的注解处理器可以正常工作了,项目在编译过程中就会自动调用我们编写的注解处理器完成对注解的解析工作,但是MyProcessor类中的process方法还没有具体的实现,一般会在该方法中处理注解的解析,以及Java代码的生成等操作,关于这些内容,会放到下一篇中记录。
    本篇中编译时注解的Demo可以在这里查看:https://github.com/yubo725/test-apt/tree/v0.1

以上是关于深入理解Java注解——注解基础的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java注解——注解基础

深入理解Java注解类型

深入理解java注解的实现原理

java注解基础入门

深入理解Java 注解原理

深入理解java 注解(Annotation)重版