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本身没有提供对类型注解执行检查的框架,因此如果需要让这些类型注解发挥作用,开发者需要自己实现类型注解检查框架。

posted @ 2020-04-18 18:32  小新和风间  阅读(217)  评论(0编辑  收藏  举报