JAVA-注解 Annotation
sschrodinger
2018/6/4
基本
注解
Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。
注解有许多用处,主要如下:
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取
注解如同标签!
注解通过如下代码定义
/**
* 通过关键词 “@interface” 定义
* 类似于接口定义但是不要忘记 @ 符号
*/
public @interface TestAnnotation {
}
然后通过如下方式使用
@TestAnnotation
public class Test {
}
元注解
元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
java 规定了5种元注解,分别是@Retention
、@Documented
、@Target
、@Inherited
、@Repeatable
。
其中@Retention
规定了你所定义的注解的持续时间,@Retention
总共有3个取值,如下:
@Retention(RetentionPolicy.SOURCE) //注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
@Retention(RetentionPolicy.CLASS) //注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
@Retention(RetentionPolicy.RUNTIME)
//注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
举个例子:
/**
*下面这个例子规定了TestAnnontation这个注解的持续时间为到程序运行时
*
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
在实现过程中,这三种注解的区别在于生成的 class 文件不同。如下例子:
@Retention(RetentionPolicy.SOURCE)
@interface InSource {}
@Retention(RetentionPolicy.CLASS)
@interface InClass {}
@Retention(RetentionPolicy.RUNTIME)
@interface InRuntime {}
class Sample {
@InSource public static void m1() {}
@InClass public static void m2() {}
@InRuntime public static void m3() {}
}
反汇编以上代码,如下所示:
class test.Sample
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // test/Sample
#2 = Utf8 test/Sample
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ltest/Sample;
#14 = Utf8 m1
#15 = Utf8 m2
#16 = Utf8 RuntimeInvisibleAnnotations
#17 = Utf8 Ltest/InClass;
#18 = Utf8 m3
#19 = Utf8 RuntimeVisibleAnnotations
#20 = Utf8 Ltest/InRuntime;
#21 = Utf8 SourceFile
#22 = Utf8 RunTests.java
{
test.Sample();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 41: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/Sample;
public static void m1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 54: 0
LocalVariableTable:
Start Length Slot Name Signature
public static void m2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
RuntimeInvisibleAnnotations:
0: #17()
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 55: 0
LocalVariableTable:
Start Length Slot Name Signature
public static void m3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
RuntimeVisibleAnnotations:
0: #20()
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 56: 0
LocalVariableTable:
Start Length Slot Name Signature
}
我们发现有效期是在源码的m1方法的注解被完全擦除,m2 方法有一个 RuntimeInvisibleAnnotations,代表在运行期不可见的注解,m3 方法有一个 RuntimeVisibleAnnotations,代表在运行期可见的注解,在运行期可见的注解可以通过反射来发现和使用。
@Documented
注解将注解中元素包含在文档中去。
@Target
注解指定了注解用到的地方。他总共有8个值可选。值如下:
@TargetElementType.ANNOTATION_TYPE) //可以给一个注解进行注解
@TargetElementType.CONSTRUCTOR) //可以给构造方法进行注解
@TargetElementType.FIELD) //可以给属性进行注解
@TargetElementType.LOCAL_VARIABLE) //可以给局部变量进行注解
@TargetElementType.METHOD) //可以给方法进行注解
@TargetElementType.PACKAGE) //可以给一个包进行注解
@TargetElementType.PARAMETER) //可以给一个方法内的参数进行注解
@TargetElementType.TYPE) //可以给一个类型进行注解,比如类、接口、枚举
@Inherited
注解指定了如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
@Repeatable
注解代表注解可重复使用,例子如下:
@interface Persons {
Person[] value();
}
@Repeatable(Persons.class)
@interface Person{
String role default "";
}
@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
}
注意上面的代码,@Repeatable
注解了 Person
。而 @Repeatable
后面括号中的类相当于一个容器注解。容器注解就是用来存放其它注解的地方。它本身也是一个注解。
我们再看看代码中的相关容器注解。
@interface Persons {
Person[] value();
}
按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable
注解过的注解数组,注意它是数组。
注解的属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。
举例如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
int id();
String msg();
}
上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。
赋值的方式是在注解的括号内以 value=”” 形式,多个属性之前用 ,隔开。
@TestAnnotation(id=3,msg="hello annotation")
public class Test {
}
当然,注解中属性可以有默认值,默认值需要用 default 关键值指定:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
public int id() default -1;
public String msg() default "Hi";
}
另外,如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内。
public @interface Check {
String value();
}
@Check("hi")
int a;
如果注解没有属性,则括号也可以省略
public @interface Perform {}
@Perform
public void testMethod(){}
java 预置注解
@Deprecated
这个元素是用来标记过时的元素,想必大家在日常开发中经常碰到。编译器在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素比如过时的方法、过时的类、过时的成员变量。
public class Hero {
@Deprecated
public void say(){
System.out.println("Noting has to say!");
}
public void speak(){
System.out.println("I have a dream!");
}
}
//...
//调用过程
Hero hero = new Hero();
hero.say();
hero.speak();
//...
在IDE中编辑如下:
image可以明显的看到加了
@Deprecated
的函数会被编译器警告。
@Override
用于在类中声明方法或其他要继承父类的方法。
@SuppressWarnings
用于阻止编译器警告。
@SafeVarargs
参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。
@SafeVarargs // Not actually safe!
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // Semantically invalid, but compiles without warnings
String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!
}
上面的代码中,编译阶段不会报错,但是运行时会抛出 ClassCastException 这个异常,所以它虽然告诉开发者要妥善处理,但是开发者自己还是搞砸了。
@FunctionalInterface
函数式接口注解。
函数式接口 (Functional Interface) 就是一个具有一个方法的普通接口。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
我们进行线程开发中常用的 Runnable 就是一个典型的函数式接口,上面源码可以看到它就被 @FunctionalInterface 注解。函数式接口可以很容易转换为 Lambda 表达式。
注解的提取
注解与反射
注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解。
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通过特定方法方法来获取 Annotation 对象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
public Annotation[] getAnnotations() {}
//前一种方法返回指定类型的注解,后一种方法返回注解到这个元素上的所有注解。
如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
@TestAnnotation()
public class Test {
public static void main(String[] args) {
boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
if ( hasAnnotation ) {
TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
System.out.println("id:"+testAnnotation.id());
System.out.println("msg:"+testAnnotation.msg());
}
}
}
属性、方法上的注解照样可以通过反射获得。
@TestAnnotation(msg="hello")
public class Test {
@Check(value="hi")
int a;
@Perform
public void testMethod(){}
@SuppressWarnings("deprecation")
public void test1(){
Hero hero = new Hero();
hero.say();
hero.speak();
}
public static void main(String[] args) {
boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
if ( hasAnnotation ) {
TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
//获取类的注解
System.out.println("id:"+testAnnotation.id());
System.out.println("msg:"+testAnnotation.msg());
}
try {
Field a = Test.class.getDeclaredField("a");
a.setAccessible(true);
//获取一个成员变量上的注解
Check check = a.getAnnotation(Check.class);
if ( check != null ) {
System.out.println("check value:"+check.value());
}
Method testMethod = Test.class.getDeclaredMethod("testMethod");
if ( testMethod != null ) {
// 获取方法中的注解
Annotation[] ans = testMethod.getAnnotations();
for( int i = 0;i < ans.length;i++) {
System.out.println("method testMethod annotation:"+ans[i].annotationType().getSimpleName());
}
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
}
}
}
如果一个注解要在运行时被成功提取,那么 @Retention(RetentionPolicy.RUNTIME) 是必须的。
注解的使用场景
注解一般使用APT(Annotation Processing Tool)解析并供给程序使用,下面的TestTool就是一个APT。
写一个java的测试类,以保证类没有错误。
测试注解如下:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Test() {
}
需要测试的类如下:
public class myExe {
@Test
public void expr(){
System.out.println("1234567890");
}
@Test
public void add(){
System.out.println("1+1="+1+1);
}
@Test
public void mult(){
System.out.println("3 x 5="+ 3*5);
}
}
最后写一个类用于测试代码
public class TestTool {
public static void main(String[] args) {
// TODO Auto-generated method stub
myExe testobj = new myExe();
Class clazz = testobj.getClass();
Method[] method = clazz.getDeclaredMethods();
//用来记录测试产生的 log 信息
StringBuilder log = new StringBuilder();
// 记录异常的次数
int errornum = 0;
for ( Method m: method ) {
// 只有被 @Test 标注过的方法才进行测试
if ( m.isAnnotationPresent( Test.class )) {
try {
m.setAccessible(true);
m.invoke(testobj, null);
} catch (Exception e) {
// TODO Auto-generated catch block
//e.printStackTrace();
errornum++;
log.append(m.getName());
log.append(" ");
log.append("has error:");
log.append("\n\r caused by ");
//记录测试过程中,发生的异常的名称
log.append(e.getCause().getClass().getSimpleName());
log.append("\n\r");
//记录测试过程中,发生的异常的具体信息
log.append(e.getCause().getMessage());
log.append("\n\r");
}
}
}
log.append(clazz.getSimpleName());
log.append(" has ");
log.append(errornum);
log.append(" error.");
// 生成测试报告
System.out.println(log.toString());
}
}
网友评论