Java注解使用与自定义注解


一. 注解基本介绍

1.1 什么是注解?

什么是注解?严谨的来说,注解提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。

Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。注解包含在 java.lang.annotation 包中。
具体定义如下:

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

1.2 注解的作用

能够读懂别人写的代码(尤其是框架相关的代码);

实现替代配置文件的功能。比如可能原本需要很多配置文件以及很多逻辑才能实现的内容,如果使用合理的注解,就可以使用一个或多个注解来实现相同的功能。这样就使得代码更加清晰和整洁;

编译时进行格式检查

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

装逼。

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

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

二. 元注解

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

基本的元标签有 @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 注解,那么对于该超类,它的子类如果没有被任何注解应用的话,那么这个子类就继承了超类的注解。

@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 的时候指注解的值可以同时取多个

@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 的泛型类型),每调用一次该方法,都会产生警告信息。如果希望禁止这个警告信息,可以使用 @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:

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FruitName {

    String value() default "";

}

水果颜色注解 FruitColor.java:

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FruitColor {
    /**
     * 颜色枚举
     */
    public enum Color{ BLUE,RED,GREEN};

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

}

水果供应者注解 FruitProvider.java:

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

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

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

注解处理器 FruitInfoUtil.java:

import java.lang.reflect.Field;

public class FruitInfoUtil {


    public static void getFruitInfo(Class<?> clazz)
    {
        StringBuilder strFruitName=new StringBuilder(" 水果名称:");
        StringBuilder strFruitColor=new StringBuilder(" 水果颜色:");
        StringBuilder strFruitProvicer=new StringBuilder("供应商信息:");

        Field[] declaredFields = clazz.getDeclaredFields();

        for(Field field: declaredFields)
        {
            if(field.isAnnotationPresent(FruitName.class))
            {
                FruitName fruitName = field.getAnnotation(FruitName.class);
                strFruitName.append(fruitName.value());
                System.out.println(strFruitName);
            }
            else if(field.isAnnotationPresent(FruitColor.class))
            {
                FruitColor fruitColor= field.getAnnotation(FruitColor.class);
                strFruitColor.append(fruitColor.fruitColor());
                System.out.println(strFruitColor);

            }

            else if(field.isAnnotationPresent(FruitProvider.class))
            {
                FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class);
                strFruitProvicer.append(" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address());
                System.out.println(strFruitProvicer);

            }
        }

    }



}

苹果 Apple.java:






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 String getAppleName() {
        return appleName;
    }

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

    public String getAppleColor() {
        return appleColor;
    }

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

    public String getAppleProvider() {
        return appleProvider;
    }

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

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

}

测试输出水果信息 Main:

public class Main {

    public static void main(String[] args) {

        FruitInfoUtil.getFruitInfo(Apple.class);
    }
}

运行后的测试结果为:

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


文章作者: jackey
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 jackey !
评论
  目录