Java中的Annotation详解

【传智播客.黑马程序员训练营成都中心】

前言:

作为Java开发人员,经常能在代码中看到注解(Annotation),有的是JAVA内置的注解,或者是在使用一些三方的开源框架的代码时候看到一些别人的自定义注解。如果你对注解不了解或者不知道如何使用,那么你在用这些三方框架的时候甚至在自己写源码修改源码的时候就会变得更加的困难和举步维艰。通过该篇文章可以基本的将注解Annotation的原理以及使用了解清楚,更加有利于你下一步比如使用Retrofit进行网络的开发打下基础。

1.概念

官方的定义:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

注解Annotation是JDK5.0的新特性,是一种能够添加到 Java 源代码的语法元数据。类、方法、变量、参数、包都可以被注解,可用来将信息元数据与程序元素进行关联。Annotation 中文常译为“注解”。

2.作用

Annotations have a number of uses, among them:

Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.

Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.

Runtime processing — Some annotations are available to be examined at runtime.

a. 标记,用于告诉编译器一些信息

b. 编译时动态处理,如动态生成代码

c. 运行时动态处理,如得到注解信息

Java注解可以用在构建期。当构建我们的工程时,构建进程会编译源码、生成xml文件,打包编译后的代码和文件到jar包。构建过程一般由构建工具自动完成,常用的构建工具有ant、maven。构建工具在构建时会自动扫描我们的代码,当遇到构建期注解时,会根据注解的内容生成源码或者其它文件

3.注解的格式

a)一个java注解由一个@符后面跟一个字符串构成,类似于这样:

@Entity

b)java注解中一般包含一些元素,这些元素类似于属性或者参数,可以用来设置值,比如我们有一个包含两个元素的@Entity注解:

@Entity(userName = "zphuan", userAge = "100")

该注解中有两个元素:userName,userAge,分别赋予了对应的元素值。

4.注解的位置

注解可以用于描述一个类、接口、方法、方法参数、字段、局部变量等。
比如:

a)方法上:

    @Override
    void mySuperMethod() { ... }
    
    @SuppressWarnings(value = "unchecked")
    void myMethod() { ... }

b)类上:

    @Author(
       name = "zphuan",
       date = "3/24/2017"
    )
    class MyClass() { ... }

使用的细节:

1.如果注解没有参数则不用写参数体,比如@Override

2.如果只有一个参数比如named value,则参数名可以省略,比如:

    @SuppressWarnings("unchecked")
    void myMethod() { ... }

3.也可以同时使用多个注解来标示,比如:

    @Author(name = "Jane Doe")
    @EBook
    class MyClass { ... }

4.可以重复使用注解,不过只有在java SE 8 才支持。比如:

    @Author(name = "Jane Doe")
    @Author(name = "John Smith")
    class MyClass { ... }

标明两个作者对该类的书写,一个管理员,一个开发者等。

  • 注意:如果想自定义一个可以用于重复使用的注解(自定义注解在后面会涉及),记得加上@Repeatable,比如:

      @Repeatable(Schedules.class)
      public @interface Schedule {
        String dayOfMonth() default "first";
        String dayOfWeek() default "Mon";
        int hour() default 12;
      }
    

5.Java内置注解

Java本身提供了三个内置注解,他们分别是:

  • @Deprecated
  • @Override
  • @SuppressWarnings

@Deprecated可以用来描述一个类、方法或者字段,表示java不赞成使用这些被描述的对象,如果我们使用了这些类、方法或者字段,编译器会给我们警告。

@Override注解是一个编译时注解,它主要用在一个子类的方法中,当被注解的子类的方法在父类中找不到与之匹配的方法时,编译器会报错。

@SuppressWarnings注解的作用是使编译器忽略掉编译器警告。比如,如果我们的一个方法调用了一个@Deprecated方法,或者做了一个不安全的类型转换,此时编译器会生成一个警告。如果我们不想看到这些警告,我们就可以使用@SuppressWarnings注解忽略掉这些警告.

6.自定义注解

很多的注解在代码上可以用来替代注释的存在!

为什么这样说?我举一个例子:如果你所在的一家开发软件的it公司有这样的要求,说你写的每一个class类都必须有以下这些的说明:

    public class Generation3List extends Generation2List {

       // Author: zphuan
       // Date: 3/17/2002
       // Current revision: 6
       // Last modified: 4/12/2004
       // By: Jane Doe
       // Reviewers: Alice, Bill, Cindy
    
       // class code goes here
    
    }

那么可能张三使用注释的方式,李四使用文档注释的方式,王五使用注解的方式,每一个人的写法和顺序可能都不一样,甚至刚来的人还不知这个代码上次是谁修改的,有哪些人在调用和关联这个类,没办法统一,那么作为一个团队的leader你就可以在项目的初期创建一个自定义的注解Annotation来规范化所有class的申明!

