14.3.3 使用注解的示例

14.3.3 使用注解的示例

下面分别介绍两个使用注解的例子,第一个注解@Testable没有任何成员变量,仅是一个标记注解,它的作用是标记哪些方法是可测试的。

程序 定义注解

1
2
3
4
5
6
7
8
9
import java.lang.annotation.*;

// 使@Retention指定注解的保留到运行时
@Retention(RetentionPolicy.RUNTIME)
// 使用@Target指定被修饰的注解可用于修饰方法
@Target(ElementType.METHOD)
// 定义一个标记注解,不包含任何成员变量,即不可传入元数据
public @interface Testable {
}

上面程序定义了一个@Testable注解,定义该注解时使用了@Retention@Target两个JDK的元注解,其中@Retention注解指定@Testable注解可以保留到运行时(JVM可以提取到该注解的信息),而@Target注解指定@Testable只能修饰方法。

上面的@Testable用于标记哪些方法是可测试的,该注解可以作为JUnit测试框架的补充,在JUnit框架中它要求测试用例的测试方法必须以test开头。如果使用@Testable注解,则可把任何方法标记为可测试的。

如下MyTest测试用例中定义了8个方法,这8个方法没有太大的区别,其中4个方法使用@Testable注解来标记这些方法是可测试的。

程序 使用注解

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
public class MyTest {
// 使用@Testable注解指定该方法是可测试的
@Testable
public static void m1() {
}

public static void m2() {
}

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

public static void m4() {
}

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

public static void m6() {
}

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

public static void m8() {
}
}

正如前面提到的,仅仅使用注解来标记程序元素对程序是不会有仼何影响的,这也是Java注解的条重要原则。为了让程序中的这些注解起作用,接下来必须为这些注解提供一个注解处理工具。

程序 反射运行注解修饰的方法

下面的注解处理工具会分析目标类,如果目标类中的方法使用了@Testable注解修饰,则通过反射来运行该测试方法。

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
import java.lang.reflect.*;

public class ProcessorTest {
public static void process(String clazz) throws ClassNotFoundException {
int passed = 0;
int failed = 0;
// 遍历clazz对应的类里的所有方法
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 + "个!");
}
}

ProcessorTest类里只包含一个process(String clazz)方法,该方法可接收一个字符串参数,该方法将会分析clazz参数所代表的类,并运行该类里使用@Testable修饰的方法

该程序的主类非常简单,提供主方法,使用ProcessorTest来分析目标类即可。

程序 测试类

1
2
3
4
5
6
public class RunTests {
public static void main(String[] args) throws Exception {
// 处理MyTest类
ProcessorTest.process("MyTest");
}
}

运行上面程序,会看到如下运行结果:

1
2
3
4
5
6
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.3\01>java RunTests
方法public static void MyTest.m3()运行失败,异常:java.lang.IllegalArgumentException: 参数出错了!
方法public static void MyTest.m7()运行失败,异常:java.lang.RuntimeException: 程序业务出现异常!
共运行了:4个方法,其中:
失败了:2个,
成功了:2个!

通过这个运行结果可以看出,程序中的@Testable起作用了,MyTest类里以@Testable注解修饰的方法都被测试了。

通过上面例子读者不难看出,其实注解十分简单,它是对源代码増加的一些特殊标记,这些特殊标记可通过反射获取,当程序获取这些特殊标记后,程序可以做出相应的处理(当然也可以完全忽略这些注解)。

使用注解来简化事件编程

前面介绍的只是一个标记注解,程序通过判断该注解存在与否来决定是否运行指定方法。下面程序通过使用注解来简化事件编程,在传统的事件编程中总是需要通过addActionListener()方法来为事件源绑定事件监听器,本示例程序中则通过@ActionListenerFor来为程序中的按钮绑定事件监听器。

程序 定义注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.*;
import java.awt.event.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
// 定义一个成员变量,用于设置元数据
// 该listener成员变量用于保存监听器实现类
Class<? extends ActionListener> listener();
}

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

下面程序使用@ActionListenerFor注解来为两个按钮绑定事件监听器。

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.*;
import javax.swing.*;

public class AnnotationTest {
private JFrame mainWin = new JFrame("使用注解绑定事件监听器");
// 使用@ActionListenerFor注解为ok按钮绑定事件监听器
@ActionListenerFor(listener = OkListener.class)
private JButton ok = new JButton("确定");
// 使用@ActionListenerFor注解为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 {
public void actionPerformed(ActionEvent evt) {
JOptionPane.showMessageDialog(null, "单击了确认按钮");
}
}

// 定义cancel按钮的事件监听器实现类
class CancelListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
JOptionPane.showMessageDialog(null, "单击了取消按钮");
}
}

上面程序中定义了两个JButton按钮,并使用@ActionListenerFor注解为这两个按钮绑定了事件监听器,使用@ActionListenerFor注解时传入了listener元数据,该数据用于设定每个按钮的监听器实现类
正如前面提到的,如果仅在程序中使用注解是不会起任何作用的,必须使用注解处理工具来处理程序中的注解。程序中①处代码使用了ActionListenerInstaller类来处理本程序中的注解,该处理器分析目标对象中的所有成员变量,如果该成员变量前使用了@ActionListenerFor修饰,则取出该注解中的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
import java.lang.reflect.*;
import java.awt.event.*;
import javax.swing.*;

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类型的注解
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注解里的listner元数据(它是一个监听器类)
Class<? extends ActionListener> listenerClazz = a.listener();
// 使用反射来创建listner类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton) fObj;
// 为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
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
import java.lang.reflect.*;
import java.awt.event.*;
import javax.swing.*;

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类型的注解
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注解里的listner元数据(它是一个监听器类)
Class<? extends ActionListener> listenerClazz = a.listener();
// 使用反射来创建listner类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton) fObj;
// 为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面程序中的两行粗体字代码根据@ActionListenerFor注解的元数据取得了监听器实现类,然后通过反射来创建监听器对象,接下来将监听器对象绑定到指定的按钮(按钮由被@ActionListenerFor修饰的Field表示)。
运行上面的AnnotationTest程序,会看到如图14.3所示的窗口

单击“确定”按钮,将会弹出如图14.4所示的“单击了确认按钮”对话框:

这表明使用该注解成功地为okcancel两个按钮绑定了事件监听器。