Java注解看这一篇就够了

小tips:本文主要以创建自定义的注解为线索。看懂本文需要你懂一些Java的基本知识,如果你还不会Java。可以去我的知乎文章中查看:java从入门到入土

Java注解

JDK的元注解

@Retention

该注解用于修饰注解定义,指定呗修饰的注解可以保留多长事件。value的值只能是以下三个

  • RententionPolicy.CLASS:编译器将注解记录在class文件中。当运行Java程序时,JVM不可获取注解信息。这是默认值。

  • RententionPolicy.RUNTIME:编译器将注解记录在class文件中。当运行Java程序时,JVM可获取注解信息,程序可以通过反射获取该注解信息。

  • RententionPolicy.SOURCE:注解只保留在源代码中,编译器直接丢弃这种注解

1
2
3
//定义下面的@Testable注解保留到运行时
@Retention(value = RententionPolicy.RUNTIME)
public @interface Testable()

使用该注解时包含一个value成员变量,如果()中只有value,那么可以省略value =

1
2
3
//定义下面的@Testable注解将被编译器直接丢弃
@Retention(RententionPolicy.SOURCE)
public @interface Testable()

@Target

@Target也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元。该注解也包含一个名value的成员变量,该成员变量的值也只能是如下几个。

  • ElementType.ANNOTATION_TYPE:指定该策略的注解只能修饰注解
  • ElementType.CONSTRUCTOR:修饰构造器
  • ElementType.FIELD:修饰成员变量
  • ElementType.LOCAL_ VARIABLE:修饰局部变量
  • ElementType.METHOD:修饰方法定义
  • ElementType.PACKAGE:修饰包定义
  • ElementType. PARAMETER:修饰参数
  • ElementType. TYPE:指定该策略的注解可以修饰类、接口或枚举定义
1
2
@Target(ElementType.FIELD)
public @interface ActionListenerFor()

上面的代码用于指定该注解只能修饰成员变量

@Documented

该注解用于指定该元注解修饰的注解类将被javadoc工具提取成文档,如果定义注解类使用了的话,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。

@Inherited

该元注解指定被它修饰的注解将具有继承性——如果某个类使用被这个注解修饰的自定义注解,则子类将自动被同样的注解修饰。

自定义注解

定义注解

定义注解需要使用@interface关键字

1
2
3
4
5
6
7
//定义一个简单的注解类型
public @interface MyTest{
//定义两个成员变量的注解
//注解中的成员变量以方法的形式来定义
String name();
int age() default 10;
}

默认情况下,注解可用来修饰任何程序元素,而且你还可以在注解中自定义成员变量,可以给成员变量设置默认值。当你定义了成员变量的时候,所有不是默认的成员变量均需要显式地赋值。

1
2
3
4
5
6
7
8
public class Test{

//使用带成员变量的注解时,需要为成员变量赋值
@MyTest(name = "xxx",age =11)
public void info(){

}
}

如果为成员变量指定了值,则默认值不会起作用。

根据注解是否包含成员变量,可以把注解分为以下两类:

  • 标记注解:没有定义成员变量的注解。例如@Override
  • 元注解:包含成员变量的注解。

提取注解中的值

提取注解中的值需要一些Java反射的知识,如果你不清楚,可以看我的知乎:java反射

关于注解的反射方法:

方法名 描述
< A extends Annotation> A getAnnotation( Class< A> annotationClass) 返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null。
< A extends Annotation> A getDeclaredAnnotation( Class< A> annotationClass) 这是 Java 8 新增的方法,该方法尝试获取直接修饰该程序元素、 指定类型的注解。如果该类型的注解不存在,则返回 null。
Annotation[] getAnnotations() 返回 该 程序 元素 上 存在 的 所有 注解。
Annotation[] getDeclaredAnnotations() 返回直接修饰该程序元素的所有注解。
boolean isAnnotationPresent( Class< ? extends Annotation> annotationClass) 判断 该 程序 元素 上 是否 存在 指定 类型 的 注解, 如果 存在 则 返回 true, 否则 返回 false。
< A extends Annotation> A[] getAnnotationsByType( Class< A> annotationClass) 该方法的功能与前面介绍的 getAnnotation() 方法基本相似。但由于 Java 8 增加了重复注解功能,因此需要使用
< A extends Annotation> A[] getDeclaredAnnotationsByType( Class< A>annotationClass) 该方法的功能与前面介绍的 getDeclaredAnnotations() 方法基本相似。 但由于 Java 8 增加了重复注解功能, 因此需要使用该 方法获取直接修饰该程序元素、 指定类型的多个注解。

