Java 注解 (Annotations)
1. What
1.1 定义
注解, 又被称为元数据, 提供了一个把信息和代码放在一起的正式的方法。 这些信息可以很容易的在后面的阶段被提取和使用。
1.2 分类
根据用途,注解可以被分类为元注解,标记注解和其他的注解。
元注解被称为注解的注解。在JDK8中, 一共有5个元注解。它们分别为:
元注解 | 用法 |
---|---|
@Target | 通过ElementType用于表明该注解可以被放置在代码中的哪些地方。 更多细节请参考:ElementType . |
@Retention | 用于表明该注解能够出现在代码生命周期的哪些阶段。 当前可用的范围为:SOURCE: 只存在于源文件当中,不会存在于字节码中. CLASS: 存在于字节码当中,但是可能不会存在于虚拟机当中. RUNTIME: 注解存在于运行时即虚拟机中,可以被反射机制使用。 |
@Documented | 用于表明该注解会出现在JavaDocs当中 |
@Inherited | 用于表明该注解会被子类继承 |
@Repeatable | 用于表明该注解可以重复出现在代码当中的同一个位置 |
标记注解不包含任何的实例变量,但是标记注解自身就可以完成特定的目的。例如@Override。
1.3 标记注解和标记接口的对比
标记注解 | 标记接口 |
---|---|
不提供编译器类型检查 | 提供编译器类型检查 |
可被自由使用,如果该注解不包含@Inherited | 标记接口可能会给开发带来非预期内的负担, e.g. Cloneable |
可被用于代码中的各种位置 | 只可以被用于类和接口级别 |
2. Why
将元数据和源代码文件组合在一起在编程里是一个趋势。在注解在Java中出现之前,元数据通常会被存放在xml或者以代码命名格式的形式存在。将元数据存放在配置文件当中不简洁也不直观。用代码命名格式的方式存放注解很容易被破坏,并且当元数据被破坏时没有任何的警告。
3. How
3.1 定义一个注解
可以用下面的方式定义一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
int id();
String description() default "no description";
}
在上述实例当中,@UseCase注解包含两个变量,分别为id和description。注解中的变量可以有默认值,例如description。如果变量不存在默认值,那么在使用该注解时,编译器强制开发者对这些变量进行赋值,否则会有编译错误。
注解的变量可以有如下的类型:所以的基本类型(例如,int,float,等等),字符串,Class类型,枚举类型,注解类型以及这些类型的数组。
3.2 用法
在Spring框架中的开发中,@Controller是一个常见的注解。
注解@Controller会被一直保持到运行时,并且只能出现在类型上。Spring框架会把对应的类进行加载、解析,最终生成一个bean。仅出现在源文件中的注解也可起到作用。例如@Override, 编译器会根据该注解来确认被注解的方法是否正确的重写了一个子类可见的父类的或者实现接口中的方法,如果不是,那么会有编译错误。另一个例子为Lombok框架。Lombok框架通过对特定注解进行解析并且通过注解处理器的方式改变生成的字节码。
3.3 反射处理注解
系统可以在运行时通过反射对相关注解进行侦测和处理。举例如下。
在上述举例中,AnnotationTypeFilter#hasAnnotation会首先去除jdk中的类名并对类进行加载。在129行,根据该注解解析的要求,通过反射的方式判断该类是否包含相应的注解。
3.4 javac处理注解
通过javac和注解处理器,我们可以通过源代码中的注解来完成一些工作而不需要已经编译完成的字节码。但是这里有一个重要的局限性:注解处理器不能改变源码,只能改变和影响新生成的文件。
注解处理器需要在编译器注册之后才能使用。例如Lombok中实现了如下的一个注解处理器。
在Lombok中,编译处理器是通过META-INF/services/javax.annotation.processing.Processor文件在编译器中进行注册。“javac -processor” 命令也可以注册注解处理器。而在maven当中,可以通过maven的编译插件进行注解处理器注册。
4. Samples
@Override 通知编译器该方法重写了一个父类或接口方法。
@SupressWarnings 会把编译器警告消除。使用该注解时,推荐把该注解放在最小的范围上从而避免对其他警告的错误消除。
@Deprecated 表明被注解的目标已经过时。编译器会在过时方法被重写或者使用后发出警告。
@FunctionalInterface 标明该接口可以被用作Lambda表达式。同时它会强制该接口有且只能有一个抽象的方法。