11.3 自定义注解
一、定义注解
1.1 简单定义语法
定义新的注解类型使用@interface关键字定义一个新的注解类型与定义一个新的接口非常像,如下代码可以定义一个简单的注解类型:
//定义一个简单的注解类型
public @interface Test
{}
1.2 注解用法和修饰范围
</font color=red>定义注解之后,可以在程序中在任何地方使用该注解,使用注解的语法非常类似于public、final这样的修饰符,注解可以修饰程序的任何元素(类、方法、变量、接口等)。通常注解另起一行且注解放在所有修饰符之前。如下程序所示:
//使用@Test修饰类
@Test
public class MyClass
{...}
1.3 注解中带成员变量
注解可带成员变量,成员变量在注解定义中以无形参的方法形式来声明,其方法名和返回值类型定义了该成员变量的名字和类型。如下定义一个有成员变量的注解:
public @interface MyTag
{
//定义两个成员变量
//注解中的成员变量以方法形式来定义
String name();
int age();
}
一旦为注解定义了成员变量之后,使用该注解时就可以为他的成员变量指定值,如下代码:
public class Test
{
//使用带有成员变量的注解时,需要为成员变量赋值
@MyTag(name='xx',age=6)
public void info()
{
...
}
...
}
也可以在定义注解成员变量时为其指定默认初始值,指定成员变量的初始值可使用default关键字。如下:
public @interface MyTag
{
//定义两个成员变量的初始值
String name() default "yeeku";
int age() default 32;
}
1.4 注解分类
根据注解是否包含成员变量,可以把注解分为两类:
★标记注解:没有定义成员变量的注解类型称为标记。这种注解进利用自身的存在与否来提供信息,如前面的@Override、@Test等注解
★元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以被称为元数据注解。
二、提取注解信息
使用注解修饰方法、类、成员变量等成员后,这些注解不会自己生效,必须有开发者提供相应的工具来提取并处理注解信息。
2.1 java.lang.annotation.Annotation接口的主要实现类
Java使用了java.lang.annotation.Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。Java 5在java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有几个实现类:
★Class:类定义
★Constructor:构造器定义
★Field:类的成员变量定义
★Method:类的方法定义
★Package:类的包定义
2.2 java.lang.reflect包介绍
java.lang.reflect包主要包含一些实现反射功能的工具类,从Java 5开始,java.lang.reflect包所提供的反射API增加了读取运行时注解的能力。只有定义注解时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时可见,JVM才会在装载*.class文件时读取保存在class文件中的注解信息。
2.3 AnnotatedElement接口
AnnotatedElement接口使所有程序元素(Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(Class、Method、Constructor等)之后,程序就可以调用该对象的如下几个方法来访问注解信息:
★A getAnnotation(ClassannotationClass):返回程序元素上存在的、指定类型的注解,如果该类型的注解不存在则返回null。
★A getDeclaredAnnotation(ClassannotationClass):这是Java 8新增的方法,该方法尝试获取直接修饰该程序元素的、指定类型的注解,如果该类型的注解不存在则返回null。
★Annotation[] getAnnotations():获取程序元素上存在的所有注解。
★Annotation[] getDeclaredAnnotation():返回直接修饰该程序元素的所有注解。
★boolean isAnnotationPresent(Class<? extends Annotation>annotationClass):判断该程序元素上是否存在指定类型的注解秒如果存在,则返回true,不存在则返回false。
★A[] getAnnotationsByType(Class annotationClass):该方法的功能与前面的getAnnotation()方法基本类似。但由于Java 8增加了重复注解的功能,因此使用该方法获取修饰该程序元素、指定类型的多个注解。
★A[] getDeclaredAnnotationsByType(Class annotationClass):该方法的功能与前面的getDeclaredAnnotation()方法基本类似。但由于Java 8增加了重复注解的功能,因此使用该方法获取直接修饰该程序元素、指定类型的多个注解。
下面定义了一个Test类的info方法,并获取info方法里的所有注解:
先自定义一个注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
//定义Testable注解将被javadoc工具提取
@Documented
@Inherited
public @interface Testable
{
String name() default "Java";
int age() default 12;
}
提出注解信息:
package section2;
import java.lang.annotation.Annotation;
class BaseClass
{
@SuppressWarnings(value = "unchecked")
public void info()
{
System.out.println("Base类中的info方法");
}
}
public class Test extends BaseClass
{
@Override//Override注解只保留到源代码中
@Testable
@Deprecated(since = "9", forRemoval = true)
public void info()
{
System.out.println("Test中的info()方法");
}
public static void main(String[] args)
throws Exception
{
//返回程序元素存在的、指定类型的注解
Annotation a1=Test.class.getMethod("info").getAnnotation(Testable.class);
System.out.println(a1);//@section2.Testable(name="Java", age=12)
Annotation a2=Test.class.getMethod("info").getAnnotation(SuppressWarnings.class);
System.out.println(a2);//null
//尝试获取直接修饰该程序元素、指定类型的注解
Annotation a3=Test.class.getMethod("info").getDeclaredAnnotation(SuppressWarnings.class);
System.out.println(a3);//null
Annotation a4=Test.class.getMethod("info").getDeclaredAnnotation(Testable.class);
System.out.println(a4);//@section2.Testable(name="Java", age=12)
//返回程序元素上所有注解
Annotation[] aArray=Test.class.getMethod("info").getAnnotations();
//遍历所有注解
for(var an:aArray)
{
System.out.println(an);
}
//@section2.Testable(name="Java", age=12)
//@java.lang.Deprecated(forRemoval=true, since="9")
//直接修饰该程序元素、指定类型的注解
Annotation[] aArray1=Test.class.getMethod("info").getDeclaredAnnotations();
for(var an:aArray1)
{
System.out.println(an);
}
//@section2.Testable(name="Java", age=12)
//@java.lang.Deprecated(forRemoval=true, since="9")
//判断是否存在指定类型的注解
System.out.println(Test.class.getMethod("info").isAnnotationPresent(Override.class));//false
System.out.println(Test.class.getMethod("info").isAnnotationPresent(Testable.class));//true
//如果需要获取某个注解里的元数据,则可以将注解强制类型转换为所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。
var tt=new Test();
Annotation[] annotation=tt.getClass().getMethod("info").getDeclaredAnnotations();
for(var tag:annotation)
{
//如果tag是Testable类型
if(tag instanceof Testable)
{
System.out.println("Tag is "+tag);//Tag is @section2.Testable(name="Java", age=12)
System.out.println("tag.name():"+((Testable)tag).name());//tag.name():Java
System.out.println("tag.age():"+((Testable)tag).age());//tag.age():12
}
}
}
}
三、使用注解的示例
3.1 标记注解
仅仅使用标记注解标记程序元素对程序不会有任何影响,这也是Java注解的重要原则。
第一个注解@Tagable没有任何成员变量,仅是一个标记注解,它的作用是标记哪些方法是可测试的:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//使用@Retention指定注解的保留到运行时
@Retention(RetentionPolicy.RUNTIME)
//使用@Taarget指定被修饰的注解可用于修饰方法
@Target(ElementType.METHOD)
public @interface Tagable
{}
如下的MyTag测试用例中定义了8个方法、这8个方法没有太大的区别,其中4个方法使用了@Tagable注解来标记这些方法是可测试的。
package section2;
public class TagTest
{
//使用@Tagable注解指定该方法是可测试的
@Tagable
public static void m1(){}
public static void m2(){}
//使用@Tagable注解指定该方法是可测试的
@Tagable
public static void m3()
{
throw new IllegalArgumentException("参数出错了!");
}
public static void m4(){}
//使用@Tagable注解指定该方法是可测试的
@Tagable
public static void m5(){}
public static void m6(){}
//使用@Tagable注解指定该方法是可测试的
@Tagable
public static void m7()
{
throw new RuntimeException("程序业务出现异常!");
}
public static void m8(){}
}
3.2 注解处理工具分析目标类
如果目标类中的方法使用了@Tagable修饰,则通过反射来允许该测试方法:
下面ProcessorTest类里包含一个process方法,该方法接受一个字符串参数,该方法分析clazz参数所代表的类,并运行该类里使用@Testable修饰的方法。
package section3;
import java.lang.reflect.Method;
public class ProcessorTest
{
public static void process(Object clazz)
{
int passed=0;
int failed=0;
//遍历clazz对应类里所有方法
for(Method m:clazz.getClass().getMethods())
{
//如果该方法使用了@Tagable修饰
if(m.isAnnotationPresent(Tagable.class))
{
try
{
//调用m方法
m.invoke(null);//对待有指定参数的只当对象调用由此Method对象表示的底层方法
//测试成功passed++
passed++;
}
catch (Exception ex)
{
System.out.println("方法"+m+"运行失败,异常:"+ex.getCause());
//测试出现异常,failed计数器加1
failed++;
}
}
}
//统计结果
System.out.println("共运行了"+(passed+failed)+"个方法,其中\n"+"失败了"+failed+"个方法\n"+"成功了"+passed+"个方法");
}
public static void main(String[] args)
throws Exception
{
var tag=new TagTest();
//处理TagTest类
ProcessorTest.process(tag);
}
}
方法public static void section3.TagTest.m3()运行失败,异常:java.lang.IllegalArgumentException: 参数出错了!
方法public static void section3.TagTest.m7()运行失败,异常:java.lang.RuntimeException: 程序业务出现异常!
共运行了4个方法,其中
失败了2个方法
成功了2个方法
通过上运行结果可以看出,程序中@Tagable起作用了,TagTest类里以@Tagable注解修饰的方法都被测试了。
四、重复注解
在Java 8以前同一个程序元素最多只能使用一个相同类型的注解;如果同一个元素前使用了多个相同类型的注解,则必须使用注解容器。
例如在Struts开发中,有时需要在Action类上使用多个@Result注解,在Java 8以前只能写成如下形式:
@Results({@Result(name="failure",location="failed.jsp"),
@Result(name="success",location=succ.jas)})
public Acton FooAction{...}
从Java 8以前,上面的语法可以得到简化:Java 8允许使用多个相同类型的注解来修饰同一个类,因此上面的代码可能简化为如下格式:
@Result(name="failure",location="failed.jsp"
@Result(name="success",location=succ.jas)
public Acton FooAction{...}
在传统语法中,必须使用@Resultls来包含多个@Result;在Java 8语法规范下,即可直接使用多个@Result修饰Action类。
开发重复注解需要使用@Repeatable修饰,下面通过实例来示范如何让开发重复注解,首先定义一个FkTag注解:
//指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTag
{
//为注解定义2个成员变量
String name() default "疯狂软件";
int age();
}
上面定义的注解还不能作为重复注解使用。为了将直接改造成重复注解,需要使用@Repeatable修饰该注解,使用@Repeatable时必须为value成员变量指定值,该成员变量的值应该是一个“容器”注解——该容器注解可包含多个@FkTag,因此还需要定义如下“容器注解”。
//指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTags
{
//定义value成员变量,该成员变量可以接受多个@FkTag注解
FkTag[] value();
}
上面FkTag[] value();定义了以FkTag[]类型的value成员变量,这意味着@FkTags注解的value成员变量可以接受多个@FkTag注解,因此@FkTags注解可作为@FkTag容器。
定义@FkTags注解时@Retention(RetentionPolicy.RUNTIME)指定了@FkTags注解保留到运行时,这是必须的。因为:@FkTag注解信息需要保留到运行时,如果@FkTags注解保留到源代码级别(RetentionPolicy.SOURCE)或类文件(RetentionPolicy.Class),将会导致@FkTags的保留期小于@FkTag的保留期,如果将多个@FkTag注解放入@FkTags中,若JVM丢弃了@FkTags注解,自然也就丢弃了@FkTag的信息。
注意:"容器"注解的保留期必须比它包含的注解保留期更长,否则将出现编译错误。
接下来程序可在定义@FkTag注解时添加@Repeatable(FkTags.class)修饰:
import java.lang.annotation.*;
// 指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag
{
// 为该注解定义2个成员变量
String name() default "疯狂软件";
int age();
}
经过上面的步骤就成功定义了一个重复注解:@FkTag。由于@FkTags具有容器注解,因此依然可以使用传统代码来使用该注解。
@FkTags({@FkTag(age=5),
@FkTag(name="疯狂Java",age=9)})
有由于@FkTag是重复注解,因此可以直接使用两个@FkTag注解,如下代码:
@FkTag(age=5)
@FkTag(name="疯狂Java",age=9)
第二种写法只是一种简化写法,系统任然将两个@FkTag注解作为@FkTags的value成员变量的数组元素。下面程序演示了重复注解的本质:
package section3;
@FkTag(age=5)
@FkTag(name="疯狂Java",age=9)
public class FkTagTest
{
public static void main(String[] args)
{
Class<FkTagTest> clazz=FkTagTest.class;
//使用Java 8新增的getDeclaredAnnotationsByType()获取修饰FkTagTest类的多个@FkTag注解
FkTag[] tags =clazz.getDeclaredAnnotationsByType(FkTag.class);
//遍历修饰FkTagTest类的多个@FkTag注解
for(var tag:tags)
{
System.out.println(tag.name()+"-->"+tag.age());
}
//使用传统方法getDeclaredAnnotion()方法获取修饰@FkTagTest类的@FkTags注解
FkTags constrainer=clazz.getDeclaredAnnotation(FkTags.class);
System.out.println(constrainer);
//getAnnotation()方法找不到重复注解
System.out.println(clazz.getAnnotation(FkTag.class));//null
System.out.println(clazz.getAnnotation(FkTags.class));//
}
}
疯狂软件-->5
疯狂Java-->9
@section3.FkTags(value={@section3.FkTag(name="疯狂软件", age=5), @section3.FkTag(name="疯狂Java", age=9)})
null
@section3.FkTags(value={@section3.FkTag(name="疯狂软件", age=5), @section3.FkTag(name="疯狂Java", age=9)}
getDeclaredAnnotationsByType()方法与传统的getDeclaredAnnotation()方法相同,只不过getDeclaredAnnotationsByType()是增强版,可以获取多个重复注解,而getDeclaredAnnotations()只能获取@FkTags注解,不能获取到FkTag注解。尽管代码中并未直接使用@FkTags修饰,但由于程序中有两个@FkTag修饰该类,因此系统会自动将两个@FkTag注解作为@FkTags的value成员变量的数组元素处理。
五、类型注解
Java 8为ElementType增加了TYPE_PARAMETER、TYPE_USE两种枚举值,这样就允许定义枚举类时,使用@Target(ElementType.TYPE_PARAMETER)修饰,这种注解被称为类型注解(Type Annotion)。类型注解可用于修饰任何地方出现的类型。
在Java 8以前只能定义在各种程序元素(定义类、接口、方法、成员变量...)时使用注解。从Java 8开始类型注解可以修饰任何地方出现的类型。比如,允许在如下未知使用类型注解:
(1)创建对象(用new关键字)
(2)类型转换
(3)使用implements实现接口
(4)使用throws声明抛出异常
这些情形都会用到类型,因此可以使用类型注解来修饰。
下面定义一个简单的类型注解,然后可以在任何用到类型的地方使用类型注解:
package section3;
import javax.swing.*;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.lang.annotation.*;
import java.util.List;
//定义一个简单的类型注解
@Target(ElementType.TYPE_USE)
@interface NotNUll{}
//定义类时使用类型注解
@NotNUll
public class TypeAnnotionTest implements Serializable//implements时使用类型注解
{
//方法形参中使用类型注解
public static void main(@NotNUll String[] args)
//throws使用类型注解
throws @NotNUll FileNotFoundException
{
Object obj="fkjava.org";
//强制类型转换时使用类型注解
String str=(@NotNUll String) obj;
//创建对象时使用类型注解
Object win=new JFrame("疯狂软件");
}
//泛型中使用类型注解
public void foo(List<@NotNUll String> info){}
}
从这个示例可以看出Java程序到处“写满”了类型注解,这种无处不在的类型注解可以让编译器执行更加严格的代码检查,从而提高程序的健壮性。
上面虽然使用了大量的@NotNull注解,但这些注解不会起作用——因为没有为这些注解提供工具。而且Java 8本身没有提供对类型注解执行检查的框架,因此如果需要让这些类型注解发挥作用,开发者需要自己实现类型注解检查框架。