使用注解的实例,没有成员参数变量

自定义注解Testable,这个注解用于标记哪些方法是可测试的。

1
2
3
4
5
6
7
8
9
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Testable {
}

如下MyTest测试用例定义了8个方法,这些方法没有太大的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MyTest {

//使用注解指定该方法是可测试的
@Testable
public static void m1() {

}

public static void m2() {

}

//使用注解指定该方法是可测试的
@Testable
public static void m3() {
throw new IllegalArgumentException("参数出错了");
}

public static void m4() {

}

//使用注解指定该方法是可测试的
@Testable
public static void m5() {

}

public static void m6() {

}

//使用注解指定该方法是可测试的
@Testable
public static void m7() {
throw new RuntimeException("程序业务出现异常");
}

public static void m8() {

}
}

仅仅使用注解来标记程序元素对程序是不会有任何影响的,这也是Java注解的一条重要原则。

如下是处理注解的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.lang.reflect.Method;

public class ProcessorTest {
public static void process(String clazz) throws ClassNotFoundException{
int passed = 0;
int failed = 0;

//遍历clzz对应的类里所有的方法
for(Method m:Class.forName(clazz).getMethods()) {

//如果该方法使用了@Testable修饰
if (m.isAnnotationPresent(Testable.class)) {
try {
//调用m方法
m.invoke(null);
//测试成功,passed 计数器加1
passed++;
}catch(Exception ex) {
System.out.println("方法"+ m + "运行失败,异常:"+ex.getCause());

//测试出现异常,failed计数器加1
failed++;
}
}
}

//统计测试结果
System.out.println("共运行了:"+(passed+failed)
+"个方法,其中:\n"+"失败了:"+failed+"个,\n"
+"成功了:"+passed+"个!");
}
}

如下是主启动类

1
2
3
4
5
6
7
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {

//处理MyTest类
ProcessorTest.process("annotation_test.MyTest");
}
}

运行上面程序就可以看出来,程序中的@Testable起作用了,MyTest类里以@Testable注解修饰的方法都被测试了。

使用注解的实例,有成员参数变量

使用到了一些swing的包,看不懂也没关系,看个大概就行

定义了这个@ ActionListenerFor 之后,使用该注解时需要指定一个 listener 成员变量,该成员变量用于指定监听器的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.awt.event.ActionListener;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {

//定义一个成员变量,用于设置元数据
//该listener 成员变量用于保存监听器实现类
Class<? extends ActionListener> listener();

}

如下是主要启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class AnnotationTest {
private JFrame mainWin = new JFrame("使用注解绑定事件监听器");

//使用注解为OK按钮绑定事件监听器
@ActionListenerFor(listener = OkListener.class)
private JButton ok = new JButton("确定");

//使用注解为cancel按钮绑定事件监听器
@ActionListenerFor(listener = CancelListener.class)
private JButton cancel = new JButton("取消");

public void init() {

//初始化界面的方法
JPanel jp = new JPanel();
jp.add(ok);
jp.add(cancel);
mainWin.add(jp);

//注解处理方法
ActionListenerInstaller.processAnnotations(this);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

public static void main(String[] args) {
new AnnotationTest().init();
}
}

//定义ok按钮的事件监听器实现类
class OkListener implements ActionListener{

@Override
public void actionPerformed(ActionEvent evt) {
// TODO Auto-generated method stub
JOptionPane.showMessageDialog(null, "单击了确认按钮");
}

}

//定义cancel按钮的事件监听器实现类
class CancelListener implements ActionListener{

@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
JOptionPane.showMessageDialog(null, "单击了取消按钮");
}

}

如下是注解处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.awt.event.ActionListener;
import java.lang.reflect.Field;

import javax.swing.AbstractButton;

