一文搞懂Java注解
1.概述
Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
官方的解释总是让人一脸懵逼,但有两个点需要关注,一是元数据,二是不直接影响你的代码执行。
元数据:是用来描述数据的数据,这里就是指描述代码的数据。纳尼这不就是注释么?从一定意义上说确实和注释一样。
不直接影响你的代码执行:意思是有可能可以影响代码执行。注释如何影响代码执行?注解和注释最大的不同是我们可以通过代码运行或编译时拿到注解,进而影响代码执行。
Java注解不仅描述了源代码还能间接影响代码运行。
2.相关概念
Java中的注解主要分为三类:
2.1 Java内置注解
Java 内置常用注解共有5个,在java.lang包中。
- @Override - 检查该方法是否是重载方法。如果发现其父类或引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface - Java 8 开始支持,编译器javac检查一个接口是否符合函数接口的标准。
2.2 元注解
元注解是用于定义注解的注解。Java中的元注解都在java.lang.annotation包中,前4个是元注解,Java1.8添加了后面2个注解。
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
- @Native - 标记属性为native属性,用在代码中,给IDE工具做提示使用。
2.3 自定义注解
根据自己需求使用元注解定义我们的注解。
3.使用注解
先看一个注解的定义,使用@interface
关键字定义一个注解,还需要使用@Retention
标记该注解保留到什么时期,@Target
标记该注解可用在什么地方。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Override {}
要理解注解的使用,需要从元注解入手,其中@Retention 和 @Target 是定义一个注解所必须的。
3.1 理解元注解
@Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
// 返回该注解保留到什么阶段
RetentionPolicy value();
}
@Retention 的定义多了一个value()
方法,返回的是一个RetentionPolicy类型的值,这是一个枚举类型的值说明只能有一个保留策略。这个返回值是在使用注解@Retention(RetentionPolicy.RUNTIME)
括号内的值RetentionPolicy.RUNTIME 。
public enum RetentionPolicy {
SOURCE,/*仅在源码中保留,编译时会删除该注解*/
CLASS,/*保留到编译期,在Class字节码文件中保留,但JVM不会加载该注解;这是默认值*/
RUNTIME/*保留到运行期,JVM会加载该注解,所以可以被反射读取*/
}
RetentionPolicy有三个值,分别标记该注解是保留在源码、编译期还是运行期,只有运行期的注解才会被反射读取。
@Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
// 返回该注解可以在何处使用,可以有多个位置
ElementType[] value();
}
@Target
标记该注解可以用在什么位置,返回的是一个ElementTypep类型数组,说明一个注解可以被用在多种位置。
public enum ElementType {
/** 可用在类、接口、枚举声明前 */
TYPE,
/** 字段声明(包括枚举常量) */
FIELD,
/** 方法声明 */
METHOD,
/** 参数声明 */
PARAMETER,
/** 构造方法声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解声明*/
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/**
* 该注解可用于类型变量的声明语句中,如泛型声明
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 该注解能写在使用类型的任何语句中
* @since 1.8
*/
TYPE_USE
}
TYPE_PARAMETER 和 TYPE_USE 我们会重点做示例讲解,这里先不展开说。
@Documented
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {}
@Documented是一个纯粹的语义元注解,生成JavaDoc文档时,被@Documented标记的注解在所使用的地方会被文档收录进去,否则Java文档不会有该注解信息。
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {}
@Inherited标记的注解是可以被集成的,当父类被可继承的注解标记,子类会自动拥有该注解。在运行时将不断向上寻找对应注解是否存在。注意在方法和接口上的注解不具有继承特性。
@Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
Class<? extends Annotation> value();
}
@Repeatable 标记的注解,可以在同一个位置存在多个
3.2 定义注解
了解了元注解以及使用方式后,自定义注解就很简单了。
@元注解1
@元注解2
修饰符 @interface 注解名 {
注解元素的声明1
注解元素的声明2
}
(1)定义注解使用@interface
关键字
(2)使用@Retention
和@Target
元注解指定保留时期和使用位置。
(3)注解可以添加成员变量,定义类似方法,模式是:变量类型 变量名()
,还可以为成员变量添加默认值,模式是:变量类型 变量名() default 默认值
。
(4)成员变量可支持基本类型、String、Class、enum、Annotation、及其以上类型的数组。
(5)没有成员变量的注解称为标记,有成员变量的注解称为元数据。
下面举几个例子:
- 可用在类、接口、枚举之前使用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotaion {
String calssId() default "xbox";
int classCode();
}
- 可用在Method上的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(MethodAnnotationArray.class)
public @interface MethodAnnotation {
String tag() default "方法默认tag";
}
// 可重复注解的容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotationArray {
MethodAnnotation[] value();
}
- 可用在Field和参数上的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface FieldAnnotation {
boolean isCheck() default false;
}
- 可用在参数上的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ParameterAnnotation {
String value();
}
当注解有且只有一个名称为value的时,使用注解时可以不用写明名称。
@ParameterAnnotation(“顶顶顶顶”)
3.3 使用注解
@ClassAnnotaion(calssIds = "bacd",classCode = 34)
public class Demo1 {
@FieldAnnotation(isCheck = true)
String name;
@MethodAnnotation(tag = "demo方法")
@MethodAnnotation(tag = "可重复注解")
public void deal(@ParameterAnnotation("kk") String input){
System.out.println(input);
}
}
要使注解只需要在对应位置添加定义的注解
@注解名(变量1 = 值1 ,变量2 = 值2, 变量3 = {数组元素1,数组元素2,数组元素3})
注意:当定义注解只有一个成员变量,且属性名称为value,此时使用注解时可以在括号内直接对value赋值,而不用显式指定value = 值。
此时在代码上已经加上了自己定义的注解,运行这些代码,此时的注解和普通的注释一样,不会影响目前的代码。
如果想要利用这些注解达到间接改变代码执行的目的,需要编写专门的注解处理器,来解析并处理注解 。
3.4 注解使用的场景
(1)源码级别:
代表技术是APT,可以在编译时获取注解和注解标记的类中的信息,通常用于生成额外的辅助类。
Annotation Processing Tool 是Oracle的JDK中javac 的一个工具,是在将java源代码编译成字节码的过程中,调用经过注册的注解处理器来处理源代码。要点是要编写注解处理器,再注册注解处理器。
OpenJDK是不带这个工具的。
源码级别的注解也可以用于代码语法检查。比如:@IntDef
(2)字节码级别:
代表技术是字节码增强,通过修改Class数据来实现修改代码逻辑的目的。
字节码增强就是在字节码中写代码。
(3)运行时级别:
在程序运行时,通过反射技术动态获取注解及其元素,从而完成不同逻辑的判定。
3.4 处理注解
注解编译后同样会生成ClassAnnotaion.class 字节码,这里使用jad 工具反编译生成的字节码。
// jad ClassAnnotation.class 反编译后代码如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: ClassAnnotaion.java
package com.company.part1;
import java.lang.annotation.Annotation;
public interface ClassAnnotaion extends Annotation{
public abstract String calssIds();
public abstract int classCode();
}
从中可以看出,注解其实就是一个接口,并且继承了Annotation接口。该接口在java.lang.annotation
包。
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
/**
* @return the annotation type of this annotation
*/
Class<? extends java.lang.annotation.Annotation> annotationType();
}
通过以上源码可以看出,注解本质上就是一个接口,接口可以有成员变量和成员方法,但接口中的成员变量时static final 类型的,使用时无法重新赋值,这对注解来说是没意义的;所以注解使用成员方法来当做注解成员变量,这也解释了定义注解时,成员变量为什么带有括号。
要处理注解需要使用反射,注解可以在很多地方使用,java在java.lang.reflect
包中定义了一个AnnotatedElement
接口,这个接口定义了可以使用注解的类型,简而言之实现了该接口的类型都可以使用注解。
public interface AnnotatedElement {
// 指定类型的注解是否在当前元素上
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
}
// 在当前元素上获取指定类型注解的实例,有则返回,无则返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 返回当前元素上所有的注解,包括当前元素继承父类的注解。
Annotation[] getAnnotations();
// 返回当前元素上直接存在的注解。
Annotation[] getDeclaredAnnotations();
//------------------以下1.8新增-------------------
// 获取当前元素上指定类型的重复注解实例,
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
T[] result = getDeclaredAnnotationsByType(annotationClass);
if (result.length == 0 && // Neither directly nor indirectly present
this instanceof Class && // the element is a class
AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
Class<?> superClass = ((Class<?>) this).getSuperclass();
if (superClass != null) {
// Determine if the annotation is associated with the
// superclass
result = superClass.getAnnotationsByType(annotationClass);
}
}
return result;
}
// 获取当前元素上直接存在的指定类型注解
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
// Loop over all directly-present annotations looking for a matching one
for (Annotation annotation : getDeclaredAnnotations()) {
if (annotationClass.equals(annotation.annotationType())) {
// More robust to do a dynamic cast at runtime instead
// of compile-time only.
return annotationClass.cast(annotation);
}
}
return null;
}
// 获取当前元素上指定类型的重复注解
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
return AnnotationSupport.
getDirectlyAndIndirectlyPresent(Arrays.stream(getDeclaredAnnotations()).
collect(Collectors.toMap(Annotation::annotationType,
Function.identity(),
((first,second) -> first),
LinkedHashMap::new)),
annotationClass);
}
}
在1.8之前,一个元素上只能有一个相同类型的注解,1.8之后被Repeatable标记的注解在使用时可在一个元素上同时标注多个。
上面讲元注解**@Target**时提到了ElementType枚举,可以用于定义注解可以使用在哪些地方,下面列举部分。
ElementType | 实现类型 | 含义 |
---|---|---|
TYPE | Class | 代表了类、接口、枚举 |
FIELD | Field | 代表了成员变量 |
METHOD | Method | 代表了成员方法 |
CONSTRUCTOR | Constructor | 代表了构造函数 |
PARAMETER | Parameter | 代表了参数 |
… |
Class 定义如下,其实现了AnnotatedElement接口。
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {}
Field 定义如下,同样实现该接口:
class Field extends AccessibleObject implements Member {}
Constructor和Method定义如下:
public class AccessibleObject implements AnnotatedElement {}
public abstract class Executable extends AccessibleObject implements Member, GenericDeclaration {}
public final class Constructor<T> extends Executable {}
public final class Method extends Executable {]
Parameter定义如下:
public final class Parameter implements AnnotatedElement {}
所以通过反射来处理注解可以简单分为两步:
第一步:通过反射拿到想要处理的元素上的注解。
第二步:拿到注解中属性值,做进一步处理。
(1)处理类上的注解
Demo1 demo1 = new Demo1();
clazz = demo1.getClass();
if (clazz.isAnnotationPresent(ClassAnnotaion.class)) {
ClassAnnotaion classAnnotaion = (ClassAnnotaion)
clazz.getDeclaredAnnotation(ClassAnnotaion.class);
System.out.println("类型注解值:" +
classAnnotaion.calssIds() + " " +
classAnnotaion.classCode()
);
}
(2)处理属性上的注解
Field field = clazz.getField("name");
if (field.isAnnotationPresent(FieldAnnotation.class)) {
FieldAnnotation fieldAnnotation = field.getDeclaredAnnotation(FieldAnnotation.class);
System.out.println("属性注解值:" + fieldAnnotation.isCheck());
}
(3)处理方法上的注解
Method method = clazz.getMethod("deal", String.class, String.class);
if (method.isAnnotationPresent(MethodAnnotationArray.class)) {
MethodAnnotationArray methodAnnotationArray =
method.getDeclaredAnnotation(MethodAnnotationArray.class);
MethodAnnotation[] methodAnnotations = methodAnnotationArray.value();
for (MethodAnnotation temp : methodAnnotations) {
System.out.println("方法注解值:" + temp.tag());
}
}
可重复注解需要指定一个容器,虽然写的时候使用的是MethodAnnotation类型,但最终编译后会被装进MethodAnnotationArray,最终在方法上合并成一个MethodAnnotationArray类型注解。
(4)处理参数注解
Annotation[][] annotations1 = method.getParameterAnnotations();
for (int i = 0; i < annotations1.length; i++) {
System.out.println("第" + (i + 1) + "个参数的注解");
Annotation[] tmp = annotations1[i];
for (Annotation a : tmp) {
System.out.println(tmp);
}
}
一个方法有多个参数,一个方法又有多个参数,所以这里使用一个二维数组来存储参数列表中的注解。
4.自定义注解应用
自定义注解在工作中的应用还是很多的,这块准备自己再积累积累再做补充,使用+原理。
1.生成文档.例如:@see,@param,@return 等
2.代替配置文件功能.例如spring基于注解的配置
3.在编译时进行格式检查。
4.JUnit 、ButterKnife、Dagger2、Retrofit
参考:
Java 注解完全解析,by 若丨寒
深入理解java注解的实现原理, by 知了123
java元注解 @Target注解用法, by 就这个名字好
Java反射API研究(1)——注解Annotation, by 光闪
Java枚举和注解梳理, by itzhouq的博客
Java之注解的定义及使用, by 炼金术师cck