针对上面的这个需求,接下来我就来创建一个自定义的注解

    @interface ClassPreamble {
       String author();
       String date();
       int currentRevision() default 1;
       String lastModified() default "N/A";
       String lastModifiedBy() default "N/A";
       // Note use of array
       String[] reviewers();
    }

注意,@interface关键字就代表这是一个注解类型,所以使用@interface关键字就可以创建注解了。注解中的每个元素定义类似于接口中的方法定义。每个元素定义包含一个数据类型和名称,注解元素的数据类型可以是java基本数据类型、String、数组,但不能是复杂对象类型。

我们可以通过default关键字为某个元素设置默认值,当一个元素被设置默认值之后,这个元素便成了注解的可选元素。

那么接下来我们就可以使用注解来演示每一个类的描述了:

        @ClassPreamble (
           author = "zphuan",
           date = "3/17/2002",
           currentRevision = 6,
           lastModified = "4/12/2004",
           lastModifiedBy = "Jane Doe",
           // Note array notation
           reviewers = {"Alice", "Bob", "Cindy"}
        )
        public class Generation3List extends Generation2List {
        
        // class code goes here
        
    }

这样写就能进行一个类的描述的规范化和统一了。

那么可能有人会问了:这样用注解的方式写没有文档注释方便,可以在使用该类的时候直接用看到该类的描述。

在java中相信大家对@Documented这个注解非常熟悉吧?没错,这个注解的作用就是java用来提供给大家将你的信息注入到java-doc文档中,作为api的文档描述查看。那么我们完全可以在我们自定义的注解上使用@Documented,比如:

    // import this to use @Documented
    import java.lang.annotation.*;
    
    @Documented
    @interface ClassPreamble {
    
       // Annotation element definitions
       
    }

这里的@Documented注解为什么能直接作用于我们自定义的注解?这里引入我们下面的元注解。

7.元注解

元注解就是用来描述注解的注解,在java中有以下几个元注解:

  • @Documented
    作用是告诉JavaDoc工具,当前注解本身也要显示在Java Doc中。比如上面我写的自定义注解。
  • @Retention
    用来定义当前注解的作用范围,有以下三个范围可选:
    1.RetentionPolicy.SOURCE : 注解只存在于源码中,不会存在于.class文件中,在编译时会被忽略掉
    2.RetentionPolicy.CLASS:注解只存在于.class文件中,在编译期有效,但是在运行期会被忽略掉,这也是默认范围
    3.RetentionPolicy.RUNTIME:在运行期有效,JVM在运行期通过反射获得注解信息
  • @Target
    用于指定注解作用于java的哪些元素,未标注则表示可修饰所有.有以下这些元素类型可供选择:
    ElementType.ANNOTATION_TYPE can be applied to an annotation type.
    ElementType.CONSTRUCTOR can be applied to a constructor.
    ElementType.FIELD can be applied to a field or property.
    ElementType.LOCAL_VARIABLE can be applied to a local variable.
    ElementType.METHOD can be applied to a method-level annotation.
    ElementType.PACKAGE can be applied to a package declaration.
    ElementType.PARAMETER can be applied to the parameters of a method.
    ElementType.TYPE can be applied to any element of a class.

通过名字就可以看出来他们的元素类型,不过有两个需要特别说明下:

1.ElementType.ANNOTATION_TYPE:元注解类型,只能用来注解其它的注解,例如@Target和@Retention。

2.ElementType.TYPE:可以用来注解任何类型的java类,如类、接口、枚举、或者注解类。

  • @Inherited

注解表示当前注解会被注解类的子类继承。比如有一个自定义注解:

    @Inherited
    public @interface InheritedAnnotation{
        
    }

如果一个类使用了上面这个注解:

    @InheritedAnnotation
    @ClassPreamble(
           author = "zphuan",
           date = "3/17/2002",
           currentRevision = 6,
           lastModified = "4/12/2004",
           lastModifiedBy = "Jane Doe",
           // Note array notation
           reviewers = {"Alice", "Bob", "Cindy"}
            )
    public class Generation3List {
    }

那么Generation3List的子类也会继承这个注解。

  1. Annotation解析

注解的详细使用已经介绍完了,那么在现实开发中我们一般如何使用呢?一般我们会自定义一些注解,来简化和设计我们的代码,然后在运行时通过反射机制获取到注解的信息来使用。

8.1 运行时Annotation解析

运行时 Annotation 指 @Retention 为 RUNTIME 的 Annotation。

