我们要学习一个东西,肯定要知道它的作用。相信没有 Java 程序猿不知道 Spring 框架的,Spring 框架中定义了大量的注解,基于注解,我们可以做到几乎零配置的开发商业项目,如 SpringBoot 就将原来 Spring 使用 XML 进行配置全部转换为使用注解达到相同的功效。本来是个非常普通直白的功能,Spirng可算是把注解玩出了花来。
注解存在的意义
代码的运行,本质上就是告诉CPU如何执行,执行的过程伴随着数据流的流动。同样的,你也可以理解成用数据来控制行为。那么问题就来了,我们java内部来传递信息可以使用简单数据类型,也可以使用一个类进行包裹。有些代码的执行需要部分信息指导,但是这部分信息每个都要使用类来承载会导致项目内类泛滥。注解的出现就是让小信息通过一种优雅的方式告诉整个虚拟机。
例如我们的测试类:
public class Test {
@Test
public void test001(){
System.out.println("Test.....");
}
}
其中@Test就是一个注解,在运行该方法时,测试框架会自动识别该方法并单独调用,运行时告诉测试框架该方法为测试方法。
注解的语法
我们先使用上面看到过的测试注解,来感受一下注解内部需要关心的信息。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
首先,需要告诉编译器这个类的性质是一个注解,这就需要使用@interface来声明Test注解。从代码上看注解的定义很像接口的定义,确实如此,毕竟在编译后也会生成Test.class文件
然后,我们的注解是需要针对我们的类成分做限制的,这就用到了@Target注解,传入ElementType.METHOD参数来标明@Test只能用于方法上。
再然后,我们还需要限制注解提供的信息的生命周期,这就用到了@Retention,RetentionPolicy.RUNTIME用来表示该注解生存期是运行时。
其中,@Target和@Retention是由Java提供的元注解,所谓元注解就是标记其他注解的注解。
元注解
所谓元注解就是,一种基本注解,可以应用在其它的注解上,也就是说,元注解是用于标记注解的注解。
元注解有6个:
- java.lang.annotation.Retention
- java.lang.annotation.Target
- java.lang.annotation.Documented
- java.lang.annotation.Inherited
- java.lang.annotation.Repeatable
- java.lang.annotation.Native
@Retention元注解
Retention 是保留的意思,表明注解产生的时间范围。其值是 java.lang.RetentionPolicy
枚举。
- RetentionPolicy.SOURCE : 只在源代码级别保留有用,在编译期就丢弃了
- RetentionPolicy.CLASS : 在编译期保留有效,在运行期(JVM中)开始丢弃;这是默认的保留策略
- RetentionPolicy.RUNTIME : 在编译期、运行其都保留有效,所以可以在反射中使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Demo {}
如果我们需要在运行时捕获成分上的注解信息,我们需要将该注解标记为RetentionPolicy.RUNTIME
@Documented
@Documented 表明注解的类型将会包含到Javadoc中去。
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}
//没有使用@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentB {
}
//使用注解
@DocumentA
@DocumentB
public class DocumentDemo {
public void A(){}
}
当你是用javadoc指令生成文档的时候,你会发现@DocumentB没有在文档内出现。
@Target
@Target 标明注解使用约束的应用上下文,是数组形式,可以标记在多个范围中使用。值由 java.lang.annotation.ElementType
指定。java.lang.annotation.ElementType 的可用值如下:
public enum ElementType {
/**标明该注解可以用于类、接口(包括注解类型)或enum声明*/
TYPE,
/** 标明该注解可以用于字段(域)声明,包括enum实例 */
FIELD,
/** 标明该注解可以用于方法声明 */
METHOD,
/** 标明该注解可以用于参数声明 */
PARAMETER,
/** 标明注解可以用于构造函数声明 */
CONSTRUCTOR,
/** 标明注解可以用于局部变量声明 */
LOCAL_VARIABLE,
/** 标明注解可以用于注解声明(应用于另一个注解上)*/
ANNOTATION_TYPE,
/** 标明注解可以用于包声明 */
PACKAGE,
/**
* 标明注解可以用于类型参数声明(1.8新加入)
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 类型使用声明(1.8新加入)
* @since 1.8
*/
TYPE_USE
}
当注解未指定Target值时,则此注解可以用于任何元素之上,如果需要使用多个值,那么就使用{}包含并用逗号隔开。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface Demo {
}
//TYPE_PARAMETER 标注在类型参数上
class D<@Parameter T> { }
//TYPE_USE则可以用于标注任意类型(不包括class)
//用于父类或者接口
class Image implements @Rectangular Shape { }
//用于构造函数
new @Path String("/usr/bin")
//用于强制转换和instanceof检查,注意这些注解中用于外部工具,它们不会对类型转换或者instanceof的检查行为带来任何影响。
String path=(@Path String)input;
if(input instanceof @Path String)
//用于指定异常
public Person read() throws @Localized IOException.
//用于通配符绑定
List<@ReadOnly ? extends Person>
List<? extends @ReadOnly Person>
@NotNull String.class //非法,不能标注class
import java.lang.@NotNull String //非法,不能标注import
这里主要说明一下TYPE_USE,类型注解用来支持在Java的程序中做强类型检查,配合第三方插件工具(如Checker Framework),可以在编译期检测出runtime error(如UnsupportedOperationException、NullPointerException异常),避免异常延续到运行期才发现,从而提高代码质量,这就是类型注解的主要作用。总之Java 8 新增加了两个注解的元素类型ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER ,通过它们,我们可以把注解应用到各种新场合中。
@Inherited
@Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解,如下:
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentB {}
@DocumentA
class A{ }
class B extends A{ }
@DocumentB
class C{ }
class D extends C{ }
//测试
public class DocumentDemo {
public static void main(String... args){
A instanceA=new B();
System.out.println("已使用的@Inherited注解:"+Arrays.toString(instanceA.getClass().getAnnotations()));
C instanceC = new D();
System.out.println("没有使用的@Inherited注解:"+Arrays.toString(instanceC.getClass().getAnnotations()));
}
/**
* 运行结果:
已使用的@Inherited注解:[@com.zejian.annotationdemo.DocumentA()]
没有使用的@Inherited注解:[]
*/
}
@Repeatable
元注解@Repeatable是JDK1.8新加入的,它表示在同一个位置重复相同的注解。在没有该注解前,一般是无法在同一个类型上使用相同的注解的。例如,在JDK1.8之前,这样的代码是编译报错的:
@FilterPath("/web/update")
@FilterPath("/web/add")
public class A {}
如果我们想实现上面类似的功能,就需要将@FilterPath注解时定义一个数组元素接收多个值。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPath {
String [] value();
}
//使用
@FilterPath({"/update","/add"})
public class A { }
但在Java8新增了@Repeatable注解后就可以采用如下的方式定义并使用了
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
// 使用Java8新增@Repeatable原注解,参数指明接收的注解class
// 在反射的时候使用FilterPaths类进行获取数据
@Repeatable(FilterPaths.class)
public @interface FilterPath {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface FilterPaths {
FilterPath[] value();
}
//使用案例
@FilterPath("/web/update")
@FilterPath("/web/add")
@FilterPath("/web/delete")
class AA{}
内置注解
jdk内部自带一些注解,可以随时使用。
@Deprecated
表示废弃,在编译期会发出警告。
public class AnnoDemo {
@Deprecated
private static void sayHello(){
}
public static void main(String[] args){
AnnoDemo.sayHello();
}
}
@FunctionalInterface
函数式接口:一个具有一个方法的普通接口。
@Override
实现类要重写父类或者接口的方法
@SafeVarargs
参数安全类型注解,告诉开发者不要用参数做一些不安全的操作
@SuppressWarnings
阻止编译器发出告警,比如调用了使用了 @Deprecated 标记的方法编译器会发出警告,可以使用 @SuppressWarnings 压制警告。常见的值有:
关键词 | 用途 |
---|---|
all | to suppress all warnings |
boxing | to suppress warnings relative to boxing/unboxing operations |
cast | to suppress warnings relative to cast operations |
dep-ann | to suppress warnings relative to deprecated annotation |
deprecation | to suppress warnings relative to deprecation |
fallthrough | to suppress warnings relative to missing breaks in switch statements |
finally | to suppress warnings relative to finally block that don’t return |
hiding | to suppress warnings relative to locals that hide variable |
incomplete-switch | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | to suppress warnings relative to non-nls string literals |
null | to suppress warnings relative to null analysis |
rawtypes | to suppress warnings relative to un-specific types when using generics on class params |
restriction | to suppress warnings relative to usage of discouraged or forbidden references |
serial | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | to suppress warnings relative to incorrect static access |
synthetic-access | to suppress warnings relative to unoptimized access from inner classes |
unchecked | to suppress warnings relative to unchecked operations |
unqualified-field-access | to suppress warnings relative to field access unqualified |
unused | to suppress warnings relative to unused code |
自定义注解
首先编写一个简单注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Demo {}
然后在注解内添加属性,注解的属性是以无参方法的形式声明的。这样一来你就可以使用这个注解为其他类进行标注:
public @interface Demo {
String role();
}
@Demo(role = "t")
class A{}
针对属性的类型是有要求的,必须是以下的类型之一:
- 所有基本类型(int,float,boolean,byte,double,char,long,short)
- 引用类型:String,Class,enum,Annotation
- 上述类型的数组
public @interface Demo {
//声明枚举
Status status();
//布尔类型
boolean showSupport();
//String类型
String name();
//class类型
Class<?> testCase();
//注解嵌套
Reference reference();
//数组类型
long[] value();
}
@Demo(
status = Status.A ,
showSupport = true ,
name="吃饭",
testCase=T.class ,
reference=@Reference(next=true),
value= {1L,2L}
)
class A{}
编译器对元素的默认值是有要求的。首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。其次,对于非基本类型的元素,无论是在源代码中声明,还是在注解接口中定义默认值,都不能以null作为值,为了绕开这个限制,只能定义一些特殊的值,例如空字符串或负数,表示某个元素不存在。
我们可以在注解内的属性后面使用 default
关键字可以给属性设置默认值。这样一来,使用注解的时候就可以不需要指明其值。
public @interface Demo {
String role() default "B";
}
@Demo
class A{}
如果你的属性名称为value,那么在使用注解的时候,可以省略名称,变成这样:
public @interface Demo {
String value();
}
@Demo("t")
class A{}
3. 注解与反射
注解仅仅是用作标记,要想它真实发挥作用,就需要利用Java反射机制编写注解解析器,用作业务需求。
我们的一个类会有很多成分,这些成分都实现了AnnotatedElement接口,罗列一下会有:
- Class:类的Class对象定义
- Constructor:代表类的构造器定义
- Field:代表类的成员变量定义
- Method:代表类的方法定义
- Package:代表类的包定义
AnnotatedElement接口下有这几个方法:
<A extends Annotation> getAnnotation(Class<A> annotationClass)
: 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。Annotation[] getAnnotations()
: 返回此元素上存在的所有注解,包括从父类继承的boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
: 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false。Annotation[] getDeclaredAnnotations()
: 返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组
这些成分都可以利用AnnotatedElement接口获得到对应成分上的注解实例。
@Target({ElementType.TYPE,
ElementType.METHOD,
ElementType.FIELD,
ElementType.TYPE_USE,
ElementType.PARAMETER,
ElementType.CONSTRUCTOR})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface AnnotationTest {
String value() default "";
}
@AnnotationTest("类上的注解")
public class TestAnnotation<@AnnotationTest("类变量类型(泛型)上的注解") T0, T1> {
@AnnotationTest("成员变量上的注解")
private Map<@AnnotationTest("成员变量泛型上的注解") String, String> map;
@AnnotationTest("成员方法上的注解")
public void Test(@AnnotationTest("方法参数上的注解") String s, @AnnotationTest("方法参数上的注解2") String s2) {}
@AnnotationTest("构造函数上的注解")
public TestAnnotation() {}
public static void main(String[] args) throws NoSuchMethodException {
Class<TestAnnotation> clazz = TestAnnotation.class;
//获取类上的注解
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
//是否存在注解
if (clazz.isAnnotationPresent(AnnotationTest.class)) {
AnnotationTest annotation1 = clazz.getAnnotation(AnnotationTest.class);
System.out.println(annotation1.value());
}
//获取类的类型变量(泛型)的注解
TypeVariable<Class<TestAnnotation>>[] typeParameters = clazz.getTypeParameters();
for (TypeVariable typeVariable : typeParameters) {
System.out.println(typeVariable.getName());
Annotation[] annotations1 = typeVariable.getAnnotations();
for (Annotation annotation : annotations1) {
System.out.println(annotation);
}
}
//获取成员变量上的注解
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
for (Annotation annotation : field.getAnnotations()) {
System.out.println(annotation);
}
}
//获取成员变量泛型的注解
for (Field field : fields) {
AnnotatedParameterizedType annotatedType = (AnnotatedParameterizedType) field.getAnnotatedType();
AnnotatedType[] typeArguments = annotatedType.getAnnotatedActualTypeArguments();
for (AnnotatedType typeArgument : typeArguments) {
for (Annotation annotation : typeArgument.getAnnotations()) {
System.out.println(annotation);
}
}
}
//获取成员方法上的注解
Method test = clazz.getMethod("Test", String.class, String.class);
for (Annotation annotation : test.getAnnotations()) {
System.out.println(annotation);
}
//获取成员方法上的参数注解
for (Parameter parameter : test.getParameters()) {
for (Annotation annotation : parameter.getAnnotations()) {
System.out.println(annotation);
}
}
//获取构造函数上的注解
Constructor<TestAnnotation> constructor = clazz.getConstructor();
for (Annotation annotation : constructor.getAnnotations()) {
System.out.println(annotation);
}
}
}
运行输出:
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=类上的注解)
类上的注解
T0
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=类变量类型(泛型)上的注解)
T1
map
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=成员变量上的注解)
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=成员变量泛型上的注解)
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=成员方法上的注解)
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=方法参数上的注解)
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=方法参数上的注解2)
@com.zifang.util.zex.bust.chapter9.AnnotationTest(value=构造函数上的注解)