概念
在很多框架(Spring ,MyBatis 等等)和平时重写方法(@Override
)都会出现注解。那么注解是什么呢?看一下百度百科。
从 JDK5 开始,Java 增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
从百度百科中,我们得出几条信息
- JDK 1.5 之后的特性
- 代码中的特殊标记,在各种情况下可以被读取并进行相应的处理。
- 可以在不改变原有代码和逻辑下,在源代码中嵌入补充信息。
Annotation,英文意思,注解、注释、释文。注释我们在 Java 中都知道,注释是用文字描述程序、解释程序,给程序员看的。而注解就不同,他是给计算机看的。注释在编译运行时候是不存在的,而注解则是可以存在编译、类加载、运行时被读取。
也可以简单的理解为,注释像标签一样。例如经常出现的 @Override
,如果我们在方法上打上 @Override
,那么在 IDEA,Eclipse 等工具,就会提前检查语法,重写的语法是否正确。
例如我们要重写一个 toString()
方法,如果我们打上 @Override 注解。但是把 toString()
改成 toString1()
,就会报 Method does not override method from its superclass
编译错误。
并且我们用 javac 编译这个 Java 文件,也会编译不通过。
这个 @Override 像一个标签贴在 toString1()
方法上,告诉编译器在编译时候就要检查重写是否成功。
作用
- 编写文档:通过代码里面标识的注解生产文档。(doc 文档)
- 例如
@param
,@version
,@author
等等
- 例如
- 代码分析:在代码中标识的注解可以用来对代码进行分析(使用反射)
- 编译检查:在代码中标识的注解可以让编译器实现基本的编译检查(
@Override
)
语法
说了那么多,那么语法是什么?
基本使用
其实这个可以不用说了,你可以在方法、类、局部变量、字段等上面写 @注解名称(参数)
,这里参数可以自己根据使用的注解查看需要什么值。这里着重讲自定义注解
自定义注解
我们声明一个 java 文件,这里不在是 class
关键字,而是 @interface
,下面自定义一个 @AnoTwo
注解,并且定义两个属性 value 和 name。
AnoTwo.java
@Retention(RetentionPolicy.RUNTIME)//这行是元注解,参考下面的解释
public @interface AnoTwo{
String value();//属性
String name();//属性
}
本质
我们把上面编译完成的 class 文件反编译一下(javap 指令),打印了如下语句
Compiled from "AnoTwo.java"
public interface com.test.annotation.AnoTwo extends java.lang.annotation.Annotation {
public abstract java.lang.String value();
public abstract java.lang.String name();
}
咦,他继承了一个接口类 Annotation,并且变成了一个接口,而我们在注解里面写的属性 value 和 name 都变成了抽象方法。原来注解的本质就是一个接口,并且把我们的属性变成了抽象方法,所以把注解里面属性当成抽象方法也是可以的。
可以看看 Annotation 都有啥
返回值 | 方法 |
---|---|
class<? extends Annotation> | annotationType() 返回此注释的注释类型。 |
boolean | equals(Object obj) 如果指定的对象表示在逻辑上等同于该注释的注释,则返回true。 |
int | hashCode() 返回此注释的哈希码,定义如下: |
String | toString() 返回此注释的字符串表示形式。 |
使用
那这也就是说,注解中写的就是抽象方法。只是这里对于属性的赋值的类型,和抽象方法(属性)的返回值是一样的。那么如何定义属性和赋值呢?
定义属性要求
-
属性的返回值只能取下列几个
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
这里你在赋值的时候,也只能赋予和返回值相同类型的才可以。
如何使用?
我在一个 Person 类的 show 方法使用该注解,语法差不多是这样 @注解名称( 属性名1=值1,属性值2=值2...)
,
@AnoTwo(value = "我是 value",name = "小王")
public void show(){
System.out.println();
}
如果这里只有一个 value 属性,如下
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoTwo {
String value();
}
那么我们可以不指定属性,直接赋值就行(如果这 value 属性是 String 的返回值),可以这样写
@AnoTwo("我是 value")
public void show(){
System.out.println();
}
注意:
- 属性定义了是要赋值的,但是如果使用了 default 关键字给属性初始化,在使用注解时候,就可以省略不赋值。
- 如果只用一个属性需要赋值,并且属性是 value,则 value 可以忽略,直接定义值就行
- 数组赋值的时候,使用
{}
包裹,如果数组中元素只用一个,则{}
可以省略。
内置注解
说了那么多,先解释一下几个内置注解
下面是作用在代码上
@Override
- 该注解用来告诉编译器判断该方法是否为重写方法,如果父类或者父接口,没有对应的方法,则会报编译错误。
@Deprecated
- 此注解可以用来提示使用该方法的调用者,该方法已经遗弃,已经不建议使用。
@SuppressWarnings
- 该注解可以压制警告,放在方法上则是压制该方法中的警告,放在类上则压制类中所用警告(压制也就是不提醒警告了)
- 通常里面的传值为 all ,例如
@SuppressWarnings("all")
作用在注解之上的,也叫做元注解
那么元注解是什么?定义在注解之上,是用来定义当前的注解的其他含有,例如当前的注解目标可以用在哪个地方,生命周期,能否生产在 doc 文档上等等。简单理解为一个标签上,又写了一个标签,表面我贴在下面的标签,表达其他的含义。
形如:
@Documented // 元注解,表示可以生成道 doc 文档中
@Retention(RetentionPolicy.RUNTIME)// 元注解,这里表示注解可以在 JVM 运行时候看的
@Target(ElementType.TYPE) // 元注解,这里表示注解只能用在类、接口等的上面。
public @interface Test {
...
}
-
@Target
Target 英文中是目标的意思,表面了你当前定义的注解要被用在哪里,字段?方法?类等等
查看它的源码
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); }
我们发现它接受的是一个 ElementType (enum)枚举值数组,其中一共有七个枚举值
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
-
@Retention
Retention 有着保留,扣留的意思,也就是这段注解可以在哪被看到。通过查看源码
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); }
咦,也发现了他也是接收一个 RetentionPolicy 的枚举值。其中有下面几个枚举值:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
-
@Documented
显而易见,这就注解也就是能够将注解中的元素包含到 Javadoc 中去。
-
@Inherited
描述注解是否被继承。
也就是被
@Inherited
所注解的注解(这里假如叫@Test
),如果父类使用@Test
注解自身,那么其子类也会继承父类的时候,也会基础这个注解,来注解自身。
从 JDK 1.7 开始,新增的三个注解
-
@SafeVarargs
- 忽略任何使用参数为泛型参数的方法或构造函数所产生的警告。
-
@FunctionalInterface
- Java 8 开始支持,标识一个匿名函数或者函数式接口
-
@Repeatable
-
Java 8 开始,标识谋注解在同一个生命上面可以使用多次
-
例如有一个注解
@Role
(角色),并且里面使用了@Repeatable
,只有一个 value 属性,那么在对一个 Student 类使用的时候,可以用多次,因为一个学生可以有很多角色。@interface Roles{ Role[] value(); } @Repeatable(Roles.class) @interface Role{ String value(); } @Role("学生") @Role("儿子") @Role("哥哥") public class Student{ ....... }
-
实战
那么平时如何通过反射使用?
例如我这里 写了一个 Person 类,里面有 name,age 属性,有 show 方法,并且定义了一个注解 @Per 如下:
Per.java
//这里得让注解在 JVM 运行时能够看到,如果在 source 时候就消失,后面我们就得不到这个注解对象
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Pre {
String className();
String method();
}
Person.java
@Per
public class Person {
private String name;
private String age;
public void setName(String name) {
this.name = name;
}
public void setAge(String age) {
this.age = age;
}
public String getAge() {
return age;
}
public String getName() {
return name;
}
public void show(){
System.out.println("name = "+name",age = "+age);
}
}
Main 函数使用这个注解
@Pre(className = "com.test.Person",method = "show")
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class main = Main.class;
System.out.println(main.getAnnotations().length);
Pre p = (Pre) main.getAnnotation(Pre.class);
String className = p.className();
String method = p.method();
Class cls = Class.forName(className);
Person person = (Person)cls.newInstance();
person.setName("小王");
person.setAge("20");
Method m = cls.getMethod(method);
m.invoke(person);
System.out.println(p.getClass());
}
}
-
第一步解析对象,获取该类的字节码,这个注解在 Main.java 上,我们获取这个 Main.java 的字节码对象
Class main = Main.class;
-
获取在类上的 @Pre 注解
// 其实在内存中生产了一个该注解接口的子类实现对象 // 通过指定 Class 对象来获得注解对象 Pre p = (Pre) main.getAnnotation(Pre.class);
这里 Java 内部自己生产了方法,大概形式是这样的
public class PreImpl implements Pre { String className(){ return "com.test.Person"; } String method(){ return "show" } }
-
调用注解对象中定义的抽象方法,因为我们知道注解其实是接口
String className = p.className(); String method = p.method();
-
加载 Person 类进入内存
Class cls = Class.forName(className);
-
创建对象
Person person = (Person)cls.newInstance(); person.setName("小王"); person.age("20");
-
获取方法对象
Method m = cls.getMethod(method);
-
执行方法
m.invoke(obj);
打印结果
name = 小王,age = 20
这里我们不妨看看打印一下 @Pre
对象 p 的 class 对象,通过 p.getClass()
,控制台会打印出
class com.sun.proxy.$Proxy1
也就是说,@Pre 这个注解里面抽象方法(属性)的实现逻辑,由动态代理完成了。
getAnnotation(Class) 通过指定 Class 对象获取注释
getAnnotations() 返回一个数组,包含在该对象(Class,Method 等)的所有注解
isAnnotationPresent(Class) 判断类上是否存在指定的注解