浅析Java注解的意义、分类、用途、如何自定义注解使用示例及Java注解的实现原理的分析
今天将从以下4个方面来系统的学习一下java注解:什么是注解、注解的用途、注解使用演示、注解的实现原理。
一、什么是注解
Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和任何元数据(metadata)的途径和方法。Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
Annotation(注解)是JDK5.0及以后版本引入的。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。从某些方面看,annotation就像修饰符一样被使用,并应用于包、类 型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在Annotation的“name=value”结构对中。
一般常用的注解可以分为三类:
1、一类是Java自带的标准注解,包括@Override(标明重写某个方法)、@Deprecated(标明某个类或方法过时)和@SuppressWarnings(标明要忽略的警告),使用这些注解后编译器就会进行检查
2、一类为元注解,元注解是用于定义注解的注解,包括@Retention(标明注解被保留的阶段)、@Target(标明注解使用的范围)、@Inherited(标明注解可继承)、@Documented(标明是否生成javadoc文档)
3、一类为自定义注解,可以根据自己的需求定义注解
二、什么是metadata(元数据)
元数据从metadata一词译来,就是“关于数据的数据”的意思。
元数据的功能作用有很多,比如:你可能用过Javadoc的注释自动生成文档。这就是元数据功能的一种。
总的来说,元数据可以用来创建文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件。如果要对于元数据的作用进行分类,目前还没有明确的定义,不过我们可以根据它所起的作用,大致可分为三类:
1. 编写文档:通过代码里标识的元数据生成文档
2. 代码分析:通过代码里标识的元数据对代码进行分析
3. 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查
在Java中元数据以标签的形式存在于Java代码中,元数据标签的存在并不影响程序代码的编译和执行,它只是被用来生成其它的文件或针在运行时知道被运行代码的描述信息。
综上所述:
第一,元数据以标签的形式存在于Java代码中。
第二,元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的。
第三,元数据需要编译器之外的工具额外的处理用来生成其它的程序部件。
第四,元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部。
三、注解的用途
在看注解的用途之前,有必要简单的介绍下XML和注解区别:
注解:是一种分散式的元数据,与源代码紧绑定。
XML:是一种集中式的元数据,与源代码无绑定
当然网上存在各种XML与注解的辩论哪个更好,这里不作评论和介绍,主要介绍一下注解的主要用途:
1、生成文档,通过代码里标识的元数据生成javadoc文档。
2、编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
3、编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
4、运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例
四、注解使用演示
这边总共定义了4个注解来演示注解的使用:
1、定义一个可以注解在Class、interface、enum上的注解
2、定义一个可以注解在METHOD上的注解
3、定义一个可以注解在FIELD上的注解
4、定义一个可以注解在PARAMETER上的注解
具体代码如下:
// 定义一个可以注解在Class,interface,enum上的注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnTargetType {
// 定义注解的一个元素 并给定默认值
String value() default "我是定义在类、接口、枚举类上的注解元素value的默认值";
}
// 定义一个可以注解在METHOD上的注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnTargetMethod {
// 定义注解的一个元素 并给定默认值
String value() default "我是定义在方法上的注解元素value的默认值";
}
// 定义一个可以注解在FIELD上的注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnTargetField {
String value() default "我是定义在字段上的注解元素value的默认值";
}
// 定义一个可以注解在PARAMETER上的注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnTargetParameter {
String value() default "我是定义在参数上的注解元素value的默认值";
}
编写一个测试处理类处理以上注解,这边不过多解释,代码中都写了对应的注释如下:
// 测试java注解类
@MyAnTargetType // 类的注解
public class AnnotationTest {
@MyAnTargetField // 字段的注解
private String field = "我是字段";
@MyAnTargetMethod("测试方法") // 方法的注解
public void test(@MyAnTargetParameter String args) { // 参数的注解
System.out.println("参数值 === "+args);
}
public static void main(String[] args) {
// 获取类上的注解MyAnTargetType
MyAnTargetType t = AnnotationTest.class.getAnnotation(MyAnTargetType.class);
System.out.println("类上的注解值 === "+t.value());
MyAnTargetMethod tm = null;
try {
// 根据反射获取AnnotationTest类上的test方法
Method method = AnnotationTest.class.getDeclaredMethod("test",String.class);
// 获取方法上的注解MyAnTargetMethod
tm = method.getAnnotation(MyAnTargetMethod.class);
System.out.println("方法上的注解值 === "+tm.value());
// 获取方法上的所有参数注解 循环所有注解找到MyAnTargetParameter注解
Annotation[][] annotations = method.getParameterAnnotations();
for(Annotation[] tt : annotations){
for(Annotation t1:tt){
if(t1 instanceof MyAnTargetParameter){
System.out.println("参数上的注解值 === "+((MyAnTargetParameter) t1).value());
}
}
}
method.invoke(new AnnotationTest(), "改变默认参数");
// 获取AnnotationTest类上字段field的注解MyAnTargetField
MyAnTargetField fieldAn = AnnotationTest.class.getDeclaredField("field").getAnnotation(MyAnTargetField.class);
System.out.println("字段上的注解值 === "+fieldAn.value());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 运行结果:
类上的注解值 === 我是定义在类接口枚举类上的注解元素value的默认值
参数上的注解值 === 我是定义在参数上的注解元素value的默认值
参数值 === 改变默认参数
方法上的注解值 === 测试方法
字段上的注解值 === 我是定义在字段上的注解元素value的默认值
五、注解的实现原理
以上只抽取了注解的其中几种类型演示,下面让我们一起来看看他们是怎么工作的。
让我们先看一下实现注解三要素:注解声明、使用注解的元素、操作注解使其起作用(注解处理器)
1、注解声明
首先我们让看一下java中的元注解(也就是上面提到的注解的注解),总共有4个如下:
@Target,
@Retention,
@Documented,
@Inherited
这4个元注解都是在jdk的java.lang.annotation包下面。
(1)@Target:Target说明的是Annotation所修饰的对象范围,源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration*/
TYPE_PARAMETER,
/**
* Use of a type*/
TYPE_USE
}
其中只有一个元素ElementType,再看看它的源码。ElementType是一个枚举类,定义了注解可以作用的类型,上面例子中演示了TYPE,FIELD,METHOD,PARAMETER 4种可以作用的目标。
(2)@Retention:定义了该Annotation被保留的时间长短。
某些Annotation仅出现在源代码中,而被编译器丢弃;
而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,
而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
使用这个元注解可以对 Annotation的“生命周期”进行限制。
(3)@Documented:@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
Documented是一个标记注解,没有成员。
(4)@Inherited:@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
这边针对这个小编专门写了一个例子方便大家理解,在以上MyAnTargetType注解类中增加@Inherited注解,如下:
/**
* 定义一个可以注解在Class,interface,enum上的注解
* 增加了@Inherited注解代表允许继承
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyAnTargetType {
/**
* 定义注解的一个元素 并给定默认值
* @return
*/
String value() default "我是定义在类、接口、枚举类上的注解元素value的默认值";
}
增加一个子类ChildAnnotationTest继承AnnotationTest测试如下:
// 增加一个子类继承AnnotationTest 演示@Inherited注解允许继承
public class ChildAnnotationTest extends AnnotationTest {
public static void main(String[] args) {
// 获取类上的注解MyAnTargetType
MyAnTargetType t = ChildAnnotationTest.class.getAnnotation(MyAnTargetType.class);
System.out.println("类上的注解值 === "+t.value());
}
}
// 结果:
类上的注解值 === 我是定义在类接口枚举类上的注解元素value的默认值
说明已经获取到了父类AnnotationTest的注解了。如果MyAnTargetType去掉@Inherited注解,运行则报错如下
Exception in thread "main" java.lang.NullPointerException
at com.test.run.ChildAnnotationTest.main(ChildAnnotationTest.java:17)
2、使用注解的元素
使用注解没什么好说的,就是在你需要的地方加上对应的你写好的注解就行。
3、注解处理器
这个是注解使用的核心了,前面我们说了那么多注解相关的,那到底java是如何去处理这些注解的呢。
从getAnnotation进去可以看到java.lang.class实现了AnnotatedElement方法
MyAnTargetType t = AnnotationTest.class.getAnnotation(MyAnTargetType.class);
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement
java.lang.reflect.AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
(1)方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
(2)方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
(3)方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
(4)方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响
六、自定义注解
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。
在定义注解时,不能继承其他的注解或接口。
@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
1、定义注解格式:public @interface 注解名 {定义体}
2、注解参数的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
3、Annotation类型里面的参数该怎么设定
第一、只能用public或默认(default)这两个访问权修饰,例如 String value(); 这里把方法设为defaul默认类型;
第二、参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组。例如,String value();这里的参数成员就为String;
第三、如果只有一个参数成员,最好把参数名称设为"value",后加小括号。
4、注解元素的默认值:
注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。
因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。例如:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
// 供应商编号
public int id() default -1;
// 供应商名称
public String name() default "";
}
定义了注解,并在需要的时候给相关类,类属性加上注解信息,如果没有相应的注解信息处理流程,注解可以说是没有实用价值。如何让注解真真的发挥作用,主要就在于注解处理方法。
七、注解处理器
1、注解处理器类库(java.lang.reflect.AnnotatedElement):
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
Class:类定义
Constructor:构造器定义
Field:累的成员变量定义
Method:类的方法定义
Package:类的包定义
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的四个个方法来访问Annotation信息,上面有队4个方法的介绍。
2、一个简单的注解处理器
(1)第一步:注解声明
// 注解声明
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
// 颜色枚举
public enum Color{ BULE,RED,GREEN};
// 颜色属性
Color fruitColor() default Color.GREEN;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
// 供应商编号
public int id() default -1;
// 供应商名称
public String name() default "";
// 供应商地址
public String address() default "";
}
(2)第二步:使用注解
public class Apple {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=Color.RED)
private String appleColor;
@FruitProvider(id=1,name="***",address="***")
private String appleProvider;
}
(3)注解处理器
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz){
String strFruitName=" 水果名称:";
String strFruitColor=" 水果颜色:";
String strFruitProvicer="供应商信息:";
Field[] fields = clazz.getDeclaredFields();
for(Field field :fields){
if(field.isAnnotationPresent(FruitName.class)){
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName=strFruitName+fruitName.value();
System.out.println(strFruitName);
} else if(field.isAnnotationPresent(FruitColor.class)){
FruitColor fruitColor= (FruitColor) field.getAnnotation(FruitColor.class);
strFruitColor=strFruitColor+fruitColor.fruitColor().toString();
System.out.println(strFruitColor);
} else if(field.isAnnotationPresent(FruitProvider.class)){
FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class);
strFruitProvicer=" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
// 输出结果
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2017-06-24 浏览器兼容性处理小技巧随手记系列(一)