这就是java中的注解
对于注解相信大家都不陌生,因为初学者第一个注解就是@Override
,用于标识重载方法。在Java EE开发过程中,注解更是无处不在,像经典的MVC设计模式就至少使用到了4个注解:@Component
、@Repository
、@Service
和@Controller
。现在问题来了,为什么要学习注解?它有什么优点,能解决什么问题?通过阅读本篇文章相信读者会有一个比较清晰的认识。
一个经常会遇到的例子
在Java Web开发中,最早是使用XML的配置方式。举个例子来说,当开发者在Servlet中定义了LoginServlet类,接下来应该是去web.xml配置文件中增加LoginServlet类的访问映射,可能会使用如下代码:
1 | <servlet> |
这样当用户访问诸如http://localhost:8080/LoginServlet
链接时,就会执行这个类中定义的方法逻辑。但是针对这种配置方式,有人觉得这种太麻烦了,于是提出了一种新的配置方式:在LoginServlet类上添加@WebServlet
注解,并结合value="/LoginServlet"
这种属性值的方式也实现了同样的目的,毫无疑问后面这种方式使用起来更加方便,而这种方式就是今天的主角—注解。
注解
什么是注解?
Java注解(Annotation)自Java1.5引入,用于描述Java代码的元信息,通常情况下注解不会直接影响代码的执行,但某些注解可以用来影响代码的执行。初学者可能被这句话给搞晕了,什么是元信息,一会儿不会影响代码的执行,一会儿又可以影响代码的执行。其实不用着急,相信通过本文的学习,你一定会对这句话有新的理解。
其实注解就像是代码中的特殊标记,这些特殊标记可以在类编译、加载、运行时被读取,并执行对应的逻辑。
注解的定义
注解虽然不像class和interface那样,自一开始就出现在大家的眼前,但是自从它诞生以来,它的地位却在一直提升。注解也是一种类型,因此它和class、interface一样值得被人记住。
定义注解非常简单,只需使用@interface
关键词声明即可,比如下面就定义了一个属于开发者自己的注解@MyAnnotation
:
1 | public @interface MyAnnotation { |
是不是觉得和接口非常相似,仅仅在前面添加了一个@
符号而已。那么这个注解如何使用呢?先定义一个Hello类,然后将自定义的@MyAnnotation
添加到这个Hello类上面这样就完成了一个最简单的注解使用案例,里面的代码为:
1 | @MyAnnotation |
由于自定义的@MyAnnotation
内不包含任何成员变量,因此该注解被称为标记注解,最常见的标记注解就是@Overried
。但是实际上注解内往往是需要定义成员变量的,因此需要学习另一个概念:元注解。元注解是对注解进行注解的注解,用于解释说明被作用的注解的作用,因此它是一种非常基本的注解。
在JDK的java.lang.annotation
包下定义了6个元注解:@Documented
、@Inherited
、@Native
、@Repeatable
、@Retention
和@Target
,如下图所示:
后续会依次解释一下这6个元注解的作用,了解和熟悉它们对于提升编程水平有极大的帮助。
元数据
不过在此之前先学习一下带有成员变量的注解,通常称之为元数据。在注解中定义成员变量,它的语法非常类似于方法的声明:
1 | public @interface MyAnnotation { |
这样就在之前自定义的MyAnnotation注解内添加了两个成员变量,username和password。请注意在注解上定义的成员变量只能是String、数组、Class、枚举类和注解。
在注解中定义成员变量其实是为了携带信息,通常在XML中可能一个标签中包含子标签:
1 | <博客> |
子标签就是一些信息,而这里在注解中定义的成员变量其实就相当于子标签。
不过由于前面的自定义注解@MyAnnotation
中定义的成员变量没有默认值,因此在使用的时候需要添加默认值:
1 | @MyAnnotation(username = "envy",password = "1234") |
通常情况下会在声明一个注解的时候,给予它的成员变量一个默认值,这样便于后续使用:
1 | public @interface MyAnnotation { |
这样在使用的时候可以根据实际情况来选择是否修改注解的默认值:
1 | @MyAnnotation() |
注意这里面存在一个特殊情况:当一个注解内只存在一个成员属性,且属性值为value,那么开发者可以在声明该注解的时候不给予它默认值,且在使用的时候不需要指出value属性,而是直接赋值即可。举一个例子来说,现在有一个注解@MyAnnotation
,它的内部结构为:
1 | public @interface MyAnnotation { |
那么使用的时候可以采取如下的方式进行:
1 | @MyAnnotation("envy") |
注意这里的写法是@MyAnnotation("envy")
,当然也可以采用@MyAnnotation(value="envy")
的通用写法,但是不建议这么做,因为这本身就是一个语法糖而已。那有人觉得是不是只要一个注解内只要满足只要一个成员属性这一条件就可以呢?往下看,这里定义了一个@OneAnnotation
,它的内部结构为:
1 | public @interface OneAnnotation { |
然后使用的时候也按照语法糖来写,那是会报错的,只要属性值为value才可以,这一点需要引起特别注意:
内置7个元注解
@Documented注解
首先查看一下@Documented
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Documented
注解自jdk1.5引入,顾名思义@Documented
注解是一个用于修饰的注解,被此注解修饰的注解可以被javadoc等工具文档化,注意它只负责标记,没有成员取值。通常javadoc中不包括注解,但是当某个注解被@Documented
修饰时,那么它会被 javadoc工具处理,之后该注解类型信息会存在于生成的文档中。
举一个例子,前面自定义了一个注解@MyAnnotation
,现在尝试在该注解上添加@Documented注解,
1 | @Documented |
然后新建一个Hello.java文件,在该类上使用该注解:
1 | @MyAnnotation |
接着进入dos命令行,切换到该注解所在的文件夹,然后执行javadoc -d doc *.java
,该命令的作用是使用javadoc工具生成一个对该目录下所有java文件的说明文档。运行命令后可以发现当前目录下多了一个doc目录,进入该目录,然后使用浏览器打开其中的index.html
文件:
可以发现这个文档中出现了对注释类型的说明(注解其实是一种类型,成为注释类型,但是通常大家都称之为注解)
现在尝试删除这个doc目录,且去掉MyAnnotation注释上的@Documented
注解,再来运行一下上述命令,可以发现此时生成的index.html
文件中就没有对注释类型的说明了:
这就是@Documented
注解的使用方法,继续下一个注解。
@Native 注解
首先查看一下@Native
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Native
注解自jdk1.8引入,用于注释该字段是一个常量,其值引用native code
,可以发现它的保留时间为SOURCE阶段,这个用的不是很多。
@Target 注解
首先查看一下@Target
注解的源码信息,如下所示:
1 | @Documented |
可以看到@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
注解,其中的代码为:
1 | @Target({ElementType.PACKAGE,ElementType.METHOD}) |
接着再创建一个package-info.java
文件,注意必须使用类似于文件创建的方式,否则IDEA不允许创建:
1 | @WeAnnotation |
这样就成功的使用了该注解。请注意在@Target
注解内定义的成员变量是一个数组,因此必须使用诸如@Target({ElementType.PACKAGE})
形式:
1 | @Documented |
@Retention 注解
首先查看一下@Retention
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Retention
注解自jdk1.5引入,用于指明被修饰注解的保留时间。定义在RetentionPolicy
枚举类中,一共有三种策略:SOURCE、CLASS和RUNTIME。
1 | @Documented |
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:
1 | //ClassAnno.java |
第二步,新建测试类RunTimeTest。新建RunTimeTest
测试类,其中的代码为:
1 | @ClassAnno("RunTime") |
第三步,获取注解内部信息。新建GetRunTimeTest类,用于运行时获取注解内部信息,相应的代码为:
1 | public class GetRunTimeTest { |
运行该方法后,结果如下:
1 | ClassAnno注解: |
可以发现使用反射技术,成功的在程序运行时动态地获取到了注解中的信息,其实在Spring框架内的@Autowried
注解就是起这个作用,在类运行时将需要的Bean注入Spring容器,以供后续程序的调用。
@Inherited 注解
首先查看一下@Inherited
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Inherited
注解自jdk1.5引入,顾名思义@Inherited
注解允许子类继承父类的注解。举个例子来说,假设你使用@Inherited
注解修饰了自定义的@HeyAnnotation
注解,如果此时自定义的@HeyAnnotation
注解修饰一个类Book后,而又一个TechBook类又继承了Book类,那么此时的TechBook类则默认也拥有你自定义的@HeyAnnotation
注解。
HeyAnnotation.java注解中的代码:
1 | @Inherited |
请注意后续是通过反射来判断子类中是否存在@HeyAnnotation
注解,而注解默认的保留时间为CLASS,这使得该注解无法进入RUNTIME时期,因此必须加上@Retention(RetentionPolicy.RUNTIME)
。接下来是Book.java文件中的代码:
1 | @HeyAnnotation |
然后是Book子类TechBook类中的代码:
1 | public class TechBook extends Book { |
然后运行该main方法,可以发现输出结果为true。
@Repeatable 注解
首先查看一下@Repeatable
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Repeatable
注解自jdk1.8引入,Repeatable是重复的意思,因此被该注解修饰的注解可以多次应用于相同的声明或类型中。那么什么样的注解可以被多次使用呢?当然是注解的值可以同时取多个了。
举个例子来说,一个男人可能是父亲,可能是儿子,也有可能是丈夫。首先定义一个@Persons
注解:
1 | public @interface Persons { |
注意这里面的成员变量Person也是注解,该注解中的代码为:
1 | @Repeatable(Persons.class) |
可以看到在该注解上使用了@Repeatable(Persons.class)
注解,这个其实就相当于一个容器注解,容器注解顾名思义就是存放其他注解的注解,注意容器注解也是一个注解。然后定一个Male类来使用这个@Person
注解,相应的代码为:
1 | @Person(role = "husband") |
回过头再来看一下@Persons
注解内的代码:
1 | public @interface Persons { |
可以看到它的成员变量是一个被@Repeatable(Persons.class)
注解修饰的注解,注意它是一个数组。
接下来通过一张图片来总结一下这6个元注解:
基本注解
在介绍完了元注解后,接下来了解一下Java中默认提供的5个基本注解,它们存在于java.lang
包下:@Deprecated
、@FunctionalInterface
、@Override
、@SafeVarargs
和@SuppressWarnings
。
请注意不要和java.lang.annotation
包下的6个元注解搞混淆了:
@Deprecated注解
首先查看一下@Deprecated
注解的源码信息,如下所示:
1 | @Documented |
可以看到@Deprecated
注解自jdk1.5引入,它用来标记过时的元素,这个注解在日常开发中会经常碰到。如果编译器在编译阶段遇到这个注解时会自动发出提醒警告,用于告诉开发人员正在调用一个过时的元素,如过时的方法、过时的类、过时的成员变量等(可以参看它的Target注解)。
举个例子,新建一个Envy类,里面的代码为:
1 | public class Envy { |
可以看到目前在该hello方法山添加了@Deprecated
注解,然后在main方法内新建一个Envy对象去调用这个hello方法,可以发现编译器此时自动给该方法中间添加了横线:
注意这个效果是编译器添加的,并不是程序具有的效果。注意过时方法不是不能使用,而是不建议使用。
@FunctionalInterface注解
首先查看一下@FunctionalInterface
注解的源码信息,如下所示:
1 | @Documented |
可以看到@FunctionalInterface
注解自jdk1.8引入,它是一个函数式接口注解,这是1.8的一个新特性。所谓的函数式接口 (Functional Interface) 其实就是一个只具有一个方法的普通接口。看到这里,笔者立马就想到了多进程中的Runnable接口,它就是一个只含有run方法的函数式接口:
1 | @FunctionalInterface |
此时你可能有一个疑惑,函数式接口有什么用,等下一篇介绍Lambda表达式的时候就会深深体会到它的用处。
@Override注解
首先查看一下@Override
注解的源码信息,如下所示:
1 | @Target(ElementType.METHOD) |
可以看到@Override
注解自jdk1.5引入,它是一个重写注解,这是使用最多的注解。当某个方法添加@Override
注解后,编译器会去检查该方法是实现父类的对应方法,可以避免一些低级错误,如单词拼写错误等。举个例子,这里有一个Movie类:
1 | public class Movie { |
然后有一个ActionMovie类,该类继承了Movie类,并实现了其中的watch方法,但是由于粗心将watch写成了wath,此时编译器就会抛出提示信息:
根据这个信息就能知道出错原因是方法单词拼错了。
@SafeVarargs注解
首先查看一下@SafeVarargs
注解的源码信息,如下所示:
1 | @Documented |
可以看到@SafeVarargs
注解自jdk1.7引入,它是一个参数安全类型注解,大家也称之为Java 7“堆污染”警告,所谓的堆污染其实就是把一个非泛型集合赋值给一个泛型集合的时候,这时就很容易发生堆污染。如果使用@SafeVarargs
注解后,它会提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生unchecked
这样的警告。这个用的不是很多。
@SuppressWarnings注解
首先查看一下@SuppressWarnings
注解的源码信息,如下所示:
1 | @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) |
可以看到@SuppressWarnings
注解自jdk1.5引入,它是一个抑制编译器警告注解。前面说过调用被@Deprecated
注解修饰的方法后,编译器会发出提醒,如果开发者想忽略这种警告,那么可以在调用时添加@SuppressWarnings
注解来实现这个目的:
1 | public class Envy { |
这样编辑器就不会去检测hello方法是否过期。同样通过一张图片来总结一下这5个基本注解:
自定义注解的使用
将自定义信息注入到方法中
在前面介绍了如何在运行时获取注解中的信息,接下来介绍如何将自定义信息注入到方法中。这里就不再自定义获取注解中信息的方法了,而是直接使用内置的方法。自定义一个@EnvyThink
注解,里面的代码为:
1 | @Retention(RetentionPolicy.RUNTIME) |
接着新建一个Envy类,其中的代码为:
1 | public class Envy { |
然后新建一个测试类EnvyTest,里面的代码为:
1 | public class EnvyTest { |
然后运行该方法,可以知道该方法的结果为:
1 | name--->envy |
将自定义对象注入到方法中
可以看到前面注入的是注解内自定义的成员属性,接下来尝试将自定义对象注入到方法中。首先解释一下什么是将自定义对象注入到方法中?举一个例子,首先定义Book实体类,里面的代码为:
1 | public class Book { |
其次再定义一个Author类,里面的代码为:
1 | public class Author { |
大家肯定知道这个setBook方法内的参数book对象其实是外界传进来的,所以如果需要调用这个方法则必须使用类似如下操作:
1 | public class AuthorTest { |
现在想通过注解的方式来将该Book对象注入到Author中,也就是如下方式:
1 | @BookAnno(name = "三国演义", price = 128) |
应该如何实现呢?往下看,首先定义一个@BookAnno
注解,里面定义两个属性name和price(注意属性数量和名称必须与Book完全保持一致),里面的代码为:
1 | @Retention(RetentionPolicy.RUNTIME) |
注意该注解必须添加@Retention(RetentionPolicy.RUNTIME)
注解语句,否则无法通过反射实现相应的功能。
接着定义一个AuthorAnnoTest
类,用于实现将Book对象注入到Author的setBook方法中。在该类中,需要通过内省(Introspector)来对JavaBean类属性、事件进行缺省处理。关于内省的相关介绍,后续会专门出一篇文章进行说明。AuthorAnnoTest
类中的代码为:
1 | public class AuthorAnnoTest { |
运行该方法,可以得到输出结果为:
1 | 三国演义 |
接下来总结一下该步骤:
1、使用内省机制来获取想要注入的属性;2、获取该属性的写方法;3、获取写方法的注解;4、获取注解上的信息(注意必须是方法,因为注解内成员变量的定义就是采用方法形式);5、获取待注入属性的实例对象;6、将注解上的信息添加到对象上;7、调用属性的写方法将已添加数据的对象注入到方法中;8、验证对象是否成功注入方法中。
Spring内置的@Autowried
注解就是完成了类似的功能才使得用户可以直接在方法中就能使用对象。
将自定义对象注入到属性中
将自定义对象注入到属性中,这也是一个常见的使用场景,通过前面的介绍,开发者可以尝试照葫芦画瓢来实现这一功能。也就是当开发者使用如下方式:
1 | public class Author { |
的代码配置时,我们依然可以像前面那样在author对象中获取注解的值。接着定义一个AuthorAnnoFiledTest
类,用于实现将Book对象注入到Author的book属性中。需要注意的是首先需要使用反射机制来获取对应的属性,其次使用内省机制来获取对应的写方法。AuthorAnnoFiledTest
类中的代码为:
1 | public class AuthorAnnoFiledTest { |
运行该方法,可以得到输出结果为:
1 | 三国演义 |
接下来总结一下该步骤:
1、使用反射机制来获取想要注入的属性;2、获取该属性的注解;3、获取注解上的信息(注意必须是方法,因为注解内成员变量的定义就是采用方法形式);4、获取待注入属性的实例对象;5、将注解上的信息添加到对象上;6、调用属性的写方法将已添加数据的对象注入到方法中;7、验证对象是否成功注入方法中。
注解总结
接下来对注解进行总结,注解其实就两个作用:(1)在适当的时候将数据注入到方法、成员变量、类或者其他组件中;(2)让编译器检查代码,进而发出提示信息。关于注解就先介绍到这里,后续会出一篇文章来总结Spring框架中常用的注解。