public class ActionListenerInstaller {

//处理注解方法,其中obj是包含注解的对象
public static void processAnnotations(Object obj) {
try {

//获取obj对象的类
Class cl = obj.getClass();

//获取指定obj对象的所有成员变量,并遍历每个成员变量
for(Field f : cl.getDeclaredFields()) {

//将该成员变量设置成可自由访问
f.setAccessible(true);

//获取该成员变量上的注解
ActionListenerFor a = f.getAnnotation(ActionListenerFor.class);

//获取成员变量f的值
Object fObj = f.get(obj);

//如果f是AbstractButton的实例,且a不为null
if(a != null && fObj != null && fObj instanceof AbstractButton) {
//获取a注解里的listener元数据
Class<? extends ActionListener> listenerClazz = a.listener();

//使用反射来创建listener类的对象
ActionListener al = listenerClazz.getDeclaredConstructor().newInstance();
AbstractButton ab = (AbstractButton)fObj;
ab.addActionListener(al);
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
}

运行AnnotationTest程序,会看到窗口。点击确定会弹出“单击确定按钮对话框”,点击取消也是如此。

重复注解

Java8之前,同一个程序元素最多只能使用同一个相同类型的注解;如果需要使用多个相同类型的注解,就必须使用注解容器,例如以下例子。

1
2
3
4
5
@Result({
@Result(name = "failure",location = "failed.jsp"),
@Result(name = "success",location = "succ.jsp")
})
public Action FooAction{...}

但是在Java8以后,可以通过使用@Repeatable修饰该注解就可以使用简化的写法。

首先需要定义一个自定义的注解,其中@Repeatable中的FkTags.class在后面定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag {

//为该注解定义2个成员变量
String name() default "疯狂软件";
int age();
}

定义上面注解的容器注解

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTags {

//定义value变量,该成员变量
FkTag[] value();
}

测试注解类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FkTag(age = 5)
@FkTag(name = "疯狂Java",age = 9)
public class FkTagTest {
public static void main(String[] args) {
Class<FkTagTest> clazz = FkTagTest.class;

/* 使用Java8新增的getDeclaredAnnotationsByType()方法获取
* 修饰FkTagTest类的多个@FkTag注解
*/
FkTag[] tags = clazz.getDeclaredAnnotationsByType(FkTag.class);

//遍历修饰FkTagTest类的多个@FkTag注解
for(FkTag tag:tags) {
System.out.println(tag.name()+"-->"+tag.age());
}

/*使用传统的getDeclaredAnnotation()方法获取
* 修饰FkTagTest类 的@KfTags注解
*/
FkTags container = clazz.getDeclaredAnnotation(FkTags.class);
System.out.println(container);
}
}

实际上重复注解只是一种简化写法,多个重复注解其实会作为容器注解的value成员对象的数组元素。而且容器注解的保留期必须比它所包含的注解的保留期更长,否则编译器会报错,这很容易理解。两者应该是同时存在的.

类型注解

Java8开始,类型注解可以修饰在任何出现的类型。比如,允许在如下位置使用类型注解.

  • 创建对象(使用new关键字创建)
  • 类型转换
  • 使用implements实现接口
  • 使用throws声明抛出异常

上面这些都会用到类型,因此都可以使用类型注解来修饰。因为Java8为ElementType枚举增加了TYPE_ PARAMETER、 TYPE_ USE 两个枚举值,所以可以使用元注解@Target来修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

import javax.swing.JFrame;

// 定义一个简单的类型注解,不带任何变量
@Target(ElementType.TYPE_USE)
@interface NotNull{ }

//定义类时使用类型注解
public class TypeAnnotationTest implements @NotNull Serializable{

//方法形参中使用类型注解
public static void main(@NotNull String[] args)
//使用throws时使用类型注解
throws @NotNull FileNotFoundException{

Object obj = "fkjava.rog";

//强制类型转换时使用类型注解
String str = (@NotNull String)obj;

//创建对象时使用类型注解
Object win = new @NotNull JFrame("疯狂得一批");
}
}

上面的程序中,导出写满了类型注解,不过需要注意得是,上面程序虽然大量使用了@NotNull注解,但是这些注解并不会起任何做哟个,因为并没有对类型注解本身执行检查得框架,如果需要这些注解发挥作用,则需要开发者自己实现类型注解检查框架。

编译时处理注解

APT是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含得注解信息,然后针对注解信息进行额外的处理。

Java提供的javac.exe工具有-processor选项,该选项可指定一个注解处理器,如果在编译Java源文件时通过该选项指定了注解处理器,那么这个注解处理器将会在编译时提取并处理Java源文件中的注解。

每个处理器都需要实现javax. annotation. processing 包下的 Processor 接口。不过实现该接口必须实现它里面所有的方法,因此通常会采用 继承 AbstractProcessor 的方式来实现注解处理器。一个注解处理器可以处理一种或者多种注解类型。

下面程序实现了,当我们运行一个类时候,生成相应的xml文件。下面的代码中可能会需要使用到JavaIO的知识,假如你不清楚,可以先看看我之前的文章:JavaIO

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Persistent {
String table();
}

这自定义注解指定在源代码中保留,运行时不能通过反射来读取该注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Id {
String column();
String type();
String generator();
}

上面的注解只是多了两个成员变量而已,并且限定了是修饰普通成员属性

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Property {
String column();
String type();
}

和上面的注解相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Persistent(table = "person_inf")
public class Person {
@Id(column = "person_id",type = "integer",generator = "identity")
private int id;

@Property(column = "perseon",type = "string")
private String name;

@Property(column = "person_age",type = "integer")
private int age;


public Person() {

}

public Person(int id,String name,int age) {
this.id=id;
this.name=name;
this.age=age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}

}

上面的Person是一个普通的Java类,但是因为有我们的注解,所以它变得不简单了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
//指定可处理之前定义的三个注解
@SupportedAnnotationTypes({"Persistent","Id","Property"})
public class HibernateAnnotationProcessor extends AbstractProcessor{

//循环处理每个需要处理的程序对象
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

//定义一个文件输出流,用于生成额外的文件
PrintStream ps =null;
try {

//遍历每个背@Persistent修饰的class文件
for(Element t:roundEnv.getElementsAnnotatedWith(Persistent.class)) {

//获取正在处理的类名
Name clazzName = t.getSimpleName();

//获取类定义前的Persistent注解
Persistent per = t.getAnnotation(Persistent.class);

//创建文件输出流
ps = new PrintStream(new FileOutputStream(clazzName+".hbm.xml"));

//执行输出
ps.println("<?xml version=\"1.0\"?>");
ps.println("<!DOCTYPE hibernate-mapping PUBLIC>");
ps.println(" \"-//Hibernate/Hibernate"+"Mapping DTD 3.0//EN\"");
ps.println(" \"http://www.hibernate.org/dtd/"+"hibernate-mapping-3.0.dtd\">");
ps.println("<hibernate-mapping>");
ps.println(" <class name=\""+t);

//输出per的table()的值
ps.println("\"table=\""+per.table()+"\">");
for(Element f:t.getEnclosedElements()) {

//只处理成员变量上的注解
if(f.getKind()==ElementKind.FIELD) {

//获取成员变量定义的@Id注解
Id id = f.getAnnotation(Id.class);

//当@Id注解存在时输出<id.../>元素
if(id !=null) {
ps.println(" <id name=\""
+f.getSimpleName()
+"\"column=\""+id.column()
+"\"type=\""+id.type()
+"\">");
ps.println(" <generator class=\""
+id.generator()+"\"/>");
ps.println(" </id>");
}

//获取成员变量定义前的@Property注解
Property p = f.getAnnotation(Property.class);

//当这个注解存在时输出对应的元素
if(p !=null) {
ps.println(" <property name=\""
+f.getSimpleName()
+"\"column=\""+p.column()
+"\"type=\""+p.type()
+"\">");
}
}
}
ps.println("</class>");
ps.println("</hibernate-mapping>");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(ps!=null) {
try {
ps.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
return true;
}

}

上面的程序是本次功能的核心,这是我们的注解解析类,这个注解处理器使用 RoundEnvironment 来获取注解信息, RoundEnvironment 里包含了一个 getElementsAnnotatedWith() 方法, 可根据注解获取需要处理的程序单元, 这个程序单元由 Element 代表。Element里包含 一个getKind() 方法, 该方法返回 Element 所代表的程序单元,返回值可以是 ElementKind. CLASS( 类)、 ElementKind. FIELD( 成员 变量)……

除此之外, Element 还包含一个 getEnclosedElements() 方法,该方法可用 于获取该 Element 里定义的所有程序单元,包括成员变量、方法、构造器、 内部类等。接下来程序只处理成员变量前面的注解,因此程序先判断这个Element 必须是 ElementKind. FIELD。再接下来程序调用了 Element 提供 的 getAnnotation( Class clazz)方法来获取修饰该 Element 的注解,获取到成员变量上的@ Id、@ Property 注解之后,接下来就根据它们提供的信息执行输出。

接下来就可以使用javac来编译Person.java了

1
2
3
4
# 首先需要编译我们的注解解析类
javac -d . HibernateAnnotationProcessor.java
# 然后再解析Person类
javac -processor HibernateAnnotationProcessor Person.java

这样当生成Person的class文件时,还会生成一个Person.hbm.xml文件。