作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
注解,和反射一样,是Java中最重要却最容易被人遗忘的知识点。哪怕Spring、SpringMVC、SpringBoot等框架中充满了注解,我们还是选择性地忽视它。很多人不明白它是怎么起作用的,甚至有人把它和注释混淆...工作中也只是机械性地在Controller上加@RequestMapping。是的,我们太习以为常了,以至于觉得它理所应当就是如此。
我刚入行不久后遇到的两件事让我对注解有了新的认识。
第一件事是,同年6月我去了趟北京,参与开发了某中国五百强企业的一个加密系统,第一次接触到了SpringBoot。当我发现一个demo项目只要简单地写个启动类并加上@SpringBootApplication就可以直接访问Controller时,感到非常震撼,我裂开了。
整个demo没有一个配置文件,连web.xml也没有。
由于开发进度很赶,当时没时间去研究它是如何做到的,但这件事让我意识到自己对注解还是了解得太少。
第二件事是,年底跳槽回到杭州后我又参与开发了一个金融借贷系统,那阵子对接了很多第三方的风控接口:
对签名验签不了解的朋友,可以百度一下。总之,每对接一个接口,都要在开头进行数据校验。一两个接口也就算了,但每次对接风控,基本上都要写10+多个方法。每个方法开头都写一份签名验签的代码,显然太冗余了。我当时的做法是将验签代码抽取成方法,方便复用,自以为算是一种改良了,直到我看到同事用了切面...40米的大刀拦腰砍去,给每个方法都做了签名验签:
注意,实际上切面的作用是在方法前后,而不是方法内部的前后。上面这样画,仅仅为了更形象。
这两件事,让我知道,是时候重新学习一下注解了。
public @interface 注解名称{
属性列表;
}
格式有点奇怪,我们稍后再研究。
大致分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解。
实际开发中,注解常常出现在类、方法、成员变量、形参位置。当然还有其他位置,这里不提及。
如果说注释是写给人看的,那么注解就是写给程序看的。它更像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据及少量附加信息。比如程序只要读到加了@Test的方法,就知道该方法是待测试方法,又比如@Before注解,程序看到这个注解,就知道该方法要放在@Test方法之前执行。有时我们还可以通过注解属性,为将来读取这个注解的程序提供必要的附加信息,比如@RequestMapping("/user/info")提供了Controller某个接口的URL路径。
注解和类、接口、枚举是同一级别的。
@interface和interface从名字上看非常相似,我猜注解的本质是一个接口(当然,这是瞎猜)。
为了验证这个猜测,我们做个实验。先按上面的格式写一个注解(暂时不附加属性):
编译后得到字节码文件:
通过XJad工具反编译MyAnnotation.class:
我们发现,@interface变成了interface,而且自动继承了Annotation:
既然确实是个接口,那么我们自然可以在里面写方法
得到class文件后反编译
由于接口默认方法的修饰符就是public abstract,所以可以省略,直接写成:
/**
* @author mx
*/
public @interface MyAnnotation {
String getValue();
}
虽说注解的本质是接口,但是仍然有很多怪异的地方,比如使用注解时,我们竟然可以给getValue()赋值:
/**
* @author mx
*/
@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"。
基于以上差异,以后还是把注解单独归为一类,不要当成接口使用。
上文已经说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。所以三者关系是:
要牢记,只要用到注解,必然有三角关系:
仅仅完成前两步,是没什么卵用的。就好比你写了一本武林秘籍却没人去学,那么这门武功还不如一把菜刀。
所以,接下来需要我们编写一个程序读取注解。读取注解的思路是:
反射获取注解信息:
/**
* @author mx
*/
public class AnnotationTest {
public static void main(String[] args) throws Exception {
// 获取类上的注解
Class 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());
// 获取hello方法上的注解
Method hello = clazz.getMethod("hello", (Class>[]) null);
MyAnnotation annotationOnMethod = hello.getAnnotation(MyAnnotation.class);
System.out.println(annotationOnMethod.getValue());
// 获取defaultMethod方法上的注解
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时,应该学过和<%-- -->的区别:前者可以在浏览器检查网页源代码时看到,而另一个在服务器端输出时就被抹去了。同样的,注解通过保留策略,控制自己可以保留到哪个阶段。保留策略也是通过注解实现,它属于元注解,也叫元数据。
所谓元注解,就是加在注解上的注解。作为普通程序员,常用的就是:
所以如果需要限定注解的使用位置,可以在自定义的注解上使用@Target(普通注解上使用元注解)。我们本次默认即可,不特别限定。
注解的保留策略有三种:SOURCE/ClASS/RUNTIME
“保留策略”这个元注解的意义在哪呢?
一般来说,普通开发者使用注解的时机都是运行时,比如反射读取注解(也有类似Lombok这类编译期注解)。既然反射是运行时调用,那就要求注解的信息必须保留到虚拟机将.class文件加载到内存为止。如果你需要反射读取注解,却把保留策略设置为RetentionPolicy.SOURCE、RetentionPolicy.CLASS,那就读取不到了。
现在,我们已经完成了使用注解的三部曲:
定义注解
/**
* @author mx
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String getValue() default "no description";
}
使用注解
/**
* @author mx
*/
@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() // 故意不指定getValue
public void defaultMethod() {}
}
读取注解信息
注意,defaultMethod()反射得到的注解信息是:no description,就是MyAnnotion中getValue的默认值。
但是,注解的读取并不只有反射一种途径。比如@Override,它由编译器读取(你写完代码ctrl+s时就编译了),而编译器只是检查语法错误,此时程序尚未运行。
保留策略为SOURCE,仅仅是源码阶段,编译成.class文件后就消失
属性的数据类型
/**
* @author mx
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
// 8种基本数据类型
int intValue();
long longValue();
// ...其他类型省略
// String
String name();
// 枚举
CityEnum cityName();
// Class类型
Class> clazz();
// 注解类型
MyAnnotation2 annotation2();
// 以上几种类型的数组类型
int[] intValueArray();
String[] names();
// ...其他类型省略
}
@interface MyAnnotation2 {
}
enum CityEnum {
BEIJING,
HANGZHOU,
SHANGHAI;
}
以Demo类上注解为例,演示给注解属性赋值多种类型:
/**
* @author mx
*/
@MyAnnotation(
// 8种基本类型
intValue = 1,
longValue = 0L,
// String
name = "annotation on class",
// 枚举
cityName = CityEnum.BEIJING,
// Class
clazz = Demo.class,
// 注解
annotation2 = @MyAnnotation2,
// 一维数组
intValueArray = {1, 2},
names = {"Are", "you", "OK?"}
)
public class Demo {
// 省略...
}
如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值:
但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系:
按IDEA的提示修正:
如果数组的元素只有一个,可以省略花括号{}:
如果你希望为注解的属性提供统一的几个可选值,可以使用常量类:
或者:
本质其实还是String,只不过是通过常量表现。上面只是举个例子,大家可以根据实际业务自由发挥。
关于注解的使用案例,请参考下篇。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