比如下面是我自己写的一个运行时自定义注解:

    import com.zphuan.MyAnnotation.ClassPreamble;
    import com.zphuan.MyAnnotation.InheritedAnnotation;
    
    @InheritedAnnotation
    @ClassPreamble(
           author = "zphuan",
           date = "3/17/2002",
           currentRevision = 6,
           lastModified = "4/12/2004",
           lastModifiedBy = "Jane Doe",
           // Note array notation
           reviewers = {"Alice", "Bob", "Cindy"}
            )
    public class Generation3List {
    }

现在当程序运行起来的时候我想要获取到Generation3List中的ClassPreamble注解中的信息:author,date,currentRevision。实现的方式如下:

    import com.zphuan.MyAnnotation.ClassPreamble;

    public class Main {
        public static void main(String[] args) {
            //通过反射获得Generation3List的注解信息
            ClassPreamble preamble = (ClassPreamble) Generation3List.class.getAnnotation(ClassPreamble.class);
            System.out.println("preamble author:"+preamble.author());
            System.out.println("preamble currentRevision:"+preamble.currentRevision());
            System.out.println("preamble date:"+preamble.date());
        }
    }

打印的结果为:

再举个获取方法注解信息的例子。
我定义了一个用于方法上的运行时注解:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(value = { ElementType.METHOD })
    public @interface MethodAnnotation{
        String author();
        String date();
    }

该注解作用与下面这个类中的test方法上:

    public class Generation3List {
        
        @MethodAnnotation(author = "zphuan", date = "3/26/2017")
        public void test(){
        }
    }

现在我想要获取到该方法上的注解信息,可以这样反射获取:

    try {
        Class clazz = Class.forName("com.zphuan.Generation3List");
        for(Method method : clazz.getMethods()){
            MethodAnnotation methodAnnotation = method.getAnnotation(MethodAnnotation.class);
            if(methodAnnotation!=null){
                System.out.println("author:"+methodAnnotation.author());
                System.out.println("date:"+methodAnnotation.date());
            }
        }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

最后得到的结果为:

当然获取其他比如Field,PARAMETER等信息也是通过该反射获取即可,主要记住以下三个方法的作用即可:

  • getAnnotation(AnnotationName.class) 表示得到该 Target 某个 Annotation 的信息,因为一个 Target 可以被多个 Annotation 修饰
  • getAnnotations() 则表示得到该 Target 所有 Annotation
  • isAnnotationPresent(AnnotationName.class) 表示该 Target 是否被某个 Annotation 修饰

8.2 编译时Annotation解析

(1) 编译时 Annotation 指 @Retention 为 CLASS 的 Annotation,甴编译器自动解析。需要做的
a. 自定义类集成自 AbstractProcessor
b. 重写其中的 process 函数

注意:

编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理

(2) 创建一个注解类型为CLASS的Annotation,如下:

    @Retention(RetentionPolicy.CLASS)
    public @interface ClassAnnotation{
        String author();
    }

(3)作用于Generation3List

    @ClassAnnotation(author = "zphuan")
    public class Generation3List {
        
        @MethodAnnotation(author = "zphuan", date = "3/26/2017")
        public void test(){
        }
    }

(4)编译时解析:

import java.util.HashMap;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.zphuan.MyAnnotation.ClassAnnotation;

@SupportedAnnotationTypes({ "com.zphuan.MyAnnotation.ClassAnnotation" })
public class ClassProcessor extends AbstractProcessor {

    private static HashMap map;

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        map = new HashMap();
        for (TypeElement te : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
                ClassAnnotation classAnnotation = element.getAnnotation(ClassAnnotation.class);
                map.put(element.getEnclosingElement().toString(), classAnnotation.author());
            }
        }
        return false;
    }
}

这个类上可以添加注解:
@SupportedAnnotationTypes的值为当前类支持的注解的完整类路径,支持通配符。表示这个 Processor 要处理的 Annotation 名字.
@SupportedSourceVersion 标识该处理器支持的源码版本

process 函数中参数 annotations 表示待处理的 Annotations,参数 env 表示当前或是之前的运行环境

process 函数返回值表示这组 annotations 是否被这个 Processor 接受,如果接受后续子的 rocessor 不会再对这个 Annotations 进行处理

编译时运行的原理主要应用于比如android中的ButterKnife,运行时的解析主要体现于比如Android下的Retrofit,大家在学习这两个主流框架的源码的时候,可以观察下他们的注解解析的不同之处。

9.总结

对于注解的总体介绍就到这儿了,相信通过该篇文章的一个系统性学习能让你对Annotation注解有一个更深入的认识和使用。不过如果有童鞋想实现一套自己的依赖注入框架,一定要注意性能上的优化,注解在运行时期的解析是通过反射的方式去获取,所以只要用到反射那么必定存在性能上的消耗,所以建议参考一些优秀开源框架来学习如何在编译时期通过注解生成代码,提高效率!

你可能感兴趣的:(Java中的Annotation详解)