java源码分析-注解基本原理
java注解在实际工作中经常彭代,无论是java内置注解还是框架自带注解,在我们使用这些注解的时候是否思考过这些注解到底是什么作用?是怎么起作用的?java以及框架是怎么识别这些注解的?本篇我们就来聊一聊注解的基本原理。
1.注解是什么
其实注解在大多数情况下与普通的修饰符(public,void,static等)在使用方式上并没有多大区别。我们先来看个例子:
public class AnnotationDemo {
@Override
public String toString() {
return super.toString();
}
@Deprecated
public static void methodA(){
System.out.println("方法已过期");
}
@SuppressWarnings("uncheck")
public static void methodB(){
System.out.println("忽略检查警告");
}
}
上面的例子中@Override是一个注解,这是一个java内置注解,java通过该注解表示某个方法重写了父类的方法。可以看到注解其实就类似于一个标识符,当java运行时会根据注解的功能完成相关功能。而对于@Deprecated和@SuppressWarnings(“uncheck”)这两个注解,也是Java本身内置的注解,在代码中也可以经常看见它们,当方法或是类上面有@Deprecated注解时,说明该方法或是类都已经过期不建议再用,@SuppressWarnings 则表示忽略指定警告,比如@SuppressWarnings(“uncheck”),这就是注解的最简单的使用方式。
自java1.5版本引入注解之后,注解就成了java平台中非常重要的一部分。注解也叫元数据,即一种描述数据的数据。所以可以说注解就是源代码的元数据。
1.1注解的基本语法
元注解
元注解是用来修饰注解的注解,通常用作注解的定义和声明上。那最常见的@Overide注解来看一下它的定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其中元注解就是@Target和@Retention。在java中有以下几个元注解:
- @Target:注解的作用目标
- @Retention:注解的生命周期
- @Documented:注解是否应当被包含在 JavaDoc 文档中
- @Inherited:是否允许子类继承该注解
@Target注解用来指定声明的注解作用范围,也就是说@Override注解可以在哪些地方使用。
@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();
}
@Target注解内部是是一个ElementType类型的数组。表示可以通过设置ElementType类型的值来为@Target设置值。看一下ElementType是一个什么?
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE, //类、接口(注解)和枚举类
/** Field declaration (includes enum constants) */
FIELD, //字段
/** Method declaration */
METHOD, //方法
/** Formal parameter declaration */
PARAMETER, //参数
/** Constructor declaration */
CONSTRUCTOR, //构造器
/** Local variable declaration */
LOCAL_VARIABLE, //局部变量
/** Annotation type declaration */
ANNOTATION_TYPE, //注解
/** Package declaration */
PACKAGE, //包
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER, //类型参数(1.8新加入)
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE //类型使用声明(1.8新加入)
}
可以看到ElementType是一个枚举类,表示@Target注解的value可以设置的范围是这些枚举值。各个枚举值的含义也已经给出。
注意当@Target未指定具体的值时,则表示此注解可用在任何地方。当有多个值时,可以使用{}并用逗号隔开:
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
@Retention用来约束注解的生命周期,它的底层也是一个枚举类,但是与@Target不同,@Retention一次只能设置一个值。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
RetentionPolicy枚举类,它有三个值:SOURCE、CLASS、RUNTIME
- SOURCE:表示注解会被编译期丢弃。该注解只会保留在源码中,源码经过编译,注解将被丢弃,即在class文件中将找不到该注解了;
- CLASS:表示注解会被VM丢弃。该注解会保留在class文件中,但是当class加载到vm内存中是,该注解会被丢弃。注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等;
- RUNTIME:表示该注解将在运行期(jvm)中保留,因此可以通过反射机制获取到该注解的相关信息。如SpringMvc中的@Controller、@Autowired、@RequestMapping等。
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
所以@Override上的@Retention(RetentionPolicy.SOURCE)就表示@Override注解只会被保留在源码中。
@Documented注解用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,也就是说被@Documented修饰的注解会生成到javadoc中。Documented是一个标记注解,没有成员。
@Inherited可以让注解被继承,但其实不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解,举例如下:
public class MyAnnotation2{
public static void main(String[] args) {
ClassA1 classA1 = new ClassA1();
Annotation[] annotations1 = classA1.getClass().getAnnotations();
System.out.println(Arrays.toString(annotations1));
ClassB1 classB1 = new ClassB1();
Annotation[] annotations2 = classB1.getClass().getAnnotations();
System.out.println(Arrays.toString(annotations2));
}
}
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotationA1 {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotationB1 {
}
@MyAnnotationA1
class ClassA {
}
class ClassA1 extends ClassA{
}
@MyAnnotationB1
class ClassB{
}
class ClassB1 extends ClassB{
}
直接代码:
可以看到ClassA、ClassB上分别使用了@MyAnnotationA1、@MyAnnotationA2注解,而只有@MyAnnotationA1住街上使用了@Inherited注解,这样在子类继承ClassA、ClassB时,使用getAnnotations()方法只能获取ClassA1的父类ClassA上的注解信息。
注解元素
在上述对@Override在注解中,我们可以看到该注解内部没有定义其他元素,所以@Override也称为标记注解(marker annotation)。但是一般在定义注解时都会包含一些元素以表示某些值,方便处理器使用。比如我们看到的@SuppressWarnings注解:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
这个注解的注解元素就是String数组类型的value;通过使用元素来进行设置一些值,注解中的元素类似于属性或者参数。注意到注解中的元素声明的方式是方法(无方法体),这一点类似与接口。
这样在使用注解时,就可以为该注解设定相关的值,当猪姐处理器解析(后面会说)注解元素的值后会做相应的处理。
注解支持的元素类型除了上面的String之外还有以下:
- 基本类型(int、char、byte、double、float、long、boolean)
- 字符串String
- 类Class
- 枚举enum
- 注解Annotation
- 上述类型的数组类型
去除这些类型,当我们使用其他类型修饰注解元素时,编译期会报错,Invalid type 'Integer' for annotation member
。当然基本类型的包装类型也是不允许在注解中修饰注解元素的。
这里还需要注意一点:注解Annotation也是可以作为注解元素的修饰类型。也就是嵌套注解。如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation3 {
// Integer value(); //编译器报错
AnnotationX value() default @AnnotationX(flag = true);
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationX{
boolean flag() default false;
}
java内置注解
java的内置注解主要有三个,上面也提到过,这里简单的过一下,分别是@Override、@Deprecated、@SuppressWarnning。
- @Override:用于标明此方法覆盖了父类的方法,源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
- @Deprecated:用于标明已经过时的方法或类,源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
- @SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告,其实现源码如下:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
该注解内部有一个String数组类型的注解元素value,主要取值如下:
- deprecation:使用了不建议使用的类或方法时的警告;
- unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
- fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
- path:在类路径、源文件路径等中有不存在的路径时的警告;
- serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
- finally:任何 finally 子句不能正常完成时的警告;
- all:关于以上所有情况的警告。
举个例子:
当我们诗句集合时,没有使用泛型来指定集合保存的类型时;
public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add(1);
System.out.println(list);
}
编辑器就会有如下警告:
可以通过@SuppressWarnings(“unchecked”)消除警告:
@SuppressWarnings("unchecked")
List list = new ArrayList();
1.2注解的默认值
注解对注解元素的默认值有着一定的限制。具体的限制如下:
(1)首先,注解元素的默认值必须明确,也就是说元素不能有不能确定的值。这就要求注解元素的默认值要么是在定义注解的时候通过default给出,要么是在使用注解时为元素提供具体的值;
(2)对于非基本类型的注解元素无论是在源代码中声明,还是在注解接口中定义默认值,都不能以null作为值。通常可以使用一些特殊字符(空字符串、负数等)来解决这个问题。
1.3注解不能够继承
首先注解是不能够被继承的,不能使用关键字extends来继承某个@interface,但是编译器在将注解编译后,编译器会自动继承java.lang.annotation.Annotation接口,本来想把@Override反编译出来论证一下,但发现Idea反编译工具太智能,反编译出来没有变化。但是我通过Idea中的类图工具Diagrams查看如下:
这也说明了Override注解是继承Annotation。
2.自定义注解
2.1如何自定义注解
我们那java内置注解@SuppressWarnings来分析:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
(1)首先@Target元注解不可少,用来标注注解的使用范围;
(2)@Retention注解也是不可少的,通过它能够确定我们定义的注解的生命周期;
(3)定义注解是使用@interface,注意月接口仅相差一个@符号,但是意义完全不同;
(4)注解元素可有可无,需要定义注解元素是通常的格式:类型 字段名()。类型范围在上文艺界说明;
(5)一般会为注解元素通过default设置默认值,如果没有设置,在使用注解时一定要有值。
例如我们定义一个表示人物信息的注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPersonAnnotation {
//姓名
String name() default "";
//年龄
int age() default 0;
//性格
Character character() default Character.OPEN;
}
enum Character{
OPEN,
IRRITABLE,
GENTLE;
}
简单分析一下:
@MyPersonAnnotation注解:
- @Target(ElementType.METHOD)表示注解可以使用在方法上;
- @Retention(RetentionPolicy.RUNTIME)表示该注解在jvm运行时可以通过反射获取到注解信息;
- 有三个注解元素分别是name、age和character,其中character的类型是我们自定义的枚举类型Character。
2.2使用注解
其实注解使用时,只要根据该注解的定义就可以基本就知道如何使用了,例如上面我们定义的注解可以用在方法上,具体如下:
public class MyPersonAnnotationTest {
public static void main(String[] args) {
getPersonInfo();
}
@MyPersonAnnotation(name = "zhangsan", age = 18, character = Character.GENTLE)
private static void getPersonInfo() {
}
}
2.3注解处理器
注解定义好了,并且也在方法上使用了,但是通过这个注解能干什么呢?这就是注解处理器的作用了,这也是自定义注解使用的核心。
下面面我们演示通过反射获取动态运行时的注解信息,而这种方式其实就是注解处理器的工作原理:
public class MyPersonAnnotationTest {
public static void main(String[] args) throws NoSuchMethodException {
process(MyPersonAnnotationTest.class);
}
@MyPersonAnnotation(name = "zhangsan", age = 18, character = Character.GENTLE)
private static void getPersonInfo() {
}
private static void process(Class clazz) throws NoSuchMethodException {
//通过Class对象拿到getPersonInfo方法对象Method
Method method = clazz.getDeclaredMethod("getPersonInfo", null);
System.out.println(method.getName());
//根据Method获取到该方法上的注解
MyPersonAnnotation declaredAnnotations = method.getDeclaredAnnotation(MyPersonAnnotation.class);
System.out.println(declaredAnnotations);
}
}
打印结果:
也就是说我们通过反射机制,拿到了运行时getPersonInfo()方法上的注解信息,通过这个方式我们就可以对这个注解标注的类、方法、字段等做更多的操作了。需要注意的是,通过反射机制获取运行时注解信息,需要我们在定义注解是设置@Retention(RetentionPolicy.RUNTIME),否则无法获取到!
实际上这就是注解处理器的原理,在Spring中就是通过通过这种方式定义各种注解处理器类和方法对spring的相关注解,如@Service等进行相应的操作。