Java 注解知识总结

引言

本博客总结自《Java 编程思想》第 20 章。

一、什么是注解

注解是 Java 5 引入的一种通过反射机制实现的语法特性,开发者可以通过在类、域、方法等元素前面标记一个“标签”达到对程序的源码类信息运行时进行某种说明或处理的效果,尽可能地简化代码,从而使程序开发更高效。但需要注意的是,编译器要确保在其构造路径上,必须有对应注解的定义。

Java 中在 1.5 之初内置了三个标准注解,@Deprecated、@Override 、@SupressWarning 。我们经常会在程序的各个角落看到它们。

以@SupressWarning为例,

package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

它一般用于去除不恰当的编译警告,俗称“报黄”。随着Java 慢慢的发展,也逐渐引入了更多的注解,比如在 Java 8 伴随着 Lambda表达式的加入,而一同入住 Java 大家庭的 @FunctionalInterface 注解:

package java.lang;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

 二、如何声明注解

注解的定义非常类似接口,不同的是,像上面的例子中,我们要在 interface 关键字前面加 “@”,以此来声明这是一个注解。

除此之外,Java 提供了四个元注解

@Target
@Retention
@Documented
@Inherited

其中,@Target 、@Retention 在定义注解时,一般情况下都是必选项。

元注解专职负责注解其他注解。

@Target :表示该注解可以用于什么地方。需要给它传入一个ElementType 枚举对象,常用选项有:

    CONSTRUCTOR : 构造器声明
    FIELD : 域声明(包括enum实例)
    LOCAL_VARIABLE : 局部变量声明
    METHOD : 方法声明
    PACKAGE : 包声明
    PARAMETER : 参数声明
    TYPE : 类、接口(包括注解声明)、enum 声明

@Retention :表示需要在什么级别保存该注解。需要传入一个 RetentionPolicy ,可选值:

    SOURCE : 注解将被编译器丢弃。
    CLASS : 注解在class文件中使用,但会被 JVM 丢弃。
    RUNTIME : vm将会在运行期间也保留该注解,因此可以通过反射机制读取注解的信息。

@Documented : 将此注解包含在javadoc 中。

@Inherited : 允许子类继承父类的注解。 

注解定义示例:

package com.mht.demo.注解;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

	public int id();

	public String description() default "no description";
}

可以看到,注解的确非常像接口,同时,和其他任何 Java 接口一样,注解也将会被编译成为一个 class 文件

@MyTest 用到了两个元注解:@Target 、@Retention ,如上所示,@MyTest 只能用于方法上,如果希望自己的注解既可以用于方法上,也可以用于类上,那么可以这样写:

@Target({ ElementType.METHOD, ElementType.TYPE })

@Retention用于定义你的注解应用在什么级别,源代码(SOURCE)、类文件(CLASS)、运行时(RUNTIME)

注解中往往包含一些元素,比如上例中的 id 、description 。

在分析和处理注解时,程序或工具可以利用这些值,它们看起来像接口中的抽象方法,唯一不同的是我们可以为这些元素指定默认值。对于没有任何元素的注解如 @FunctionalInterface 被称为标记注解。

编译器会对注解中的元素进行类型检查,因此,将这些元素与数据库相关联是安全的。注解元素可用的类型有一定的限制,它不允许任何包装类型。被允许的注解元素类型有:

1、8大基本类型

2、String

3、Class

4、enum

5、Annotion : 嵌套注解,非常有用的技巧

6、以上类型的数组

如果使用了除上述几种以外的其他类型,那么编译器就会报错。 注意,注解的元素值永远不能为null要么在声明元素之初设置默认值,要么就在使用注解时添加该元素值,而且必须是不为 null 的值(如果希望注解中的某个元素是必填项,那么在声明时就可以不为其指定默认值)。因此,注解处理器无法通过null 来判断元素是否缺失。为了绕开这个限制,一般会通过 自己定义特殊值来判断元素是否存在,比如 -1 或 ""。

default 关键字来定义元素的默认值,在使用该注解时,如果没有给出元素的值 那么注解处理器就会使用此元素的默认值。

三、注解的使用与自定义注解处理器

以 @MyTest 为例,我们来看看注解如何使用,以及如何处理这个注解。

public class SomeService {

	@MyTest(id = 1, description = "Hello Annotation! Hello 2020 !")
	public void testMyTestFeature() {
		System.out.println("这是testMyTestFeature()方法!");
	}
}

我们定义了一个类,声明了一个方法,并为其标记我们的 @MyTest 注解。

接下来我们来实现一个注解处理器 MyTestProcessor :

/**
 * '@MyTest'注解处理器
 * @author mht
 *
 */
public class MyTestProcessor {
	
	public static void processMyTest(Class<?> clz) {
		Method[] declaredMethods = clz.getDeclaredMethods();
		// getAnnotation() 方法会返回指定类型的注解,如果没有,则返回null
		MyTest myTest = declaredMethods[0].getAnnotation(MyTest.class);
		// 这里一般都会判断获取到的注解是否为空
		if (myTest != null) {
			System.out.println("找到标记注解:id:" + myTest.id() + ", 描述:" + myTest.description());
		}
		
	}
	
	public static void main(String[] args) {
		processMyTest(SomeService.class);
	}
}

执行 main 方法,测试输出结果:

找到标记注解:id:1, 描述:Hello Annotation! Hello 2020 !

注解处理器虽然名字听起来很专业,但实际上,我们并不需要为我们处理注解的类或方法继承或实现什么。正如第一节开始所说的,注解是一种通过反射机制来实现的特性,我们可以通过 Class 对象来获取我们想要的注解。

像上面的代码有点过于简单了,一般情况下,我们可能会为一个目标添加多个注解,因此一般的处理注解的思路就是:

1、获取类信息(Class 对象可以直接获取类上的注解对象或注解对象数组)

2、通过 getDeclaredMethods() 等方法,获取目标信息(有时也有可能是 类或域)

3、通过 getAnnotation(Class<T> annotationClass)、getAnnotations() 等方法,获取一个或多个待处理的注解对象。

4、通过注解对象,获取内部元素,根据其值进行逻辑处理。

这是一个一般的注解处理思路,许多框架中的注解处理往往比较复杂,且经常需要配合遍历来处理多个类信息,多个注解的情况。

值得注意的是,注解往往都是被动的处理,它不能主动发出某种信号传递给注解处理器,也就是说,我们必须主动找到这些注解或将携带他们的类传入注解处理器。

某些框架在批量处理注解的时候,就必须为注解处理器指定一个尽可能小的路径范围,以此来扫描该路径下的类信息。

Mybatis 中的 @MapperScan 就是一个很好的例证,如果不为其指定一个扫描路径,Mybatis 框架就可能必须从 classpath 的根路径找起,这会非常影响框架处理效率。

综上就是关于 注解的总结和思考,欢迎文末留言。

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页