什么是注解,注解的出现是要解决什么问题?
使用Annotation之前(甚至在使用之后),XML被广泛的应用于描述元数据。不知何时开始一些应用开发人员和架构师发现XML的维护越来越糟糕了。他们希望使用一些和代码紧耦合的东西,而不是像XML那样和代码是松耦合的(在某些情况下甚至是完全分离的)代码描述。如果你在Google中搜索“XML vs. annotations”,会看到许多关于这个问题的辩论。最有趣的是XML配置其实就是为了分离代码和配置而引入的。上述两种观点可能会让你很疑惑,两者观点似乎构成了一种循环,但各有利弊。下面我们通过一个例子来理解这两者的区别。
Annotation本质上就是元数据,诞生的目的就是在java中做标记。
XML vs. annotations
假如你想为应用设置很多的常量或参数,这种情况下,XML是一个很好的选择,因为它不会同特定的代码相连。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须认识到这点。
另一个很重要的因素是Annotation定义了一种标准的描述元数据的方式。在这之前,开发人员通常使用他们自己的方式定义元数据。例如,使用标记interfaces,注释,transient关键字等等。每个程序员按照自己的方式定义元数据,而不像Annotation这种标准的方式。
目前,许多框架将XML和Annotation两种方式结合使用,平衡两者之间的利弊。
下面讲述java中jdk对注解(Annotation)的支持:
J2SE5.0版本在 java.lang.annotation提供了四种元注解,专门注解其他的注解:
@Documented –注解是否将包含在JavaDoc中
@Retention –什么时候使用该注解
@Target –注解用于什么地方
@Inherited – 是否允许子类继承该注解
jdk1.6之后又根据上述的四种基本的注解声明了java开发中常用到的注解:比如@Overrride,@SuppressWarnings,@Deprecation等,这些都是jdk1.6以后自带的注解。
还有:
@SafeVarargs 是JDK 7 专门为抑制“堆污染”警告提供的。
@FunctionalIterface (java 8 新增的) 函数式接口。Java8规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口。
使用上述的四个注解基本量(Documented,Retention,Target,Inherited)声明一个新的注解:
@Documented 一个简单的Annotations标记注解,表示是否将注解信息添加在java文档中。
@Retention 定义该注解的生命周期。
RetentionPolicy.SOURCE – 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
RetentionPolicy.CLASS – 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
RetentionPolicy.RUNTIME– 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
(三个级别)
@Target 表示该注解用于什么地方。如果不明确指出,该注解可以放在任何地方。以下是一些可用的参数。需要说明的是:属性的注解是兼容的,如果你想给7个属性都添加注解,仅仅排除一个属性,那么你需要在定义target包含所有的属性。
ElementType.TYPE:用于描述类、接口或enum声明
ElementType.FIELD:用于描述实例变量
ElementType.METHOD
ElementType.PARAMETER
ElementType.CONSTRUCTOR
ElementType.LOCAL_VARIABLE
ElementType.ANNOTATION_TYPE 另一个注释
ElementType.PACKAGE 用于记录java文件的package信息
@Inherited 定义该注释和子类的关系
————————————————————————————————————————————————那么,注解的内部到底是如何定义的呢?Annotations只支持基本类型、String及枚举类型。注释中所有的属性被定义成方法,并允许提供默认值。(Annotations中只有成员属性,其就是标记key,其值就是value)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Todo {
public enum Priority {LOW, MEDIUM, HIGH}
public enum Status {STARTED, NOT_STARTED}
String author() default "Yash";
Priority priority()defaultPriority.LOW;
Status status() default Status.NOT_STARTED;
}
下面的例子演示了如何使用上面的注解:
@Todo(priority = Todo.Priority.MEDIUM, author ="Yashwant", status = Todo.Status.STARTED)
public void incompleteMethod1() {
//Some business logic is written
//But it’s not complete yet
}
如果注解中只有一个属性,可以直接命名为“value”,使用时无需再标明属性名。
@interface Author{
String value();
}
@Author("Yashwant")
public void someMethod() {
}
但目前为止一切看起来都还不错,我们定义了自己的注解并将其应用在业务逻辑的方法上。
但是现在我们需要写一个用户程序调用我们业务逻辑上使用我们自己的注解,并且根据注解做进一步处理。这里我们需要使用反射机制,如果你熟悉反射代码,就会知道反射可以提供类名、方法和实例变量对象。所有这些对象都有getAnnotation()这个方法用来返回注解信息。我们需要把这个对象转换为我们自定义的注释(使用 instanceOf()检查之后),同时也可以调用自定义注释里面的方法。看看以下的实例代码,使用了上面的注解:
Class businessLogicClass = BusinessLogic.class;
for(Method method : businessLogicClass.getMethods()) {
Todo todoAnnotation = (Todo)method.getAnnotation(Todo.class);
if(todoAnnotation !=null) {
System.out.println(" Method Name : "+ method.getName());
System.out.println(" Author : "+ todoAnnotation.author());
System.out.println(" Priority : "+ todoAnnotation.priority());
System.out.println(" Status : "+ todoAnnotation.status());
}
}
#################################################################################
下面讲一下,虚拟机层面怎么对注解元数据的保存,传递的。
下文所使用的java版本信息
$ java -version
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)
public interface Annotation {
...
}
下面看一下具体示例。
定义一个注解:import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Administrator on 2015/1/18.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
int count() default 1;
}
Classfile /e:/workspace/intellij/SpringTest/target/classes/TestAnnotation.class
Last modified 2015-1-18; size 379 bytes
MD5 checksum 200dc3a75216b7a88ae17873d5dffd4f
Compiled from "TestAnnotation.java"
public interface TestAnnotation extends java.lang.annotation.Annotation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
#1 = Class #14 // TestAnnotation
#2 = Class #15 // java/lang/Object
#3 = Class #16 // java/lang/annotation/Annotation
#4 = Utf8 SourceFile
#5 = Utf8 TestAnnotation.java
#6 = Utf8 RuntimeVisibleAnnotations
#7 = Utf8 Ljava/lang/annotation/Target;
#8 = Utf8 value
#9 = Utf8 Ljava/lang/annotation/ElementType;
#10 = Utf8 TYPE
#11 = Utf8 Ljava/lang/annotation/Retention;
#12 = Utf8 Ljava/lang/annotation/RetentionPolicy;
#13 = Utf8 RUNTIME
#14 = Utf8 TestAnnotation
#15 = Utf8 java/lang/Object
#16 = Utf8 java/lang/annotation/Annotation
{
}
SourceFile: "TestAnnotation.java"
RuntimeVisibleAnnotations:
0: #7(#8=[e#9.#10])
1: #11(#8=e#12.#13)
从反编译后的信息中可以看出,注解就是一个继承自`java.lang.annotation.Annotation`的接口。
那么接口怎么能够设置属性呢?简单来说就是java通过动态代理的方式为你生成了一个实现了"接口" `TestAnnotation`的实例(对于当前的实体来说,例如类、方法、属性域等,这个代理对象是单例的),然后对该代理实例的属性赋值,这样就可以在程序运行时(如果将注解设置为运行时可见的话)通过反射获取到注解的配置信息。
具体来说是怎么实现的呢?
写一个使用该注解的类:
import java.io.IOException;
/**
* Created by Administrator on 2015/1/18.
*/
@TestAnnotation(count = 0x7fffffff)
public class TestMain {
public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException, IOException {
TestAnnotation annotation = TestMain.class.getAnnotation(TestAnnotation.class);
System.out.println(annotation.count());
System.in.read();
}
}
反编译一下这段代码:
Classfile /e:/workspace/intellij/SpringTest/target/classes/TestMain.class
Last modified 2015-1-20; size 1006 bytes
MD5 checksum a2d5367ea568240f078d5fb1de917550
Compiled from "TestMain.java"
public class TestMain
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#34 // java/lang/Object."<init>":()V
#2 = Class #35 // TestMain
#3 = Class #36 // TestAnnotation
#4 = Methodref #37.#38 // java/lang/Class.getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
#5 = Fieldref #39.#40 // java/lang/System.out:Ljava/io/PrintStream;
#6 = InterfaceMethodref #3.#41 // TestAnnotation.count:()I
#7 = Methodref #42.#43 // java/io/PrintStream.println:(I)V
#8 = Fieldref #39.#44 // java/lang/System.in:Ljava/io/InputStream;
#9 = Methodref #45.#46 // java/io/InputStream.read:()I
#10 = Class #47 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 LTestMain;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 annotation
#23 = Utf8 LTestAnnotation;
#24 = Utf8 Exceptions
#25 = Class #48 // java/lang/InterruptedException
#26 = Class #49 // java/lang/NoSuchFieldException
#27 = Class #50 // java/lang/IllegalAccessException
#28 = Class #51 // java/io/IOException
#29 = Utf8 SourceFile
#30 = Utf8 TestMain.java
#31 = Utf8 RuntimeVisibleAnnotations
#32 = Utf8 count
#33 = Integer 2147483647
#34 = NameAndType #11:#12 // "<init>":()V
#35 = Utf8 TestMain
#36 = Utf8 TestAnnotation
#37 = Class #52 // java/lang/Class
#38 = NameAndType #53:#54 // getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
#39 = Class #55 // java/lang/System
#40 = NameAndType #56:#57 // out:Ljava/io/PrintStream;
#41 = NameAndType #32:#58 // count:()I
#42 = Class #59 // java/io/PrintStream
#43 = NameAndType #60:#61 // println:(I)V
#44 = NameAndType #62:#63 // in:Ljava/io/InputStream;
#45 = Class #64 // java/io/InputStream
#46 = NameAndType #65:#58 // read:()I
#47 = Utf8 java/lang/Object
#48 = Utf8 java/lang/InterruptedException
#49 = Utf8 java/lang/NoSuchFieldException
#50 = Utf8 java/lang/IllegalAccessException
#51 = Utf8 java/io/IOException
#52 = Utf8 java/lang/Class
#53 = Utf8 getAnnotation
#54 = Utf8 (Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
#55 = Utf8 java/lang/System
#56 = Utf8 out
#57 = Utf8 Ljava/io/PrintStream;
#58 = Utf8 ()I
#59 = Utf8 java/io/PrintStream
#60 = Utf8 println
#61 = Utf8 (I)V
#62 = Utf8 in
#63 = Utf8 Ljava/io/InputStream;
#64 = Utf8 java/io/InputStream
#65 = Utf8 read
{
public TestMain();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTestMain;
public static void main(java.lang.String[]) throws java.lang.InterruptedException, java.lang.NoSuchFieldException, java.lang.IllegalAccessException, java.io.IOException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #2 // class TestMain
2: ldc #3 // class TestAnnotation
4: invokevirtual #4 // Method java/lang/Class.getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
7: checkcast #3 // class TestAnnotation
10: astore_1
11: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_1
15: invokeinterface #6, 1 // InterfaceMethod TestAnnotation.count:()I
20: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
23: getstatic #8 // Field java/lang/System.in:Ljava/io/InputStream;
26: invokevirtual #9 // Method java/io/InputStream.read:()I
29: pop
30: return
LineNumberTable:
line 10: 0
line 11: 11
line 12: 23
line 13: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
11 20 1 annotation LTestAnnotation;
Exceptions:
throws java.lang.InterruptedException, java.lang.NoSuchFieldException, java.lang.IllegalAccessException, java.io.IOException
}
SourceFile: "TestMain.java"
RuntimeVisibleAnnotations:
0: #23(#32=I#33)
最后一行的代码说明,注解`TestAnnotation`的属性设置是在编译时就确定了的。(对属性的说明在[这里][1])。
然后,运行上面的程序,通过CLHSDB在eden区找到注解实例,
hsdb> scanoops 0x00000000e1b80000 0x00000000e3300000 TestAnnotation
0x00000000e1d6c360 com/sun/proxy/$Proxy1
类型`com/sun/proxy/$Proxy1`是jdk动态代理生成对象时的默认类型,其中`com.sun.proxy`是默认的包名,定义于`ReflectUtil`类的`PROXY_PACKAGE`字段中。代理类名`$PROXY1`包含两部分,其中前缀`$PROXY`是jdk种默认的代理类类名前缀(参见`java.lang.reflect.Proxy`类的javadoc),后的1是自增的结果。
下面看一下这个代理类的内容。运行java程序时添加参数`-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true`可以将转储出jdk动态代理类的class文件。若是项目较大或是使用了各种框架的话,慎用此参数。
Classfile /e:/workspace/intellij/SpringTest/target/classes/com/sun/proxy/$Proxy1.class
Last modified 2015-1-19; size 2062 bytes
MD5 checksum 7321e44402258ba9e061275e313c5c9f
public final class com.sun.proxy.$Proxy1 extends java.lang.reflect.Proxy implements TestAnnotation
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_FINAL
...
太长了,只截取一部分。从中可以看到,这个代理类实现了继承自`java.lang.reflect.Proxy`类,又实现了“接口”TestAnnotation。
接下来查看一下代理对象的内容:
hsdb> inspect 0x00000000e1d6c360
instance of Oop for com/sun/proxy/$Proxy1 @ 0x00000000e1d6c360 @ 0x00000000e1d6c360 (size = 16)
_mark: 1
_metadata._compressed_klass: InstanceKlass for com/sun/proxy/$Proxy1
h: Oop for sun/reflect/annotation/AnnotationInvocationHandler @ 0x00000000e1ce7670 Oop for sun/reflect/annotation/Annota
tionInvocationHandler @ 0x00000000e1ce7670
其中,0xe1ce74e0是成员变量AnnotationInvocationHandler的地址(AnnotationInvocationHandler是定义在`java.lang.reflect.Proxy`类中的),通过查看类`AnnotationInvocationHandler`的源码可以知道注解的代理实例的值就存储在它的成员变量`memberValues`中,然后继续向下挖就好了:
hsdb> inspect 0x00000000e1ce7670
instance of Oop for sun/reflect/annotation/AnnotationInvocationHandler @ 0x00000000e1ce7670 @ 0x00000000e1ce7670 (size =
24)
_mark: 1
_metadata._compressed_klass: InstanceKlass for sun/reflect/annotation/AnnotationInvocationHandler
type: Oop for java/lang/Class @ 0x00000000e1ccc5f8 Oop for java/lang/Class @ 0x00000000e1ccc5f8
memberValues: Oop for java/util/LinkedHashMap @ 0x00000000e1ce7548 Oop for java/util/LinkedHashMap @ 0x00000000e1ce7548
memberMethods: null null
hsdb> inspect 0x00000000e1ce7548
instance of Oop for java/util/LinkedHashMap @ 0x00000000e1ce7548 @ 0x00000000e1ce7548 (size = 56)
_mark: 1
_metadata._compressed_klass: InstanceKlass for java/util/LinkedHashMap
keySet: null null
values: null null
table: ObjArray @ 0x00000000e1ce75b8 Oop for [Ljava/util/HashMap$Node; @ 0x00000000e1ce75b8
entrySet: null null
size: 1
modCount: 1
threshold: 1
loadFactor: 0.75
head: Oop for java/util/LinkedHashMap$Entry @ 0x00000000e1ce75d0 Oop for java/util/LinkedHashMap$Entry @ 0x00000000e1ce7
5d0
tail: Oop for java/util/LinkedHashMap$Entry @ 0x00000000e1ce75d0 Oop for java/util/LinkedHashMap$Entry @ 0x00000000e1ce75d0
accessOrder: false
hsdb> inspect 0x00000000e1ce75d0
instance of Oop for java/util/LinkedHashMap$Entry @ 0x00000000e1ce75d0 @ 0x00000000e1ce75d0 (size = 40)
_mark: 1
_metadata._compressed_klass: InstanceKlass for java/util/LinkedHashMap$Entry
hash: 94852264
key: "count" @ 0x00000000e1bd7c90 Oop for java/lang/String @ 0x00000000e1bd7c90
value: Oop for java/lang/Integer @ 0x00000000e1ce7630 Oop for java/lang/Integer @ 0x00000000e1ce7630
next: null null
before: null null
after: null null
hsdb> inspect 0x00000000e1ce7630
instance of Oop for java/lang/Integer @ 0x00000000e1ce7630 @ 0x00000000e1ce7630 (size = 16)
_mark: 1
_metadata._compressed_klass: InstanceKlass for java/lang/Integer
value: 2147483647
最后可以看到,key=“count”, value=Integer(2147483647 = 0x7fffffff),正是在TestMain中设置的值.
总结:
jvm对注解的支持,就是底层创建一个动态代理类,然后代理类中创建各个AnnotationInvocationHandler对象;每个AnnotationInvocationHandler对应一个我们声明的Annotation接口类,且AnnotationInvocationHandler收集对应注解接口类中的属性键值对。我们通过反射method.getAnnotation(**Annotation.class)或者field.getAnnotation(**Annotation.class),底层就是调用代理类,获取AnnotationInvocationHandler中的memberValues。