前言
在日常开发过程中,我们会经常使用别人已经定义好的系统或者框架注解,有时也需要我们自定义一些注解,来简化我们的开发,本文主要讲解注解的概念、注解的作用、注解的分类、元注解、以及剖析注解的原理,然后将通过一个案例解析我们在开发中如何自定义注解和使用自定义注解简化开发,最后将介绍一些源码中的注解案例。
一、注解的概念
* 注解是JDK1.5的新特性。 * 注解相当一种标记,是类的组成部分,可以给类携带一些额外的信息。 * 标记(注解)可以加在包,类,字段(成员变量),方法,方法参数以及局部变量上。 * 注解是给Java虚拟机(jvm)看的,编译器或JVM可以根据注解来完成对应的功能;而注释是给人(程序员)看的!
注解(Annotation) 相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无何种标记,看你的程序有什么标记,就去干相应的事,标记可以加在包、类,属性、方法,方法的参数以及局部变量上。
二、注解的作用
注解的作用: 注解的作用就是给程序带入参数。【替换xml配置文件】
注解的作用分类:
- 编写文档: 通过代码里标识的元数据生成文档【生成文档doc文档】
- 代码分析: 通过代码里标识的元数据对代码进行分析【使用反射】
- 编译检查: 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override等】
以下几个常用操作中都使用到了注解:
1. 生成帮助文档
* @author: 用来标识作者姓名。 * @version:用于标识对象的版本号,适用范围:文件、类、方法。
使用@author和@version注解就是告诉Javadoc工具在生成帮助文档时把作者姓名和版本号也标记在文档中。
2. 编译检查
* @Override:用来修饰方法声明。 复制代码
用来告诉编译器该方法是重写父类中的方法,如果父类不存在该方法或者重载的方法签名有误,则编译失败。
3. 框架的配置
我们在后续的系统架构时,可以通过注解的形式将*.properties或者*.yaml将配置加载系统里(框架=代码+配置)
具体使用请关注框架相关的内容分享。 【使用注解取代 xml配置!】
三、 注解分类
3.1 注解分类
注解可以根据注解参数分为三大类:
- 标记注解: 没有参数的注解,仅用自身的存在与否为程序提供信息,如@Override注解,该注解没有参数,用于表示当前方法为重写方法。
- 单值注解: 只有一个参数的注解,如果该参数的名字为value,那么可以省略参数名,如 @SuppressWarnings(value = "all"),可以简写为@SuppressWarnings("all")。
- 完整注解: 有多个参数的注解。
3.2 标记注解
@Override注解是一个标记注解,那我们进入到该注解的源码查看一下。从上往下看该注解源码,发现它继承了导入了java.lang.annotation.*
,也就是有使用到该包的内容。然后下面就又是两个看不懂的注解,其实发现注解的定义格式是public修饰的@Interface,最终看到该注解中方法体并没有任何参数,也就是只起到标记作用。
3.3 单值注解
在上面我们用到的@SuppressWarnings注解就是一个单值注解。那我们进入到它的源码看一下是怎么个情况。其实,和标记注解比较,它就多一个value参数而已,而这就是单值注解的必要条件,即只有一个参数。并且这一个参数为value时,我们可以省略value。
3.4 完整注解
上述两个类型注解讲解完,至于完整注解嘛,这下就能更明白了。其中的方法体就是有多个参数而已。
四、元注解
元注解:对注解进行修饰的注解就是元注解!
Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解有以下几种:
- @Target
- @Retention
- @Documented
- @Inherited
4.1 @Target
用于描述注解的范围,即注解在哪用。它说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)等。取值类型(ElementType)有以下几种:
TYPE,类,接口 FIELD, 成员变量 METHOD, 成员方法 PARAMETER, 方法参数 CONSTRUCTOR, 构造方法 LOCAL_VARIABLE, 局部变量 ......
示例:
@Target({ElementType.METHOD, ElementType.TYPE}) public @interface Log { ...... }
上述表示Log注解可以用在类、接口、enum和方法上
4.2 @Retention
用于描述注解的生命周期,表示需要在什么级别保存该注解,即保留的时间长短。取值类型(RetentionPolicy)有以下几种:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在class文件中有效(即class保留)
- RUNTIME:在运行时有效(即运行时保留)
示例:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Log { ...... }
上述示例使用RetentionPolicy.RUNTIME,这样注解处理器可以通过反射,获取到该注解的属性值,从而去做一些运行时的逻辑处理。
4.3 @Documented
用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。它是一个标记注解,没有成员。
示例:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { ...... }
4.4 @Inherited
用于表示某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
五、注解解析
5.1 注解解析概念和原理
使用Java技术获得注解上数据的过程则称为注解解析。与注解解析相关的接口
* Annotation: 注解类,该类是所有注解的父类。 * AnnotatedElement:该接口定义了与注解解析相关的方法(Method/Field/Constructor等都实现了该接口) boolean isAnnotationPresent(Class<Annotation> annotationClass) //判断当前对象是否使用了指定的注解,如果使用了则返回true,否则false T getAnnotation(Class<T> annotationClass) // 根据注解类型获得对应注解对象 Annotation[] getAnnotations() //获得当前对象上使用的所有注解,返回注解数组,包含父类继承的 Annotation[] getDeclaredAnnotations() // 获得当前对象上使用的所有注解,返回注解数组,只包含本类的
获取注解数据的原理:
注解作用在哪个成员上就会得该成员对应的对象来获得注解
- 比如注解作用成员方法,则要获得该成员方法对应的Method对象
- 比如注解作用在类上,则要该类的Class对象
- 比如注解作用在成员变量上,则要获得该成员变量对应的Field对象。
Field,Method,Constructor,Class等类都是实现了AnnotatedElement接口
5.2 注解解析演示
需求:
① 定义注解Book,要求如下:
② 定义BookStore类,在类和成员方法上使用Book注解
@Book(value = "JavaWeb核心指南", authors = {"张杰", "苏强"}) public class BookStore { // 成员方法 @Book(value = "Linux删库跑路", authors = {"王金良", "何向日"}, price = 88) public void show() { } }
③ 定义TestAnnotation测试类获取Book注解上的数据
/* 定义TestAnnotation测试类获取Book注解上的数据 比如注解作用成员方法,则要获得该成员方法对应的Method对象 比如注解作用在类上,则要该类的Class对象 比如注解作用在成员变量上,则要获得该成员变量对应的Field对象。 */ public class TestAnnotaion { /* 注解解析,获得BookStore类上面Book注解的里面属性对应的值 */ @Test public void testType(){ // 获得Class对象 Class<BookStore> bookStore = BookStore.class; // 判断类BookStore上面有没有使用Book注解! if(bookStore.isAnnotationPresent(Book.class)){ // 获得类BookStore上面的注解对象! Book book = bookStore.getAnnotation(Book.class); // 获得注解属性的值 String name = book.value(); String[] authors = book.authors(); double price = book.price(); // 获得默认值 100 // 打印 System.out.println("书名:" + name); System.out.println("作者:" + Arrays.toString(authors)); System.out.println("价格:" + price); } } /* 注解解析,获得BookStore类中show方法上面Book注解里面的属性对应的值 */ @Test public void testMethod() throws NoSuchMethodException { // 获得字节码对象 Class<BookStore> bookStore = BookStore.class; // 获得指定的方法对象 Method show = bookStore.getMethod("show"); // 判断show方法上面有没有使用@Book注解 if(show.isAnnotationPresent(Book.class)){ // 通过Method对象获得show方法上面的注解对象! Book book = show.getAnnotation(Book.class); // 获得注解属性的值 String name = book.value(); String[] authors = book.authors(); double price = book.price(); // 获得默认值 100 // 打印 System.out.println("书名:" + name); System.out.println("作者:" + Arrays.toString(authors)); System.out.println("价格:" + price); } } }
六、 jdk的内置注解
6.1 内置注解
- @Override: 标记在成员方法上,用于标识当前方法是重写父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合重写规则,如果不符合,编译报错。
- @Deprecated: 用于标记当前类、成员变量、成员方法或者构造方法过时如果开发者调用了被标记为过时的方法,编译器在编译期进行警告。
- @SuppressWarnings: 压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息。
6.2 @Override注解
标记在成员方法上,用于标识当前方法是重写父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合重写规则,如果不符合,编译报错。
这里解释一下@Override注解,在我们的Object基类中有一个方法是toString方法,我们通常在实体类中去重写此方法来达到打印对象信息的效果,这时候也会发现重写的toString方法上方就有一个@Override注解。如下所示:
于是,我们试图去改变重写后的toString方法名称,将方法名改为toStrings。你会发现在编译期就报错了!如下所示:
那么这说明什么呢?这就说明该方法不是我们重写其父类(Object)的方法。这就是@Override注解的作用。
6.3 @Deprecated注解
用于标记当前类、成员变量、成员方法或者构造方法过时,如果开发者调用了被标记为过时的方法,编译器在编译期进行警告。
想要理解**@Deprecated** 注解,就需要模拟一种场景了。公司产品V1.0版本为用户提供了show1方法的功能。后期开发V2.0版本,在show1方法的功能进行了扩展,打算发布show2。这时就可在show1方法上标注@Deprecated表示show1方法已经过时。
6.4 @SuppressWarnings注解
压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息,该注解为单值注解,只有 一个value参数,该参数为字符串数组类型,参数值常用的有如下几个。
- unchecked:未检查的转化,如集合没有指定类型还添加元素
- unused:未使用的变量
- resource:有泛型未指定类型
- path:在类路径,原文件路径中有不存在的路径
- deprecation:使用了某些不赞成使用的类和方法
- fallthrough:switch语句执行到底没有break关键字
- rawtypes:没有写泛型,比如: List list = new ArrayList();
- all:全部类型的警告
@SuppressWarning主要用于编译器编译过程中发出的警告信息,可以让程序忽略警告顺利编译。
七、自定义注解的定义和使用
7.1 自定义注解格式
public @interface 注解名{ // 里面定义属性即可! }
如:定义一个名为Student的注解
public @interface Student { }
7.2 自定义注解的属性
* 属性定义的格式: 格式1:数据类型 属性名(); 格式2:数据类型 属性名() default 默认值; * 属性适用的数据类型 * 八种数据数据类型(int,short,long,double,byte,char,boolean,float) * String,Class,注解类型,枚举类 * 以上类型的一维数组形式
* 属性定义的示例: public @interface MyAnnotation { // 为注解定义属性 String[] value(); int age() default 38; }
7.3自定义注解的定义
需求:定义一个注解:Book
- 包含属性:String value() 书名
- 包含属性:double price() 价格,默认值为 100
- 包含属性:String[] authors() 多位作者
/* 自定义注解! 包含属性:String value() 书名 包含属性:double price() 价格,默认值为 100 包含属性:String[] authors() 多位作者 */ public @interface Book { // 为注解定义属性 String value(); double price(); String[] authors(); }
7.4 使用自定义注解(为注解的属性赋值)
* 普通属性: 在使用注解的时候需要为注解的属性赋值!(定义注解属性给定的是什么类型的,那么使用的时候就给这个属性赋一个该类型的值) * 特殊属性value: 如果注解中只有一个属性且名字叫value,则在使用该注解时可以直接给该属性赋值,而不需要给出属性名。 如果注解中除了value属性之外还有其他属性且只要有一个属性没有默认值,则在给属性赋值时value属性名也不能省略了。 * 小结:如果注解中只有一个属性时,一般都会将该属性名命名为value
/* 注解使用: 在方法上面添加自定义注解,然后为该注解的属性赋值! */ public class TestBook { public static void main(String[] args) { } // 使用自定义注解的时候,需要为自定义注解的属性赋值! @Book(value ="xxx", price = 68.0 , authors = {"csjava","恕毅"}) public void method(){ } } // 若Book注解在定义的时候没有指定属性,那么使用这个注解的时候,不用为注解的属性赋值!
7.5 为注解的特殊属性赋值
① 若定义的注解中只有1个属性,而且属性的名字为value,那么在使用注解为属性赋值时,注解的属性名可以省略不写!(写上也不报错!)
public @interface Book { // 为注解定义属性 String value(); }
@Book("xxx") // 此处可以写成 @Book(value="xxxx",还可以省略属性名称value不写 @Book("yyyy")) public void method(){ }
② 若定义的注解中只有1个属性,属性的名字为value,并且为value赋默认值了,那么在使用注解为属性赋值时可以不用为注解属性赋值
public @interface Book { // 为注解定义属性 String value() default "vip"; }
@Book // 正确的! public void method(){}
也可以这么干:
@Book(value="xxxx") // 正确的! public void method(){} // 或者 @Book("xxxx") // 正确的! public void method(){}
7.6 为注解的属性赋值(各种类型演示)
public class Test01{ @Deprecated public void test1(){ } }
public @interface MyAnnotation { // 为注解定义属性 String[] value(); int age() default 38; }
public enum Sex { FatBoy,PrettyGirl }
自定义注解:
/* 注解属性支持的数据类型有: * 八种数据数据类型(int,short,long,double,byte,char,boolean,float) * String,Class,注解类型,枚举类 * 以上类型的一维数组形式 */ public @interface itheima { // 基本类型 char a(); // 字符串类型 String b(); // Class类型 Class c(); // 注解类型 MyAnnotation d(); // 枚举类型 Sex e(); // 数组 int[] f(); }
使用自定义注解(为注解的属性赋值):
/* 使用自定义注解@book */ public class TestBook{ // 使用自定义注解,必须为注解的属性赋值! @book(a = 'A', b = "666", c = Test01.class, d = @MyAnnotation(value = {"xxx", "ooo"}), e = Sex.FatBoy, f = {10, 20}) public void method() { } }
总结
在实际的开发中,可以采用注解加配置文件的形式来完成代码的解耦,在配置信息发生改变的情况下,我们可以通过修改配置文件的方法,通过注解来动态取值。