引言
本博客总结自《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() 方法会返回指定类型的注解,如果没有,则返回nullMyTest 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 的根路径找起,这会非常影响框架处理效率。
综上就是关于 注解的总结和思考,欢迎文末留言。