目录
注解本身是一种元数据机制,它提供了一种在代码中提供额外信息的方式,程序可以根据这些注解来执行特定的动作。对于注解的基本介绍在之前的文章里已经详细介绍了(Java注解总结),这里补充一下自定义注解的使用案例,其与命名方式相比的一些优势以及与标记接口的对比。
注解的使用
简单注解使用
考虑自定义一个Test注解来标注测试类中的测试方法。
1、首先定义一个自定义的Test注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
//@Interface关键字定义注解
//@Retention注解指定注解的保存策略
//@Target注解指定注解的应用范围
2、然后将注解应用在测试类的测试方法上
public class Sample{
@Test
public static void m1(){};
@Test
public static void m2(){
throw new RuntimeException("m2 failed");
};
@Test
public void m3(){};
@Test
public static int m4(int input){
return input;
}
}
//这里Sample类中定义了四个方法:
//m1:公共静态方法
//m2:抛出异常的公共静态方法
//m3:公共非静态方法
//m4:有返回值的公共静态方法
3、通过反射机制调用有Test注解的方法
public class RunTest {
public static void main(String[] args) throws Exception{
int test=0;
int pass=0;
Class<?> testclass = Class.forName(args[0]);
for(Method m : testclass.getDeclaredMethods()){
if(m.isAnnotationPresent(Test.class)){
test++;
try{
m.invoke(null);
pass++;
}catch(InvocationTargetException wrappedExc){
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
}catch(Exception exc){
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", pass, test-pass);
}
}
要注意这里的反射调用方法没有实例和参数,因此默认调用静态方法。这也是书里为什么强调该注释方法只能用于静态方法的原因。
如果测试方法抛出异常(m2),反射机制会将它封装在InvocationTargetException中。如果抛出其他异常,说明该@Test标注的方法是无效方法(invoke方法无法调用)。
调用结果:
(base) MacBook-Pro:TestSample $ java RunTest Sample
public static void Sample.m2() failed: java.lang.RuntimeException: m2 failed
Invalid @Test: public void Sample.m3()
Invalid @Test: public static int Sample.m4(int)
Passed: 1, Failed: 3
带参数的注解使用
还是上面的例子,如果要求筛选出抛出特定异常的方法:
首先自定义ExceptionTest注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
注意,这里注解中多了一个value方法,它返回的是一个Exception的子类型。
然后重新将测试类的方法进行标注:在这里通过ExceptionTest筛选出抛出RuntimeException的方法。
public class TestWithInput{
@ExceptionTest(RuntimeException.class)
public static void m1(){};
@ExceptionTest(RuntimeException.class)
public static void m2(){
throw new RuntimeException("m2 failed");
};
@ExceptionTest(RuntimeException.class)
public void m3(){};
@ExceptionTest(RuntimeException.class)
public static int m4(int input){
return input;
}
}
最后就是基于方法反射的检测方法:
public class RunTestwithInput {
public static void main(String[] args) throws Exception {
int targetmethod = 0;
int passed = 0;
Class<?> claz = Class.forName(args[0]);
for(Method m : claz.getDeclaredMethods()){
if(m.isAnnotationPresent(ExceptionTest.class)){
targetmethod++;
try{
m.invoke(null);
System.out.printf("Test %s passed%n", m);
}catch(InvocationTargetException WrappedEx){
Throwable ex = WrappedEx.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
if(excType.isInstance(ex)){
passed++;
System.out.printf("Test %s throws targeted Exception: %s%n", m, ex.getClass().getName());
}else{
System.out.printf("Test %s failed: expected %s, got %s%n", m, excType.getName(), ex.getClass().getName());
}
}catch(Exception ex){
System.out.println("INVALID @Test: " + m);
}
}
}
}
}
//这里通过try...catch进行三种情况的处理:
//1. 测试方法正常执行:显示Test Passed;
//2. 测试方法有效,但是抛出异常,这里就是检测抛出的异常是否是筛选的特定异常;
//3. 测试方法无效,显示Invalid。
执行的结果如下:通过ExceptionTest注解筛选出了有效方法中的 RuntimeException。
(base) MacBook-Pro:TestSample $ java RunTestwithInput TestWithInput
Test public static void TestWithInput.m2() throws targeted Exception: java.lang.RuntimeException
Test public static void TestWithInput.m1() passed
INVALID @Test: public void TestWithInput.m3()
INVALID @Test: public static int TestWithInput.m4(int)
多值注解的使用
多值注解即允许给同一个对象进行多次注释的注解。这可以让对象带上多个注解对象。比如上面的异常检测注解,如果想同时筛选出NullPointerException和IndexOutOfBoundsException,就需要用到多值注解:
首先定义注解ExceptionTest和其对应的容器类ExceptionTestContainer,然后通过@Repeatable注解将两者进行关联。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTestContainer{
ExceptionTest[] value();
}
之后就可以对目标方法进行多值注解:
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad(){...}
注解与命名模式相比的优势
了解了注解的使用后,就可以对比一下其与命名模式的优劣。命名模式就是通过在特定对象中留下特定名称来对对象进行标注的一种注释方式,在Junit4之前的单元测试框架中比较常见。注解的优势主要有以下三条:
- 命名模式如果拼写出现错误,将会导致失败并且没有特定的提示,而注释名称标记错误会有注解不存在的错误提示。
- 命名模式一般只针对方法,如果要编写一个测试类,则要对所有的方法都要进行标注。而注解可以直接标注在类上。
- 命名模式无法让注释带有一个参数并使用到程序中,但是注解可以通过带参数注解的形式实现。
坚持使用@Override注解
这一条归根到底两句话,为什么对于父类方法的覆盖要使用Override:
- 可以分清楚重载和覆盖。(如果不注意导致两个方法名称一样,但是签名的输入不一样,不使用Override不会报任何错误,因为程序默认作为重载处理)
- 对于没有Override,但实际上覆盖了超类方法的情况会出告警,以避免无意识覆盖。
标记接口与注解的对比
标记接口是不包含方法声明的接口,它只是指明一个类实现了具有某种属性的接口,只是对类做的一个筛选。比如Serializable接口,通过这个接口的实现,类可以被允许写入ObjectOutPutStream当中。标记接口相对于注解有几个优势:
1、标记接口可以直接参与到程序的执行当中去,所以在编译时就能够捕捉到相关标注,而注解要到运行时才可以。
public class Person implements Serializable{...}
public class App {
public static void main(args[]){
Serializable OutPutperson = new Person(...);
}
}
2、标记接口可以作用于更精确的范围--特定类或者接口及其实例(而注解只能通过Element.Type.TYPE指定应用到类还是方法等等)。
但是注解有一个无可取代的优势,那就是只要是除了类与接口的注释(比如方法),只能使用注解。
所以,首先要看注解的对象,如果是类或者接口的注解要再考虑一下这个注解的类或者接口是要具体标记某些个特定实例还是整体的类,如果是前者,那就需要通过标记接口来实现。