一、概念:
Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
注解与注释很类似,但是注解是给应用程序看的,单独使用注解毫无意义,一定要跟工具一起使用,这个所谓的工具实际就是能读懂注解的应用程序;而注释对代码没有影响。对代码起到解释、说明的作用;
Annotation能被用来为某个程序元素(类、方法、成员变量等)关联任何的信息。需要注意的是,这里存在着一个基本的规则:Annotation不能影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一的执行。另外,尽管一些annotation通过java的反射api方法在运行时被访问,而java语言解释器在工作时忽略了这些annotation。正是由于java虚拟机忽略了Annotation,导致了annotation类型在代码中是“不起作用”的; 只有通过某种配套的工具才会对annotation类型中的信息进行访问和处理。
根据注解参数的个数,我们可以将注解分为三类:
- 标记注解:一个没有成员定义的Annotation类型被称为标记注解。这种Annotation类型仅使用自身的存在与否来为我们提供信息。比如后面的系统注解@Override;
- 单值注解
- 完整注解
根据注解使用方法和用途,我们可以将Annotation分为三类:
- JDK内置系统注解;
- 元注解;
- 自定义注解
二、JDK内置注解:
JavaSE中内置三个标准注解,定义在java.lang中:
1.@Override:用于修饰此方法覆盖了父类的方法;
@Override 是一个标记注解类型,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种Annotation在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示。
2.@Deprecated:用于修饰已经过时的方法;
Deprecated也是一个标记注解。当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。而且这种修饰具有一定的 “延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为 @Deprecated,但编译器仍然要报警。
3.@SuppressWarnnings:用于通知java编译器禁止特定的编译警告。
@SuppressWarnings 被用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。在java5.0,sun提供的javac编译器为我们提供了-Xlint选项来使编译器对合法的程序代码提出警告,此种警告从某种程度上代表了程序错误。例如当我们使用一个generic collection类而又没有提供它的类型时,编译器将提示出"unchecked warning"的警告。通常当这种情况发生时,我们就需要查找引起警告的代码。如果它真的表示错误,我们就需要纠正它。例如如果警告信息表明我们代码中的switch语句没有覆盖所有可能的case,那么我们就应增加一个默认的case来避免这种警告。
有时我们无法避免这种警告,此时@SuppressWarning就要派上用场了,在调用的方法前增加@SuppressWarnings修饰,告诉编译器停止对此方法的警告。
SuppressWarning不是一个标记注解。它有一个类型为String[]的成员,这个成员的值为被禁止的警告名。
SuppressWarning常见的参数值说明:
序号 |
参数值 |
说明 |
1 |
deprecation |
使用了不赞成使用的类或方法时的警告; |
3 |
fallthrough |
当 Switch 程序块直接通往下一种情况而没有 Break 时的警告; |
4 |
path |
在类路径、源文件路径等中有不存在的路径时的警告; |
5 |
serial |
当在可序列化的类上缺少 serialVersionUID 定义时的警告; |
6 |
finally |
任何 finally 子句不能正常完成时的警告; |
7 |
all |
以上所有情况的警告 |
三、元注解:
1、@Target:
@Target说明了Annotation所修饰的对象范围,取值有:
序号 |
参数值 |
说明 |
1 |
CONSTRUCTOR |
用于描述构造器 |
2 |
FIELD |
用于描述域 |
3 |
LOCAL_VARIABLE |
用于描述局部变量 |
4 |
METHOD |
用于描述方法 |
5 |
PACKAGE |
用于描述包 |
6 |
PARAMETER |
用于描述参数 |
7 |
TYPE |
用于描述类、接口(包括注解类型) 或enum声明 |
2、@Retention:
@Retention定义了该Annotation被保留的时间长短,表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效);取值有:
序号 |
参数值 |
说明 |
1 |
SOURCE |
在源文件中有效(即源文件保留) 表示注解只在java文件里面才有,在编译的时候,这部分注解就会被擦出。类似于@Override只是给程序员看的。 |
2 |
CLASS |
在class文件中有效(即class保留) 注解在编译的时候会存在,在运行的时候就会擦除。 |
3 |
RUNTIME |
在运行时有效(即运行时保留) 在运行的时候会存在,这里就要用到了反射来实现了。 |
3、@Document:
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
4、@Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
四、自定义注解:
1、自定义注解格式:
public @interface 注解名 {定义体}
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。
2、注解参数的可支持数据类型:
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String类型
- Class类型
- enum类型
- Annotation类型
- 以上所有类型的数组
3、注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。
4、注解处理工具类:
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
- Class:类定义
- Constructor:构造器定义
- Field:累的成员变量定义
- Method:类的方法定义
- Package:类的包定义
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
序号 |
名称 |
说明 |
1 |
getAnnotation |
返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。 |
2 |
getAnnotations |
返回该程序元素上存在的所有注解。 |
3 |
AnnotationPresent |
判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false. |
4 |
getDeclaredAnnotations |
返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。) |
使用示例如下:
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); } } |
五、编译时注解:
在编译期间,JVM会自动运行注解处理器(当然,我们需要将其注册)。虽然我们写的Java代码被编译成class就不能被改动了,但是注解处理器会重新生成其他的java代码,我们可以通过反射来调用新生成的java文件里面的类或方法。然后JVM再对这些生成的java代码进行编译。
AbstractProcessor被称为抽象处理器类,每一个自定义注解处理器都是从这个类继承,这个类的方法说明如下:
序号 |
名称 |
说明 |
1 |
init |
对一些工具进行初始化。 通常写法如下: super.init(env); elementUtils = env.getElementUtils(); filer = env.getFiler(); typeUtils = env.getTypeUtils(); messager = env.getMessager(); |
2 |
process |
就是真正生成java代码的地方。 |
3 |
getSupportedAnnotationTypes |
表示该注解处理器可以出来那些注解的集合。 |
4 |
getSupportedSourceVersion |
返回能支持的最近的JDK版本;通常SourceVersion.latestSupported就能返回这个版本; |
Init方法初始化的工具类介绍:
1、Element:
Element代表程序的元素,例如包、类或者方法。每个Element代表一个静态的、语言级别的构件。它只是结构化的文本,他不是可运行的.可以理解为一个签名;
如下例子:
package com.example; // PackageElement public class Foo { // TypeElement private int a; // VariableElement private Foo other; // VariableElement public Foo () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA) // TypeElement { ............. } } |
而且可以通过aType.getEnclosingElement();取得aType的父标签(没有父标签就返回NULL);
2、Messager:
因为注解处理器不能抛出Exception(因为是在编译器),因此错误信息输出就要用到Messager;
3、Types:一个用来处理TypeMirror的工具类;
4、Filer:这个工具可以支持向当前工程输出新的Java代码;
编译时注解处理器实现步骤:
1、派生自AbstractProcessor类实现自定义注解处理器,并实现其init, process, getSupportedAnnotationTypes和getSupportedSourceVersion方法,并在process方法里面完成代码生成的逻辑处理;
对于信息输出或者错误信息输出,可以使用messager.printMessage来输出信息;
2、注册注解处理器:
在目录“resources-->META-INF--->services”下建立文件javax.annotation.processing.Processor,在文件中填写刚才的从AbstractProcessor派生过来的自定义注解处理器的全名称;
3、生成jar文件;
4、使用时,在工程的“settings”中的“Build, execution, Deployment”--->“compiler”--------->“Annotation processors”中,勾选“Enable annotation processing”选项,并设置好自定义注解处理器生成路径,如下图所示:
5、使用注解并编译代码,查看生成的代码;
六、ibernate validator自定义注解:
1、自定义注解需要实现ConstraintValidator校验类,同时实现它下面的两个方法initialize、isValid,一个是初始化参数的方法,另一个就是校验逻辑的方法(可以将校验类定义在该注解内,用@Constraint(validatedBy = EnumValue.Validator.class)注解指定校验类);
2、对于被校验的方法我们要求,它必须是返回值类型为Boolean或boolean,并且必须是一个静态的方法,返回返回值为null时我们认为是校验不通过的,按false逻辑走。
示例如下:
1 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Constraint(validatedBy = NumberRange.NumberRangeValidator.class) 4 public @interface NumberRange { 5 int[] array() default {}; 6 7 String message() default "数值不在数组范围内"; 8 9 Class<?>[] groups() default {}; 10 11 Class<? extends Payload>[] payload() default {}; 12 13 class NumberRangeValidator implements ConstraintValidator<NumberRange, Integer> { 14 /** 15 * 数组验证注解 16 */ 17 private NumberRange numberRange; 18 19 /** 20 * 取得类属性的注解 21 * 22 * @param numberRange 23 */ 24 @Override 25 public void initialize(NumberRange numberRange) { 26 this.numberRange = numberRange; 27 } 28 29 @Override 30 public boolean isValid(Integer value, ConstraintValidatorContext context) { 31 int[] array = numberRange.array(); 32 33 if (array == null || array.length == 0) { 34 return true; 35 } else { 36 Arrays.sort(array); 37 if (Arrays.binarySearch(array, value) <= -1) { 38 return false; 39 } 40 } 41 return true; 42 } 43 } 44 }
3、可以使用Hibernate的校验功能对这些注解进行校验,示例如下:
1 public class ValidationUtil { 2 3 /** 4 * 使用hibernate的注解来进行验证 5 * 6 */ 7 private static Validator validator = Validation 8 .byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator(); 9 10 public static <T> void validate(T obj) throws Exception { 11 Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj); 12 // 抛出检验异常 13 if (constraintViolations.size() > 0) { 14 String msg = String.format("参数校验失败:%s", constraintViolations.iterator().next().getMessage()); 15 throw new Exception(msg); 16 } 17 } 18 }
4、参考文档: