[TOC]
https://www.zhihu.com/question/24401191/answer/1724982163
概述
格式
1 2 3
| public @interface 注解名称{ 属性列表; }
|
格式有点奇怪,我们稍后再研究。
分类
大致分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解。
- 自定义注解就是我们自己写的注解,比如@UserLog
- JDK内置注解,比如@Override检验方法重写,@Deprecated标识方法过期等
- 第三方框架定义的注解比如SpringMVC的@Controller等
使用位置
实际开发中,注解常常出现在类、方法、成员变量、形参位置。当然还有其他位置,这里不提及。
作用
如果说注释是写给人看的,那么注解就是写给程序看的。它更像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据及少量附加信息。比如程序只要读到加了@Test的方法,就知道该方法是待测试方法,又比如@Before注解,程序看到这个注解,就知道该方法要放在@Test方法之前执行。有时我们还可以通过注解属性,为将来读取这个注解的程序提供必要的附加信息,比如@RequestMapping(“/user/info”)提供了Controller某个接口的URL路径。
级别
注解和类、接口、枚举是同一级别的。
注解的本质
@interface和interface从名字上看非常相似,我猜注解的本质是一个接口(当然,这是瞎猜)。
为了验证这个猜测,我们做个实验。先按上面的格式写一个注解(暂时不附加属性):
)
编译后得到字节码文件:
)
通过XJad工具反编译MyAnnotation.class:
)
我们发现,@interface变成了interface,而且自动继承了Annotation:
)
既然确实是个接口,那么我们自然可以在里面写方法
)
得到class文件后反编译
)
由于接口默认方法的修饰符就是public abstract,所以可以省略,直接写成:
1 2 3 4 5 6
|
public @interface MyAnnotation { String getValue(); }
|
虽说注解的本质是接口,但是仍然有很多怪异的地方,比如使用注解时,我们竟然可以给getValue()赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@MyAnnotation(getValue = "annotation on class") public class Demo {
@MyAnnotation(getValue = "annotation on field") public String name;
@MyAnnotation(getValue = "annotation on method") public void hello() {}
}
|
你见过给方法赋值的操作吗?(别闹了,你脑中想到的是给方法传参)。
虽然这里的getValue可能不是指getValue(),底层或许是getValue()返回的一个同名变量。但不管怎么说,还是太怪异了。所以在注解里,类似于String getValue()这种,被称为“属性”,而给属性赋值显然听起来好接受多了。
另外,我们还可以为属性指定默认值:
)
)
当没有赋值时,属性将使用默认值,比如上面的defaultMethod(),它的getValue就是“no description”。
基于以上差异,以后还是把注解单独归为一类,不要当成接口使用。
反射注解信息
上文已经说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。所以三者关系是:
)
要牢记,只要用到注解,必然有三角关系:
仅仅完成前两步,是没什么卵用的。就好比你写了一本武林秘籍却没人去学,那么这门武功还不如一把菜刀。
所以,接下来需要我们编写一个程序读取注解。读取注解的思路是:
)
反射获取注解信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public class AnnotationTest { public static void main(String[] args) throws Exception { Class<Demo> clazz = Demo.class; MyAnnotation annotationOnClass = clazz.getAnnotation(MyAnnotation.class); System.out.println(annotationOnClass.getValue());
Field name = clazz.getField("name"); MyAnnotation annotationOnField = name.getAnnotation(MyAnnotation.class); System.out.println(annotationOnField.getValue());
Method hello = clazz.getMethod("hello", (Class<?>[]) null); MyAnnotation annotationOnMethod = hello.getAnnotation(MyAnnotation.class); System.out.println(annotationOnMethod.getValue());
Method defaultMethod = clazz.getMethod("defaultMethod", (Class<?>[]) null); MyAnnotation annotationOnDefaultMethod = defaultMethod.getAnnotation(MyAnnotation.class); System.out.println(annotationOnDefaultMethod.getValue());
} }
|
Class、Method、Field对象都有个getAnnotation()方法,可以获取各自位置上的注解信息,但IDEA好像提示我们错误:
Annotation ‘MyAnnotation.class’ is not retained for reflective。
直译的话就是:注解MyAnnotation并没有为反射保留。
)
我不管,我要开搞了,哪轮得到你一个编译器在这瞎比比,直接run一下:
)
不听老人言,吃亏在眼前。
这是因为注解其实有所谓“保留策略”的说法。大家学习JSP时,应该学过和<%– –>的区别:前者可以在浏览器检查网页源代码时看到,而另一个在服务器端输出时就被抹去了。同样的,注解通过保留策略,控制自己可以保留到哪个阶段。保留策略也是通过注解实现,它属于元注解,也叫元数据。
基本Annotation
@Override
:限定重写父类方法,强制一个子类必须覆盖父类的的方法。
1 2 3
| @Target(value=METHOD) @Retention(value=SOURCE) public @interface Override
|
@Deprecated
:标记已过时,用于表示某个程序元素(类,方法等)已过时,当其他程序使用已过时的类,方法时,编译器将会给出警告。
1 2 3 4
| @Documented @Retention(value=RUNTIME) @Target(value={CONSTRUCTOR,ElementType,LOCAL_VARIABLE,METHOD,PACKAGE,PARAMETER,TYPE}) public @interface Deprecated
|
@SuppressWarnings
:抑制编译器警告,指示被该Annotation修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。
1 2 3
| @Target(value={TYPE,ElementType,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE}) @Retention(value=SOURCE) public @interface SuppressWarnings
|
@SafeVarargs
:Java7的“堆污染”警告
堆污染:当把一个不带泛型的对象赋给一个带泛型的的变量时,就会引发堆污染。
1 2 3 4
| @Documented @Retention(value=RUNTIME) @Target(value={CONSTRUCTOR,METHOD}) public @interface SafeVarargs
|
@FunctionalInterface
:函数式接口
1 2 3 4
| @Documented @Retention(value=RUNTIME) @Target(value=TYPE) public @interface FunctionalInterface
|
JDK的元Annotation
所谓元注解,就是加在注解上的注解。作为普通程序员,常用的就是:
- @Documented:用于制作文档,不是很重要,忽略便是
- @Target:加在注解上,限定该注解的使用位置。不写的话,好像默认各个位置都是可以的。
- @Retention(注解的保留策略)
- @Inherted(指定该注解将具有继承性)
所以如果需要限定注解的使用位置,可以在自定义的注解上使用@Target(普通注解上使用元注解)。我们本次默认即可,不特别限定。
)
)
注解的保留策略有三种:SOURCE/ClASS/RUNTIME
)
“保留策略”这个元注解的意义在哪呢?
一般来说,普通开发者使用注解的时机都是运行时,比如反射读取注解(也有类似Lombok这类编译期注解)。既然反射是运行时调用,那就要求注解的信息必须保留到虚拟机将.class文件加载到内存为止。如果你需要反射读取注解,却把保留策略设置为RetentionPolicy.SOURCE、RetentionPolicy.CLASS,那就读取不到了。
现在,我们已经完成了使用注解的三部曲:
定义注解
1 2 3 4 5 6 7
|
@Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String getValue() default "no description"; }
|
使用注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@MyAnnotation(getValue = "annotation on class") public class Demo {
@MyAnnotation(getValue = "annotation on field") public String name;
@MyAnnotation(getValue = "annotation on method") public void hello() {}
@MyAnnotation() public void defaultMethod() {} }
|
重新运行程序,成功读取注解信息:
)
注意,defaultMethod()反射得到的注解信息是:no description,就是MyAnnotion中getValue的默认值。
但是,注解的读取并不只有反射一种途径。比如@Override,它由编译器读取(你写完代码ctrl+s时就编译了),而编译器只是检查语法错误,此时程序尚未运行。
)
保留策略为SOURCE,仅仅是源码阶段,编译成.class文件后就消失
)
属性的数据类型及特别的属性:value和数组
属性的数据类型
- 八种基本数据类型
- String
- 枚举
- Class
- 注解类型
- 以上类型的一维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { int intValue(); long longValue();
String name(); CityEnum cityName(); Class<?> clazz(); MyAnnotation2 annotation2();
int[] intValueArray(); String[] names(); }
@interface MyAnnotation2 { }
enum CityEnum { BEIJING, HANGZHOU, SHANGHAI; }
|
)
以Demo类上注解为例,演示给注解属性赋值多种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@MyAnnotation( intValue = 1, longValue = 0L,
name = "annotation on class", cityName = CityEnum.BEIJING, clazz = Demo.class, // 注解 annotation2 = @MyAnnotation2, intValueArray = {1, 2}, names = {"Are", "you", "OK?"} ) public class Demo { }
|
)
value属性
如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值:
)
)
)
但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系:
)
)
按IDEA的提示修正:
)
数组属性
如果数组的元素只有一个,可以省略花括号{}:
)
用常量类为注解属性赋值
如果你希望为注解的属性提供统一的几个可选值,可以使用常量类:
)
)
或者:
)
)
本质其实还是String,只不过是通过常量表现。上面只是举个例子,大家可以根据实际业务自由发挥。
山寨版Junit
还是三步曲:
)
MyBefore注解(定义注解)
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyBefore { }
|
MyTest注解(定义注解)
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyTest { }
|
MyAfter注解(定义注解)
1 2 3 4
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAfter { }
|
EmployeeDAOTest(使用注解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public class EmployeeDAOTest { @MyBefore public void init() { System.out.println("初始化..."); }
@MyAfter public void destroy() { System.out.println("销毁..."); }
@MyTest public void testSave() { System.out.println("save..."); }
@MyTest public void testDelete() { System.out.println("delete..."); } }
|
MyJunitFrameWork(读取注解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
public class MyJunitFrameWork { public static void main(String[] args) throws Exception { Class clazz = EmployeeDAOTest.class; Object obj = clazz.newInstance();
Method[] methods = clazz.getMethods();
List<Method> myBeforeList = new ArrayList<>(); List<Method> myAfterList = new ArrayList<>(); List<Method> myTestList = new ArrayList<>(); for (Method method : methods) { if (method.isAnnotationPresent(MyBefore.class)) { myBeforeList.add(method); } else if (method.isAnnotationPresent(MyTest.class)) { myTestList.add(method); } else if (method.isAnnotationPresent(MyAfter.class)) { myAfterList.add(method); } }
for (Method testMethod : myTestList) { for (Method beforeMethod : myBeforeList) { beforeMethod.invoke(obj); } testMethod.invoke(obj); for (Method afterMethod : myAfterList) { afterMethod.invoke(obj); } } } }
|
效果展示:
)
小结
- 注解就像标签,是程序判断执行的依据。比如,程序读到@Test就知道这个方法是待测试方法,而@Before的方法要在测试方法之前执行
- 注解需要三要素:定义、使用、读取并执行
- 注解分为自定义注解、JDK内置注解和第三方注解(框架)。自定义注解一般要我们自己定义、使用、并写程序读取,而JDK内置注解和第三方注解我们只要使用,定义和读取都交给它们
- 大多数情况下,三角关系中我们只负责使用注解,无需定义和执行,框架会将注解类和读取注解的程序隐藏起来,除非阅读源码,否则根本看不到。平时见不到定义和读取的过程,光顾着使用注解,久而久之很多人就忘了注解如何起作用了!
)