今でもあなたは私の光丶

Java 注解 —— 注解的理解、注解的使用与自定义注解

参考网址:

《秒懂,Java 注解 (Annotation)你可以这样学》
《Java注解基本原理》
《注解Annotation实现原理与自定义注解例子》
《框架开发之Java注解的妙用》


一. 注解基本介绍

1.1 什么是注解?

什么是注解?严谨的来说,注解提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。注解包含在 java.lang.annotation 包中。
具体定义如下:

注解 (Annotation),也叫元数据。一种代码级别的说明。它是 JDK1.5 及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
——摘自百度百科

上面的说明虽然严谨,但比较难懂。笔者认为《秒懂,Java 注解 (Annotation)你可以这样学》一文中,作者 frank909 大佬的解释十分亲民:可以完全将注解当做生活中我们对人对物贴的标签

拿笔者最喜欢的一部动画电影来打个比方吧:《Zootopia》。《Zootopia》整个电影将动物们拟人化,性格各异。不管是兔子,狐狸,羚羊,豹子等等,每个动物都有一张固有标签:兔子乖巧,狐狸狡黠,羚羊温顺,豹子凶猛
但它们又有着自己真实的性格:想当警察的兔子,狡黠却不失善良的狐狸,披着狼皮的腹黑羚羊,吃着甜甜圈有少女心的豹子

Java 注解 —— 注解的理解、注解的使用与自定义注解
Zootopia

《Zootopia》这个电影的内核是在讲,我们要试图冲破外界对自己所贴的标签的限制。但在这里笔者要稍微的当一下杠精,吹一下标签的作用:贴标签是较为精准的了解一个事物的最高效率方法。疯狂动物城中的动物们,外界对他们的第一印象,往往都是直接引用了该物种性格的固有标签。同样的在 Java 中,注解的作用就是告诉开发人员,被注解的内容是用来做什么的,换句话说,注解就是 Java 代码的标签。
在 Java 中,给代码贴合适的标签是很重要的,它很大程度的提高了效率。虽然写代码的时候开发人员也可以致敬《Zootopia》主旨,尝试突破标签的限制(比如给实现了 @Controller 功能的代码加了 @Service 注解),但笔者不保证写下这样代码开发人员的后续人身安全,太睿智的人肯定是要被针对的……

1.2 注解的作用

  1. 能够读懂别人写的代码(尤其是框架相关的代码);
  2. 实现替代配置文件的功能。比如可能原本需要很多配置文件以及很多逻辑才能实现的内容,如果使用合理的注解,就可以使用一个或多个注解来实现相同的功能。这样就使得代码更加清晰和整洁;
  3. 编译时进行格式检查

    • 如 @Override 注解放在方法前,如果该方法不是覆盖了某个超类方法,编译的时候编译器就能检查出来。
  4. 装逼。

    • 做技术的怎么可以没有一点用技术吹牛逼的心理呢?如果会在合适的地方恰好的使用注解或者自定义注解的话,老板肯定会双手送你 666 的。当然笔者现在只是初学而已,距离用技术吹牛逼的道路还远。

1.3 注解的原理

注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 $Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。
——摘自《注解Annotation实现原理与自定义注解例子》

这里涉及的内容比较深入,笔者目前不能理解。先贴上来,以后慢慢来吧。

二. 元注解

元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。或者可以理解为:元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的

基本的元标签有 @Retention, @Documented, @Target, @Inherited 四种(后来到了 Java 8 又加入了 @Repeatable)。

2.1 @Retention

@Retention 定义了该注解的生命周期。当 @Retention 应用到一个注解上的时候,作用就是说明这个注解的存活时间。

  • RetentionPolicy.SOURCE: 注解只在源码阶段保留,在编译器完整编译之后,它将被丢弃忽视;

    • 例:@Override, @SuppressWarnings
  • RetentionPolicy.CLASS: 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中;
  • RetentionPolicy.RUNTIME: 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们;笔者接触到大部分的注解都是 RUNTIME 的生命周期。

以 SpringMVC 中的 @Service 的源码为例:

package org.springframework.stereotype;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    String value() default "";
}

这里 @Service 拥有 @Retention(RetentionPolicy.RUNTIME) 注解,所以在程序运行时可以捕获到它们。

2.2 @Target

@Target 表示该注解用于什么地方,可以理解为:当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。可以使用的 ElementType 参数:

  • ElementType.CONSTRUCTOR: 对构造方法进行注解;
  • ElementType.ANNOTATION_TYPE: 对注解进行注解;
  • ElementType.FIELD: 对属性、成员变量、成员对象(包括 enum 实例)进行注解;
  • ElementType.LOCAL_VARIABLE: 对局部变量进行注解;
  • ElementType.METHOD: 对方法进行注解;
  • ElementType.PACKAGE: 对包进行注解;
  • ElementType.PARAMETER: 对描述参数进行注解;
  • ElementType.TYPE: 对类、接口、枚举进行注解;

如上面的 @Service 所示,@Service 的 @Target 注解值为 ElementType.TYPE,即 @Service 只能用于修饰类。

2.3 @Documented

@Documented 是一个简单的标记注解,表示是否将注解信息添加在 Java 文档,即 Javadoc 中。

2.4 @Inherited

Inherited 是指继承,@Inherited 定义了一个注释与子类的关系。如果一个超类带有 @Inherited 注解,那么对于该超类,它的子类如果没有被任何注解应用的话,那么这个子类就继承了超类的注解。

用《秒懂,Java 注解 (Annotation)你可以这样学》一文中的例程与解释来说明:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}


@Test
public class A {}


public class B extends A {}

注解 Test 被 @Inherited 修饰,之后类 A 被 Test 注解,类 B 继承 A,类 B 也拥有 Test 这个注解。可以这样理解:
老子非常有钱,所以人们给他贴了一张标签叫做富豪。
老子的儿子长大后,只要没有和老子断绝父子关系,虽然别人没有给他贴标签,但是他自然也是富豪。
老子的孙子长大了,自然也是富豪。
这就是人们口中戏称的富一代,富二代,富三代。虽然叫法不同,好像好多个标签,但其实事情的本质也就是他们有一张共同的标签,也就是老子身上的那张富豪的标签。

2.5 @Repeatable

@Repeatable 是 Java 8 中加入的,是指可重复的意思。通常使用 @Repeatable 的时候指注解的值可以同时取多个。依旧用《秒懂,Java 注解 (Annotation)你可以这样学》一文中的例程与解释来说明:一个人既是程序员,又是产品经理,同时也是画家。

@interface Persons {
    Person[] value();
}

@Repeatable(Persons.class)
@interface Person {
    String role default "";
}

@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan {
    ...
}

上面的代码通过 @Repeatable 定义了 Person,而 @Repeatable 后面括号的类相当于一个容器注解。容器注解就是用来存放其它注解的地方,它本身也是一个注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    Class<? extends Annotation> value();
}

上面是 @Repeatable 的源码。按照规定,如果使前面的 Persons 里面可以重复调用某个注解,则 Persons 必须有一个 value 的属性,且属性类型必须为被 @Repeatable 注解的 Person

三. 注解的属性

注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以无形参的方法形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。以下面的例程为例:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Coder {
    int id();
    String name();
    String language();
    String company();
}

上面假设定义了一个名为 @Coder 的注解,该注解有 id, name, language, company 三个属性。使用的时候,我们应该对其赋值。赋值的方式类似于 key="value" 的方式进行,属性之间用 "," 隔开:

@Coder(id = 10086, name = "GRQ", language = "JAVA", company = "cetc")
public class coderGRQ() {

}

此外,注解可以有默认值,需要用 default 关键字指定。例如上例:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Coder {
    public int id() default -1;
    public String name() default "GRQ";
    public String language() default "C++";
    public String company() default "China_Company";
}

如果:

@Coder
public class coderXX {}

由于在 @Coder 注解中设置了默认值,所以就不需要再 @Coder 后面的括号里进行赋值了。

此外,如果注解内只有一个名为 value 的属性时,应用该属性时可以将值直接写到括号内,不用写 value = "..."。例如:

public @interface language {
    String value();
}

那么下面两种声明是相同的:

// 第一种声明
@language("JAVA")
int coderA;
// 第二种声明
@language(value = "JAVA")
int coderA;

四. 常用注解

Java 中自带且常用的几种注解有 @Override, @Deprecated, @SuppresWarninngs, @SafeVarargs。

@Override 是一个标记类型注解,用于提示子类要复写父类中被 @Override 修饰的方法,它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。

@Deprecated 也是一个标记类型注解,用于标记过时的元素。比如如果开发人员正在调用一个过时的方法、类或成员变量时,可以用该注解进行标注。

@SuppressWarnings 并不是一个标记类型注解,它可以阻止警告的提示。它有一个类型为 String[] 的成员,其值为被禁止的警告名。

@SafeVarargs 是一个参数安全类型注解。它的目的是提醒开发人员,不要用参数做一些不安全的操作。它的存在会阻止编译器产生 unchecked 的警告。例如对于可变长度参数,如果和泛型一起使用,会产生比较多的编译器警告。如下面的方法:

public static <T> T useVarargs(T... args) {  
    return args.length > 0 ? args[0] : null;  
} 

如果参数传递的是不可具体化的类型(类似于 List<String> 的泛型类型),每调用一次该方法,都会产生警告信息。如果希望禁止这个警告信息,可以使用 @SuppressWarnings("unchecked") 注解进行声明。同时在 Java 7 版本之后的 @SafeVarargs 注解针对 "unchecked" 警告进行了屏蔽,我们也可以用 @SafeVarargs 获得 @SuppressWarnings("unchecked") 同样的效果。

五. 自定义注解

此处参考《注解Annotation实现原理与自定义注解例子》的原理介绍和水果例程。

自定义注解类编写的规则:

  1. 注解类型定义为 @interface,所有的注解会自动继承 java.lang.Annotation 这一接口,而且不能再去继承其他的类或接口;
  2. 参数成员只能用 public 或 default 两个关键字修饰;
  3. 参数成员只能用基本类型:byte, short, char, int, long, float, double, boolean,以及 String, Enum, Class, Annotations 等数据类型,以及这些类型的数组;
  4. 要获取类方法和字段的注解信息,必须通过 Java 的反射技术
  5. 注解也可以不定义成员变量,但这样的注解没有什么卵用;
  6. 自定义注解需要使用元注解进行编写;

以水果与水果供应商为例:

水果名称注解 FruitName.java:

package com.grq.FruitAnnotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果名称注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

水果颜色注解 FruitColor.java:

package com.grq.FruitAnnotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果颜色注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 颜色枚举
     */
    public enum Color{ BLUE,RED,GREEN};

    /**
     * 颜色属性
     */
    Color fruitColor() default Color.GREEN;

}

水果供应者注解 FruitProvider.java:

package com.grq.FruitAnnotation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果供应者注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitProvider {
    /**
     * 供应商编号
     */
    public int id() default -1;

    /**
     * 供应商名称
     */
    public String name() default "";

    /**
     * 供应商地址
     */
    public String address() default "";
}

注解处理器 FruitInfoUtil.java:

package com.grq.FruitAnnotation;

import java.lang.reflect.Field;

/**
 * 注解处理器
 */
public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz){

        String strFruitName=" 水果名称:";
        String strFruitColor=" 水果颜色:";
        String strFruitProvicer="供应商信息:";

        Field[] fields = clazz.getDeclaredFields();

        for(Field field :fields){
            if(field.isAnnotationPresent(FruitName.class)){
                FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
                strFruitName=strFruitName+fruitName.value();
                System.out.println(strFruitName);
            }
            else if(field.isAnnotationPresent(FruitColor.class)){
                FruitColor fruitColor= (FruitColor) field.getAnnotation(FruitColor.class);
                strFruitColor=strFruitColor+fruitColor.fruitColor().toString();
                System.out.println(strFruitColor);
            }
            else if(field.isAnnotationPresent(FruitProvider.class)){
                FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class);
                strFruitProvicer=" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }
    }
}

苹果 Apple.java:

package com.grq.FruitAnnotation;

/**
 * 注解使用
 */
public class Apple {

    @FruitName("Apple")
    private String appleName;

    @FruitColor(fruitColor = FruitColor.Color.RED)
    private String appleColor;

    @FruitProvider(id=1,name="陕西红富士集团",address="陕西省西安市延安路89号红富士大厦")
    private String appleProvider;

    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }
    public String getAppleColor() {
        return appleColor;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }
    public String getAppleName() {
        return appleName;
    }

    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }
    public String getAppleProvider() {
        return appleProvider;
    }

    public void displayName(){
        System.out.println("水果的名字是:苹果");
    }
}

测试输出水果信息 FruitTestAnnotation:

package com.grq.FruitAnnotation;

public class TestFruitAnnotation {
    public static void main(String[] args) {
        FruitInfoUtil.getFruitInfo(Apple.class);
    }
}

运行后的测试结果为:

水果名称:Apple
水果颜色:RED
供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦

后记

这段时间虽然在 SpringMVC 中用注解用的飞起,各种 @RequestMapping, @Service, @Controller 等注解信手拈来,但还是不了解它的运作原理到底是什么样的。尤其是在框架中,大量运用到了注解与反射操作,所以以后也会认真了解一下如 Spring 框架中注解的运行原理,想必这无论是对理解框架,还是对理解注解本身,都会有很大的帮助。

发表评论