攻破Java技术盲点之剖析注解技术的实现原理和开发指南
- 什么是Annotation注解
- Annotation的定义
- Annotation的作用
- Annotation不会影响程序代码的执行
- 什么是metadata(元数据)
- Annotation和Annotation类型
- Annotation
- Annotation类型
- 注解的分类(参数个数)
- 注解的分类(作用和用途)
- 系统内置标准注解
- @Override(限定重写父类方法)
- @Deprecated(标记已过时)
- @deprecated和@Deprecated
- SuppressWarnnings(抑制编译器警告)
- 忽略警告
- SuppressWarnings注解的常见参数
- 注解(Annotation)自定义注解
- 元注解
- Java定义的元注解
- @Target
- Annotation可以应用于以下对象
- 取值(ElementType)
- 使用案例
- @Retention
- 取值(RetentionPoicy)
- @Documented
- @Inherited
- 自定义注解
- 定义注解格式
- 注解参数的可支持数据类型
- Annotation类型里面的参数该怎么设定
- 简单的自定义注解和使用注解实例
- 注解元素的默认值
- 注解(Annotation)注解处理器
- 注解处理器类库(java.lang.reflect.AnnotatedElement)
- AnnotatedElement
- 注解处理器测试类
什么是Annotation注解
Annotation(注解)是在JDK 5.0及更高版本中引入的强大工具。它不仅可用于创建文档,还能跟踪代码中的依赖关系,甚至在编译时执行基本检查。从某些角度来看,annotation与修饰符的使用方式相似,可以应用于包、类、构造方法、方法、成员变量、参数和本地变量的声明中。这些信息被存储在Annotation的“name=value”结构中,使得代码更加清晰、易于维护,并提高了可读性。
Annotation的定义
在Annotation类型中,成员的声明采用无参数方法的形式,这意味着方法名和返回值共同定义了成员的名称和类型,下面用Spring框架中的RequestMapping注解举例:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
Annotation允许为其成员指定默认值,这意味着你可以使用name=value对为没有默认值的Annotation成员赋值,或者用它来覆盖其他成员的默认值。
@Slf4j
@RestController
@RequestMapping(value = "/support/blackList")
public class BlackListController {
这与类的继承特性有些相似,就像父类的构造函数可以作为子类的默认构造函数,但也可以被子类覆盖一样。
Annotation的作用
Annotation是一种元数据结构,可用于关联信息与程序元素(如类、方法、成员变量等),为元程序元素与各种信息、元数据之间建立了一种有效的关联方式。它实质上是一个接口,使得程序能够通过反射技术,获取特定程序元素的Annotation对象,进而访问注解中的元数据。
Annotation不会影响程序代码的执行
无论添加或删除Annotation,代码的执行方式保持不变。虽然一些Annotation可以通过Java的反射API在运行时被访问,但Java语言解释器在执行时忽略了这些Annotation。这导致Annotation在代码中看似“不起作用”,实际上只有通过特定的工具才能访问和处理Annotation中的信息。
本文将涵盖标准的Annotation和meta-Annotation类型,这些类型的工具通常需要以特殊方式处理,例如通过Java编译器。
什么是metadata(元数据)
元数据是关于数据的数据。它能够用于多种功能,如通过Javadoc注释自动生成文档。总体而言,元数据可用于生成文档、分析代码、进行编译时格式检查和替代配置文件等任务。根据其功能,元数据大致可分为以下三类:
在Java中,元数据以标签的形式存在于代码中。元数据标签不会影响程序的编译和执行,它仅用于生成其他文件或在运行时获取代码的描述信息。
Annotation和Annotation类型
Annotation
Annotation是Java 5.0引入的新语法,它的行为类似于修饰符(如public、final)。每个Annotation都有一个名称,并且可以具有零个或多个成员。每个Annotation的成员由键值对(类似于JavaBean)组成,用于存储Annotation的信息。
Annotation类型
Annotation类型定义了Annotation的名称、类型和成员默认值。可以将Annotation类型看作是Java中的特殊接口,其成员变量受限制,并且声明Annotation类型时需要使用新的语法。
通过Java反射API访问Annotation时,返回的对象实际上是实现了该Annotation类型接口的对象。通过访问这个对象,我们可以方便地访问其Annotation成员。在Java 5.0的java.lang包中,包含了三个标准Annotation类型。后面的章节将详细介绍这些标准Annotation类型。
注解的分类(参数个数)
根据注解参数的个数,我们可以将注解分为三类:
-
标记注解:没有成员定义的Annotation类型被称为标记注解。标记注解仅通过其存在与否来提供信息,不包含任何成员。一个例子是Java中的系统注解@Override。
-
单值注解:只有一个成员定义的Annotation类型被称为单值注解。单值注解可以通过提供一个值来设置成员。例如,我们可以使用注解@Deprecated来标记被废弃的API。
-
完整注解:具有多个成员定义的Annotation类型被称为完整注解。完整注解可以通过为每个成员赋值来设置注解的各个属性。这是最常见的类型,例如常见的注解如@RequestMapping、@Autowired等都属于完整注解。
注解的分类(作用和用途)
根据注解使用方法和用途,我们可以将Annotation分为三类:
系统内置标准注解
注解的语法相对简单,除了使用@符号外,其余基本与Java的语法保持一致。在JavaSE中,内置了三个标准注解,这些注解定义在java.lang包中,下面我们依次看看三个内置标准注解的作用和使用场景。
- @Override:用于修饰此方法覆盖了父类的方法;
- @Deprecated:用于修饰已经过时的方法;
- @SuppressWarnnings:用于通知java编译器禁止特定的编译警告。
@Override(限定重写父类方法)
@Override是一个标记注解,用于标注方法。它表示被标注的方法是重写父类方法的,起到断言的作用。如果我们在一个没有覆盖父类方法的方法上使用这个注解,Java编译器将会产生一个编译错误来提醒我们。这个注解经常在我们试图重写父类方法但却拼写错误时发挥作用。使用它非常简单,只需在要修饰的方法前面加上@Override注解即可。
下面的代码展示了一个使用@Override注解修饰的示例,该方法企图重载父类的displayName()方法,但存在拼写错误:
@Override
public class Fruit {
public void displayName(){
System.out.println("水果的名字是:*****");
}
}
class Orange extends Fruit {
@Override
public void displayName(){
System.out.println("水果的名字是:桔子");
}
}
class Apple extends Fruit {
@Override
public void displayname(){
System.out.println("水果的名字是:苹果");
}
}
这样,如果我们在编译时存在拼写错误,编译器将会报错,提醒我们修正错误。例如,Orange 类编译不会有任何问题,Apple 类在编译的时候会提示相应的错误。@Override注解只能用于方法,不能用于其他程序元素。
@Deprecated(标记已过时)
@Deprecated也是一种标记注解,当一个类型或者类型成员被@Deprecated修饰时,编译器会不推荐使用被标注的程序元素。而且这个标记具有一定的"延续性":即使我们通过继承或者覆盖的方式使用了被标记为过时的类型或者成员,尽管继承或者覆盖后的类型或者成员没有被声明为@Deprecated,但编译器仍然会给出警告。
注意,@Deprecated这个注解类型和javadoc中的@deprecated标签有所区别:前者是Java编译器识别的,而后者是被javadoc工具识别用于生成文档,主要包含了被过时的程序成员应该为什么过时以及应该如何被禁止或替代的描述。
@deprecated和@Deprecated
Java编译器仍然像之前的版本一样寻找@deprecated这个javadoc标签,并使用它们来产生警告信息。但是,这种情况在将来的版本中可能会有所改变。因此,我们应该从现在开始就使用@Deprecated注解来标记过时的方法,而不是使用@deprecated javadoc标签。
下面一段程序中使用了@Deprecated注解标示方法过期,同时在方法注释中用@deprecated tag 标示该方法已经过时,代码如下:
class AppleService {
public void displayName(){
System.out.println("水果的名字是:苹果");
}
/**
* @deprecated 该方法已经过期,不推荐使用
*/
@Deprecated
public void showTaste(){
System.out.println("水果的苹果的口感是:脆甜");
}
public void showTaste(int typeId){
if(typeId==1){
System.out.println("水果的苹果的口感是:酸涩");
}
else if(typeId==2){
System.out.println("水果的苹果的口感是:绵甜");
}
else{
System.out.println("水果的苹果的口感是:脆甜");
}
}
}
public class FruitRun {
/**
* @param args
*/
public static void main(String[] args) {
Apple apple=new Apple();
apple.displayName();
AppleService appleService=new AppleService();
appleService.showTaste();
appleService.showTaste(0);
appleService.showTaste(2);
}
}
AppleService类的showTaste() 方法被@Deprecated标注为过时方法,在FruitRun类中使用的时候,编译器会给出该方法已过期,不推荐使用的提示。
SuppressWarnnings(抑制编译器警告)
@SuppressWarnings是用于有选择地关闭编译器对类、方法、成员变量和变量初始化的警告。Sun提供的javac编译器为我们提供了-Xlint选项,用于对合法的程序代码提出警告。这些警告在一定程度上表示了可能存在的程序错误。
-Xlint选项后面可以跟上不同的警告类型,例如:
- -Xlint:unchecked:启用未经检查的操作警告,用于泛型代码中的未检查转换。
- -Xlint:deprecation:启用已过时的API使用警告,用于标记已弃用的方法或类的使用。
- -Xlint:rawtypes:启用原始类型使用警告,用于发现使用了未使用泛型的代码。
当我们在使用一个泛型集合类时未提供类型参数,编译器将会给出"unchecked warning"的警告。通常情况下,当出现此类警告时,我们需要
检查引发警告的代码,如果确实存在问题,则需要进行修正。如果警告提示我们的代码中的switch语句没有覆盖所有可能的case,我们可以通过添加一个默认的case来避免此警告。
忽略警告
有时候,我们无法避免出现警告,例如在与非泛型的旧代码进行交互时使用泛型集合类时就会产生未检查的警告。这时,@SuppressWarnings就派上用场了。我们可以在调用这些方法之前加上@SuppressWarnings修饰,告诉编译器忽略这个方法的警告。
@SuppressWarnings不是一个标记注解。它具有一个名为value的成员,类型为String[],用于指定需要禁止的警告。对于javac编译器而言,与-Xlint选项有效的警告名称对@SuppressWarnings同样有效,并且编译器会忽略无法识别的警告名称。
annotation语法允许在annotation名后跟括号,括号中是使用逗号分割的name=value对用于为annotation的成员赋值。案例代码如下:
public class FruitService {
@SuppressWarnings(value={ "rawtypes", "unchecked" })
public static List<Fruit> getFruitList(){
List<Fruit> fruitList=new ArrayList();
return fruitList;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static List<Fruit> getFruit(){
List<Fruit> fruitList=new ArrayList();
return fruitList;
}
@SuppressWarnings("unused")
public static void main(String[] args){
List<String> strList=new ArrayList<String>();
}
}
在这个例子中SuppressWarnings annotation类型只定义了一个单一的成员,所以只有一个简单的value={…}作为name=value对。又由于成员值是一个数组,故使用大括号来声明数组值。
注意:我们可以在下面的情况中缩写annotation:当annotation只有单一成员,并成员命名为"value=“。这时可以省去"value=”。比如将上面方法getFruit()的SuppressWarnings annotation就是缩写的。
SuppressWarnings注解的常见参数
注解(Annotation)自定义注解
要深入学习注解,我们就必须能定义自己的注解,并使用注解,在定义自己的注解之前,我们就必须要了解Java为我们提供的元注解和相关定义注解的语法。
元注解
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
Java定义的元注解
这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明。
@Target
@Target用于指定Annotation修饰的对象范围。
Annotation可以应用于以下对象
packages(包),types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
作用:通过在Annotation类型的声明中使用@Target,可以清晰地指定其所修饰的目标,用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
使用案例
@Target(ElementType.TYPE)
public @interface Table {
/**
* 数据表名称注解,默认值为类名称
* @return
*/
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {}
注解Table 可以用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。
@Retention
@Retention用于定义Annotation的保留策略,即该Annotation在何时被保留,使用@Retention这个元注解可以对Annotation的"生命周期"进行限制。
有些Annotation只存在于源代码中,在编译后会被丢弃;而其他一些Annotation会保留在编译生成的class文件中;而另一些被编译在class文件中的Annotation可能会被虚拟机忽略,而只在类被加载时读取(需要注意的是,Annotation的使用与类的执行是相互独立的)。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)
Retention meta-annotation类型有唯一的value作为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。
具体实例如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
Column注解的的RetentionPolicy的属性值是RUTIME,这样注解处理器可以通过反射,获取到该注解的属性值,从而去做一些运行时的逻辑处理
@Documented
@Documented用于标记其他类型的注解,以便将其文档化并作为被标注的程序成员的公共API。这意味着这些被标注的注解可以被类似javadoc的工具文档化。@Documented是一个标记注解,没有任何成员。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
@Inherited
@Inherited是一个标记注解,用于说明被标注的类型可以被继承。如果一个使用了@Inherited修饰的注解类型被应用于一个类上,那么这个注解将被继承到该类的子类上。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
@Inherited
public @interface Greeting {
public enum FontColor{ BULE,RED,GREEN};
String name();
FontColor fontColor() default FontColor.GREEN;
}
自定义注解
使用@interface自定义注解时,会自动继承java.lang.annotation.Annotation接口,并由编译程序自动完成其他细节。在定义注解时,无法继承其他注解或接口。@interface用于声明一个注解,其中的每个方法实际上是声明一个配置参数。方法名即为参数名,返回值类型即为参数类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default关键字声明参数的默认值。
定义注解格式
public @interface 注解名 {定义体}
注解参数的可支持数据类型
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String类型
- Class类型
- enum类型
- Annotation类型
- 以上所有类型的数组
Annotation类型里面的参数该怎么设定
- 自定义注解的访问修饰符只能是public或默认的(default)。
- 参数成员可以是基本类型、String、Enum、Class、注解等数据类型,以及它们的数组。
- 如果注解只有一个参数成员,通常将其命名为"value",并在后面加上小括号。
简单的自定义注解和使用注解实例
package annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
package annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
/**
* 颜色枚举
* @author peida
*
*/
public enum Color{ BULE,RED,GREEN};
/**
* 颜色属性
* @return
*/
Color fruitColor() default Color.GREEN;
}
package annotation;
import annotation.FruitColor.Color;
public class Apple {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=Color.RED)
private String appleColor;
public void setAppleColor(String appleColor) {
this.appleColor = appleColor;
}
public String getAppleColor() {
return appleColor;
}
public void setAppleName(String appleName) {
this.appleName = appleName;
}
public String getAppleName() {
return appleName;
}
public void displayName(){
System.out.println("水果的名字是:苹果");
}
}
注解元素的默认值
注解元素必须具有确定的值。这个值可以在定义注解时指定默认值,也可以在使用注解时指定。对于非基本类型的注解元素,它们的值不能为null。因此,在默认值中使用空字符串或0是一种常见的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在并具有相应的值。为了解决这个约束,我们可以定义一些特殊的值,例如空字符串或负数,用于表示某个元素不存在。这已经成为定义注解时的一种惯用法。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供应商编号
* @return
*/
public int id() default -1;
/**
* 供应商名称
* @return
*/
public String name() default "";
/**
* 供应商地址
* @return
*/
public String address() default "";
}
定义了注解,并在需要的时候给相关类,类属性加上注解信息,如果没有响应的注解信息处理流程,注解可以说是没有实用价值。如何让注解真真的发挥作用,主要就在于注解处理方法,接下来我们将学习注解信息的获取和处理!
注解(Annotation)注解处理器
如果没有用于读取注解的方法和工具,那么注解就无法发挥比注释更大的作用。在使用注解的过程中,注解处理器的创建和使用是非常重要的一环。
通过使用Java的反射API,我们可以在运行时获取和处理注解,实现自定义的注解处理逻辑。这些注解处理器可以帮助我们在编译时或运行时对注解进行应用,执行特定的操作。
注解处理器类库(java.lang.reflect.AnnotatedElement)
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。此外,Java还在java.lang.reflect包中引入了AnnotatedElement接口,该接口代表可以接受注解的程序元素。
AnnotatedElement
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement接口主要有以下几个实现类:
通过这些实现类,我们可以在运行时获取程序元素上的注解,并进行相应的操作。它们为我们提供了更丰富的注解处理能力,能够动态地检查和操作注解。通过使用这些类,我们可以实现个方法来访问Annotation信息:
方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass)
- 返回该程序元素上存在的指定类型的注解,如果该类型注解不存在,则返回null。
方法2:Annotation[] getAnnotations()
- 返回该程序元素上存在的所有注解。
方法3:boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
- 判断该程序元素上是否存在指定类型的注解,存在则返回true,否则返回false。
方法4:Annotation[] getDeclaredAnnotations()
- 返回直接存在于此元素上的所有注解。该方法忽略继承的注解。如果没有直接存在的注解,则返回长度为零的空数组。
注解处理器测试类
/***********注解声明***************/
/**
* 水果名称注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
/**
* 水果颜色注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
/**
* 颜色枚举
* @author peida
*
*/
public enum Color{ BULE,RED,GREEN};
/**
* 颜色属性
* @return
*/
Color fruitColor() default Color.GREEN;
}
/**
* 水果供应者注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供应商编号
* @return
*/
public int id() default -1;
/**
* 供应商名称
* @return
*/
public String name() default "";
/**
* 供应商地址
* @return
*/
public String address() default "";
}
/***********注解使用***************/
public class Apple {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=Color.RED)
private String appleColor;
@FruitProvider(id=1,name="陕西红富士集团",address="陕西省西安市延安路89号红富士大厦")
private String appleProvider;
public void setAppleColor(String appleColor) {
this.appleColor = appleColor;
}
public String getAppleColor() {
return appleColor;
}
public void setAppleName(String appleName) {
this.appleName = appleName;
}
public String getAppleName() {
return appleName;
}
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
public void displayName(){
System.out.println("水果的名字是:苹果");
}
}
/***********注解处理器***************/
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 {
/**
* @param args
*/
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
}
}