对于注解相信大家都不陌生,因为初学者第一个注解就是@Override,用于标识重载方法。在Java EE开发过程中,注解更是无处不在,像经典的MVC设计模式就至少使用到了4个注解:@Component、@Repository、@Service和@Controller。现在问题来了,为什么要学习注解?它有什么优点,能解决什么问题?通过阅读本篇文章相信读者会有一个比较清晰的认识。
一个经常会遇到的例子
在Java Web开发中,最早是使用XML的配置方式。举个例子来说,当开发者在Servlet中定义了LoginServlet类,接下来应该是去web.xml配置文件中增加LoginServlet类的访问映射,可能会使用如下代码:
LoginServletcom.envy.servlet.LoginServletLoginServlet/LoginServlet
这样当用户访问诸如http://localhost:8080/LoginServlet链接时,就会执行这个类中定义的方法逻辑。但是针对这种配置方式,有人觉得这种太麻烦了,于是提出了一种新的配置方式:在LoginServlet类上添加@WebServlet注解,并结合value="/LoginServlet"这种属性值的方式也实现了同样的目的,毫无疑问后面这种方式使用起来更加方便,而这种方式就是今天的主角---注解。
注解
什么是注解?
Java注解(Annotation)自Java1.5引入,用于描述Java代码的元信息,通常情况下注解不会直接影响代码的执行,但某些注解可以用来影响代码的执行。初学者可能被这句话给搞晕了,什么是元信息,一会儿不会影响代码的执行,一会儿又可以影响代码的执行。其实不用着急,相信通过本文的学习,你一定会对这句话有新的理解。
其实注解就像是代码中的特殊标记,这些特殊标记可以在类编译、加载、运行时被读取,并执行对应的逻辑。
注解的定义
注解虽然不像class和interface那样,自一开始就出现在大家的眼前,但是自从它诞生以来,它的地位却在一直提升。注解也是一种类型,因此它和class、interface一样值得被人记住。
定义注解非常简单,只需使用@interface关键词声明即可,比如下面就定义了一个属于开发者自己的注解@MyAnnotation:
public @interface MyAnnotation {}
是不是觉得和接口非常相似,仅仅在前面添加了一个@符号而已。那么这个注解如何使用呢?先定义一个Hello类,然后将自定义的@MyAnnotation添加到这个Hello类上面这样就完成了一个最简单的注解使用案例,里面的代码为:
@MyAnnotationpublic class Hello {}
由于自定义的@MyAnnotation内不包含任何成员变量,因此该注解被称为标记注解,最常见的标记注解就是@Overried。但是实际上注解内往往是需要定义成员变量的,因此需要学习另一个概念:元注解。元注解是对注解进行注解的注解,用于解释说明被作用的注解的作用,因此它是一种非常基本的注解。
在JDK的java.lang.annotation包下定义了6个元注解:@Documented、@Inherited、@Native、@Repeatable、@Retention和@Target,如下图所示:
后续会依次解释一下这6个元注解的作用,了解和熟悉它们对于提升编程水平有极大的帮助。
元数据
不过在此之前先学习一下带有成员变量的注解,通常称之为元数据。在注解中定义成员变量,它的语法非常类似于方法的声明:
public @interface MyAnnotation { String username(); String password();}
这样就在之前自定义的MyAnnotation注解内添加了两个成员变量,username和password。请注意在注解上定义的成员变量只能是String、数组、Class、枚举类和注解。
在注解中定义成员变量其实是为了携带信息,通常在XML中可能一个标签中包含子标签:
余思作者> www.envyzhan.club网址>博客>
子标签就是一些信息,而这里在注解中定义的成员变量其实就相当于子标签。
不过由于前面的自定义注解@MyAnnotation中定义的成员变量没有默认值,因此在使用的时候需要添加默认值:
@MyAnnotation(username = "envy",password = "1234") public void test(){ }
通常情况下会在声明一个注解的时候,给予它的成员变量一个默认值,这样便于后续使用:
public @interface MyAnnotation { String username() default "envy"; String password() default "1234";}
这样在使用的时候可以根据实际情况来选择是否修改注解的默认值:
@MyAnnotation()public void test(){}
注意这里面存在一个特殊情况:当一个注解内只存在一个成员属性,且属性值为value,那么开发者可以在声明该注解的时候不给予它默认值,且在使用的时候不需要指出value属性,而是直接赋值即可。举一个例子来说,现在有一个注解@MyAnnotation,它的内部结构为:
public @interface MyAnnotation { String value();}
那么使用的时候可以采取如下的方式进行:
@MyAnnotation("envy") public void test(){ }
注意这里的写法是@MyAnnotation("envy"),当然也可以采用@MyAnnotation(value="envy")的通用写法,但是不建议这么做,因为这本身就是一个语法糖而已。那有人觉得是不是只要一个注解内只要满足只要一个成员属性这一条件就可以呢?往下看,这里定义了一个@OneAnnotation,它的内部结构为:
public @interface OneAnnotation { String name();}
然后使用的时候也按照语法糖来写,那是会报错的,只要属性值为value才可以,这一点需要引起特别注意:
内置7个元注解
@Documented注解
首先查看一下@Documented注解的源码信息,如下所示:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Documented {}
可以看到@Documented注解自jdk1.5引入,顾名思义@Documented注解是一个用于修饰的注解,被此注解修饰的注解可以被javadoc等工具文档化,注意它只负责标记,没有成员取值。通常javadoc中不包括注解,但是当某个注解被@Documented修饰时,那么它会被 javadoc工具处理,之后该注解类型信息会存在于生成的文档中。
举一个例子,前面自定义了一个注解@MyAnnotation,现在尝试在该注解上添加@Documented注解,
@Documentedpublic @interface MyAnnotation {}
然后新建一个Hello.java文件,在该类上使用该注解:
@MyAnnotationpublic class Hello {}
接着进入dos命令行,切换到该注解所在的文件夹,然后执行javadoc -d doc *.java,该命令的作用是使用javadoc工具生成一个对该目录下所有java文件的说明文档。运行命令后可以发现当前目录下多了一个doc目录,进入该目录,然后使用浏览器打开其中的index.html文件:
可以发现这个文档中出现了对注释类型的说明(注解其实是一种类型,成为注释类型,但是通常大家都称之为注解)
现在尝试删除这个doc目录,且去掉MyAnnotation注释上的@Documented注解,再来运行一下上述命令,可以发现此时生成的index.html文件中就没有对注释类型的说明了:
这就是@Documented注解的使用方法,继续下一个注解。
@Native 注解
首先查看一下@Native注解的源码信息,如下所示:
@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.SOURCE)public @interface Native {}
可以看到@Native注解自jdk1.8引入,用于注释该字段是一个常量,其值引用native code,可以发现它的保留时间为SOURCE阶段,这个用的不是很多。
@Target 注解
首先查看一下@Target注解的源码信息,如下所示:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Target { ElementType[] value();}
可以看到@Target注解自jdk1.5引入,用于指明被修饰注解的使用范围,即用来描述被修饰的注解可以在哪里使用。定义在ElementType枚举类中,一共有10种策略(1.8之前存在前8种,jdk1.8新增后2种):
枚举常量注解使用范围TYPE类,接口(包括注解类型),和枚举FIELD字段(包括枚举常量)METHOD方法PARAMETER形式参数CONSTRUCTOR构造函数LOCAL_VARIABLE局部变量ANNOTATION_TYPE注解类型PACKAGE包TYPE_PARAMETER (jdk1.8引入)类型参数TYPE_USE (jdk1.8引入)使用类型
请注意这里的PACKAGE,不是使用在一般类中,而是用在固定的package-info.java文件中,注意名称必须是package-info不能修改。举个例子来说,新建一个WeAnnotation注解,其中的代码为:
@Target({ElementType.PACKAGE,ElementType.METHOD})@Documentedpublic @interface WeAnnotation {}
接着再创建一个package-info.java文件,注意必须使用类似于文件创建的方式,否则IDEA不允许创建:
@WeAnnotationpackage com.envy.annotation;
这样就成功的使用了该注解。请注意在@Target注解内定义的成员变量是一个数组,因此必须使用诸如@Target({ElementType.PACKAGE})形式:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Target { ElementType[] value();}
@Retention 注解
首先查看一下@Retention注解的源码信息,如下所示:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Retention { RetentionPolicy value();}
可以看到@Retention注解自jdk1.5引入,用于指明被修饰注解的保留时间。定义在RetentionPolicy枚举类中,一共有三种策略:SOURCE、CLASS和RUNTIME。
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value();}
SOURCE,顾名思义就是源码阶段,该注解只存在于.java源文件中,当.Java文件编译成.class字节码文件的时候,该注解即被遗弃,被编译器忽略。CLASS,顾名思义就是字节码阶段,该注解保留到.class字节码文件中,当.class字节码文件被jvm加载时即被遗弃,这是默认的生命周期。RUNTIME,注解不仅被保存到.class字节码文件中,且被jvm加载后,仍然存在,因此这个时候就有一种非常厉害的技术--反射。
其实这三个生命周期分别对应于三个阶段:.java源文件---> .class字节码文件 ---> 内存中的字节码。也就是说生命周期长度顺序为SOURCE< CLASS< RUNTIME,因此前者能作用的地方后者一定也能作用。
一般的,如果开发者想要做一些检查性的操作,如是否方法重载,禁止编译器警告等,可以选择SOURCE生命周期。如果想要在编译时进行一些预处理操作,如生成一些辅助代码等,可以选择CLASS生命周期。如果想要在运行时去动态获取注解信息,那此时必须选择RUNTIME生命周期。
反射技术demo演示@Retention和@Target注解使用。前面提到的反射技术就是在运行时动态获取类的相关信息,下面就结合反射来介绍如何使用@Retention和@Target注解,便于demo演示,这里仅仅获取类的常用信息,如Class、Field和Method 。
第一步,新建3个自定义注解。新建myannotation包,并在其中依次创建3个自定义运行时注解,且作用范围依次为Class、Field和Method:
//ClassAnno.java@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface ClassAnno { String value();}//MethodAnno.java@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface MethodAnno { String name() default "envy"; String data(); int age() default 22;}//FieldAnno.java@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface FieldAnno { int[] value();}
第二步,新建测试类RunTimeTest。新建RunTimeTest测试类,其中的代码为:
@ClassAnno("RunTime")public class RunTimeTest { @FieldAnno(value = {66,88}) public String word = "hello"; @FieldAnno(value = {1024}) public int number = 2018; @MethodAnno(data = "good",name="envy", age = 22) public static void sayHello(){ System.out.println("this is sayHello method."); }}
第三步,获取注解内部信息。新建GetRunTimeTest类,用于运行时获取注解内部信息,相应的代码为:
public class GetRunTimeTest { public static void outInfo(){ StringBuilder builder = new StringBuilder(); Class> aClass = RunTimeTest.class; /**获取ClassAnno注解信息*/ builder.append("ClassAnno注解:").append(""); ClassAnno classAnno = aClass.getAnnotation(ClassAnno.class); if(classAnno!=null){ builder.append(Modifier.toString(aClass.getModifiers())).append(" ") .append(aClass.getSimpleName()).append(""); builder.append("注解值为:").append(classAnno.value()).append(""); } /**获取MethodAnno注解信息*/ builder.append("MethodAnno注解:").append(""); Method[] methods = aClass.getDeclaredMethods(); for(Method method:methods){ MethodAnno methodAnno = method.getAnnotation(MethodAnno.class); if(methodAnno !=null){ builder.append(Modifier.toString(method.getModifiers())).append(" ") .append(method.getReturnType().getSimpleName()).append(" ") .append(method.getName()).append(""); builder.append("注解值为:").append(""); builder.append("name--->").append(methodAnno.name()).append(""); builder.append("data--->").append(methodAnno.data()).append(""); builder.append("age--->").append(methodAnno.age()).append(""); } } /**获取FieldAnno注解信息*/ builder.append("FieldAnno注解:").append(""); Field[] fields = aClass.getDeclaredFields(); for(Field field:fields){ FieldAnno fieldAnno = field.getAnnotation(FieldAnno.class); if(fieldAnno != null){ builder.append(Modifier.toString(field.getModifiers())).append(" ") .append(field.getType().getSimpleName()).append(" ") .append(field.getName()).append(""); builder.append("注解值为:").append(Arrays.toString(fieldAnno.value())).append(""); } } System.out.println(builder.toString()); } public static void main(String[] args){ outInfo(); }}
运行该方法后,结果如下:
ClassAnno注解:public RunTimeTest注解值为:RunTimeMethodAnno注解:public static void sayHello注解值为:name--->envydata--->goodage--->22FieldAnno注解:public String word注解值为:[66, 88]public int number注解值为:[1024]
可以发现使用反射技术,成功的在程序运行时动态地获取到了注解中的信息,其实在Spring框架内的@Autowried注解就是起这个作用,在类运行时将需要的Bean注入Spring容器,以供后续程序的调用。
@Inherited 注解
首先查看一下@Inherited注解的源码信息,如下所示:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Inherited {}
可以看到@Inherited注解自jdk1.5引入,顾名思义@Inherited注解允许子类继承父类的注解。举个例子来说,假设你使用@Inherited注解修饰了自定义的@HeyAnnotation注解,如果此时自定义的@HeyAnnotation注解修饰一个类Book后,而又一个TechBook类又继承了Book类,那么此时的TechBook类则默认也拥有你自定义的@HeyAnnotation注解。
HeyAnnotation.java注解中的代码:
@Inherited@Retention(RetentionPolicy.RUNTIME)public @interface HeyAnnotation {}
请注意后续是通过反射来判断子类中是否存在@HeyAnnotation注解,而注解默认的保留时间为CLASS,这使得该注解无法进入RUNTIME时期,因此必须加上@Retention(RetentionPolicy.RUNTIME)。接下来是Book.java文件中的代码:
@HeyAnnotationpublic class Book {}
然后是Book子类TechBook类中的代码:
public class TechBook extends Book { public static void main(String[] args){ System.out.println(TechBook.class.isAnnotationPresent(HeyAnnotation.class)); }}
然后运行该main方法,可以发现输出结果为true。
@Repeatable 注解
首先查看一下@Repeatable注解的源码信息,如下所示:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Repeatable { Class extends Annotation> value();}
可以看到@Repeatable注解自jdk1.8引入,Repeatable是重复的意思,因此被该注解修饰的注解可以多次应用于相同的声明或类型中。那么什么样的注解可以被多次使用呢?当然是注解的值可以同时取多个了。
举个例子来说,一个男人可能是父亲,可能是儿子,也有可能是丈夫。首先定义一个@Persons注解:
public @interface Persons { Person[] value();}
注意这里面的成员变量Person也是注解,该注解中的代码为:
@Repeatable(Persons.class)public @interface Person { String role() default "";}
可以看到在该注解上使用了@Repeatable(Persons.class)注解,这个其实就相当于一个容器注解,容器注解顾名思义就是存放其他注解的注解,注意容器注解也是一个注解。然后定一个Male类来使用这个@Person注解,相应的代码为:
@Person(role = "husband")@Person(role = "son")@Person(role = "father")public class Male {}
回过头再来看一下@Persons注解内的代码:
public @interface Persons { Person[] value();}
可以看到它的成员变量是一个被@Repeatable(Persons.class)注解修饰的注解,注意它是一个数组。
接下来通过一张图片来总结一下这6个元注解: