Java项目的程序里为什么老用注解?注解有哪些作用

4,449 阅读11分钟

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

注解的英文名叫“Annotation”,是 Java 中给类、方法以及成员变量等元素增加元数据的方式。换言之注解就是用于描述这些元素的。

元数据一词从单词 metadata 译来,意为“描述数据的数据”。

注解和注释不同的是,注解会被 Java 编译器处理而非跳过。

注解是在JDK5.0版本开始引入的,它可以在编译期使用预编译工具进行处理, 也可以在运行期使用 Java 反射机制进行处理。注释可以用于创建 Javadoc,跟踪代码中的依赖性,甚至执行基本编译时检查。本质上,Annotion是一种特殊的接口,程序可以通过反射来获取指定程序元素的 Annotion 对象,通过 Annotion 对象来获取注解里面的元数据。

相比上来就给大家介绍注解的各种概念,它都有哪些作用外,我更希望把自己带入到一个 Java 小白的身份从零开始了解注解。我们通过“认识注解、使用注解”开始了解注解,随后带出 Java 原生自带的注解以及如何自定义注解,在编写自定义注解的时候顺便通过使用元注解把元注解的概念介绍一下,最后再来说注解都有哪些作用,以及使用注解带来的收益时需要付出什么代价。

本文内容大纲如下:

初识注解

Java 注解的简写形式如下:

@Entity

@ 符号告诉编译器这是一个注解, @ 字符后面的名称是注解的名称。在上面的示例中,注解名称是 Entity。

注解还可以包含为其设置值的元素,类似注解的一个属性。下面是一个带元素的 PostMapping 注解示例。

@PostMapping(path = "/")

上面的 PostMapping 注解包含一个名为 path 的元素,我们为其赋值为 "/"。设置元素值的语法是在注解名称后的括号内进行设置。

一个注解可以包含多个元素,比如 PostMapping 注解就可以通过consumes元素,指定接收的请求体的格式为application/json

@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE)

当然,PostMapping 注解能设置哪些元素的值,也是根据 PostMapping 注解的定义来的。

public @interface PostMapping {
    
    // 省略其他元素的定义......
    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] path() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] consumes() default {};
    
    // ......

}

如果注解只包含一个元素,那么按照约定惯例,会把该元素命名成“value”:

@InsertNew(value = "yes")

当注解只包含一个名为 value 的元素时,使用注解时我们可以省略元素名称,只提供元素值。

@InsertNew("yes")

使用注解

Java 的注解可以应用在类、接口、方法、方法的参数、成员变量和方法内的局部变量之上,比如在类上应用注解,就是把注解放在类声明之上。

@Entity
@Table(name = "coffees")
public class Coffee implements Serializable {
	// ......
}

下面这个例子在类、成员变量、成员方法、方法参数上都应用了注解。

@RestController
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
    @Autowired
    private CoffeeOrderService orderService;
    @Autowired
    private CoffeeService coffeeService;


    /**
     * 创建Coffee订单
     *
     * @param newOrder
     * @return CoffeeOrder
     */
    @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
    public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
		// ...
    }
}

上面是一个典型的 Spring MVC 的 Controller API 处理方法,如果你对 Spring 框架还不太熟悉,可以先不管这些注解什么意思,这里主要是演示一下注解可以用在哪里。在Java程序的这些成员上都可以加上注解,为成员添加元数据。

Java 内置的注解

Java 带有三个内置注解,可以直接被编译器处理,用于为 Java 编译器提供指令。这些注解是:

  • @Override
  • @Deprecated
  • @SupressWarning

@Override

@Override 注解用于标注方法,它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们在一个没有覆盖父类方法的方法上应用 @Override 注解时,Java编译器会告警。

public class MySuperClass {

    public void doTheThing() {
        System.out.println("Do the thing");
    }
}


public class MySubClass extends MySuperClass{

    @Override
    public void doTheThing() {
        System.out.println("Do it differently");
    }
}

在覆盖了父类方法的子类方法上使用 @Override 注解并不是必需的。不过,使用了@Override 注解的方法,编译器在编译时会去父类找相同的方法签名,验证方法覆盖是否真实存在。

使用@Override 注解的一个好处是--比如说有人在项目里修改了父类的被子类覆盖的方法,子类覆盖方法上不使用 @Override 注解的话编译器是不会提示出子类方法未覆盖父类方法的,可能会导致调用方无法正确调用到子类方法的问题,这时就显出 @Override 的重要性了。

@Deprecated

@Deprecated 注解用于将类、方法或字段标记为已弃用,这意味着不推荐再使用它。如果你的代码使用了不推荐使用的类、方法或字段,编译器在编译时会产生一条 Warning 级别的告警。

@Deprecated
public class MyComponent {

}

在类声明上方使用 @Deprecated 注解将该类标记为已弃用。 还可以在方法和字段声明上方使用@Deprecated 注解,将方法或字段标记为已弃用。 当使用 @Deprecated 注解时,最好也使用相应的 @deprecated 注解,该注解用于JavaDoc ,一般使用它来解释为什么不推荐使用以及应该改用什么。

@Deprecated
/**
 * @deprecated Use MyNewComponent instead.
 */
public class MyComponent {
    
    @Deperecated
    // @deprecated Use MyNewComponent's printComponentName instead.
	public void printComponentName() {
        System.out.println("MyComponent")
    }
}

@SupressWarning

@SuppressWarnings 用于关闭对类、方法、成员编译时产生的特定警告。 @SuppressWarnings 不是一个标记注解。它有一个类型为 String[] 的数组成员,这个数组中存储的是要关闭的告警类型。对于 javac 编译器来讲,对 -Xlint 选项有效的警告名也同样对 @SuppressWarings 有效,同时编译器会忽略掉无法识别的警告名。

@SuppressWarnings({"rawtypes", "unchecked"})
public class SuppressWarningsAnnotationDemo {
    static class SuppressDemo<T> {
        private T value;

        public T getValue() {
            return this.value;
        }

        public void setValue(T var) {
            this.value = var;
        }
    }

    @SuppressWarnings({"deprecation"})
    public static void main(String[] args) {
        SuppressDemo d = new SuppressDemo();
        d.setValue("London");
        System.out.println("Place:" + d.getValue());
    }
}

自定义注解

下面让我们自己定义一个注解。

public @interface MyAnnotation {
    String   value();
    String   name();
    int      age();
    String[] newNames();
}

使用 @interface 关键字来声明一个注解,注解的声明有点类似于接口声明,其中的每一个方法实际上是声明了一个注解的元素。方法的名称就是元素的名称,返回值类型就是元素的值类型(返回值类型只能是基本类型、Class、String、enum)。这个例子定义了一个名为 MyAnnotation 的注解,它有四个元素。

@MyAnnotation(
    value="123",
    name="Jacob",
    age=37,
    newNames={"Jenkov", "Peterson"}
)
public class MyClass {

	// ...
}

现在使用 @MyAnnotation 必须像上面这个例程中的这样,为其的所有元素指定值。但其实是可以在声明注解时给元素设置默认值的。

注解元素的默认值

在定义注解的时候可以为元素指定默认值。这样元素就变成了可选的,在使用的时候被省略则直接使用其默认值。下面是在注解定义里如何给元素指定默认值的例子:

@interface MyAnnotation {
    String   value() default "";
    String   name();
    int      age();
    String[] newNames();
}

现在我们可以在使用 MyAnnotation 注解时选择省略 value 元素,这样注解会默认使用 value 元素的默认值。如下所示:

@MyAnnotation(
    name="Jakob",
    age=37,
    newNames={"Jenkov", "Peterson"}
)
public class MyClass {


}

像上面这个例子,在注解的使用中我们并没有指定其 value 元素的值。

元注解

元注解是用于修饰注解的注解,在注解的定义中使用,例如:

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

}

这是 @Override 注解的定义,可以看到其中的 @Target,@Retention 两个注解就是『元注解』,元注解一般用于指定注解的生命周期以及作用目标等信息。 Java 中有以下几个元注解:

  • @Retention:注解的生命周期或者叫保留策略
  • @Target:注解的作用目标
  • @Inherited:是否允许子类继承该注解
  • @Documented:注解是否应当被包含在 JavaDoc 文档中

@Retention

我们可以为上面自定义的注解 MyAnnotation 指定它是否在运行时可用,以便能通过反射进行检查。通过在注解 MyAnnotation 的定义中使用 @Retention 元注解来做到这一点。

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)

@interface MyAnnotation {

    String   value() default "";

}

添加到 MyAnnotation 定义上的元注解。

@Retention(RetentionPolicy.RUNTIME)

会指示 Java 编译器和 JVM 当前注解在运行时可以通过反射获取到。RetentionPolicy 类表示注解的保留策略,包含三个两个可以使用的值:

  • RetentionPolicy.RUNTIME 表示注解永久保存,在运行时可以通过反射获取。
  • RetentionPolicy.CLASS 表示注解存储在 .class 文件中,在类加载阶段被丢弃,运行时不可用。
  • RetentionPolicy.SOURCE 表示注解仅在源代码中可用,在 .class 文件和运行时中不可用。

在定义注解时如果不指定注解的任何保留策略,RetentionPolicy.CLASS 就是默认的保留策略。如果创建的注解是与扫描代码的构建工具一起使用,则可以使用保留策略 RetentionPolicy.SOURCE,这样就会避免 .class 文件受到不必要的污染。

@Target

上面例子第一的 MyAnnotation 注解并没有标明该注解的作用目标,是能作用在类上,方法上,还是字段上。如果要限制注解的可作用目标的话,就需要在注解定义中使用 @Target 元注解来进行限制。

下面我们给自己定义的 MyAnnotation 加上 @Target ,限制它只能注解方法上。

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
public @interface MyAnnotation {

    String   value();
}

ElementType 包含以下可用的枚举值:

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

大部分枚举值的名称都是自解释的,可以通过字面单词意思看出来。有两个需要额外说明下,ElementType.ANNOTATION_TYPE 表示注解只能用于注解其他注解。在 @Target 和@Retention 这些元注解的定义里我们能看到使用的正式这个枚举值。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

ElementType.TYPE 表示注解能用于任何类型,可以是类、接口、枚举,包括注解。

@Inherited

@Inherited 表示自动继承注解类型。 如果注解声明中存在 @Inherited 元注解,则注解所修饰类的所有子类都将会继承此注解。

java.lang.annotation.Inherited

@Inherited
public @interface MyAnnotation {

}

---

@MyAnnotation
public class MySuperClass { ... }

---
    
public class MySubClass extends MySuperClass { ... }

在这个例子中,MySubClass 会从父类 MySuperClass 自动继承 MyAnnotation 注解。

@Documented

@Documented 元注解被用于告知 JavaDoc 生成工具,当前注解需要在使用它的类的文档中显示。

import java.lang.annotation.Documented;

@Documented
public @interface MyAnnotation {

}
@MyAnnotation
public class MySuperClass { ... }

当为 MySuperClass 生成 JavaDoc 的时候, @MyAnnotation 注解会被包含其中。 @Documented 这个元注解并不会经常用到,当我们看源码的时候遇到它后,能知道它是干什么用的就行了。

Java 里注解的用途

在 Java 里注解有许多用途,可以归纳为三类:

  • 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查,编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时的处理:程序可以处理注解信息以生成代码,XML 文件等。
  • 运行时处理:可以在运行时检查某些注解并处理。

作为 Java 程序员,尤其是编程十几年年的老手,多多少少都曾经历过被各种配置文件(xml、properties)支配的恐惧。过多的配置文件会使得项目难以维护。使用注解可以减少配置文件或代码,是注解最大的用处,现在 Spring 家族的 SpringBoot 就是靠注解维护各种 Bean 组件的,让开发中者不再用XML指定各种Java Bean 的路径、名称等属性,减少了不少项目配置的步骤,从而让Java项目的开发提速了不少。

相关阅读