• Java的反射、依赖注入和注解

https://gitee.com/oliverwyydp

https://github.com/oliverwy

反射(Reflection)是程序的自我分析能力,通过反射可以确定类有哪些方法、有哪些构造方法以及有哪些成员变量。Java语言提供了反射机制,通过反射机制能够动态读取一个类的信息;能够在运行时动态加载类,而不是在编译期。反射可以应用于框架开发,它能够从配置文件中读取配置信息动态加载类、创建对象,以及调用方法和成员变量。

提示 Java反射机制在一般的Java应用开发中很少使用,即便是Java EE阶段也很少使用。除非你为了开发一个框架或出于兴趣对反射机制感兴趣,否则可以跳过本章内容。

2.1 Java反射机制API

Java反射机制API主要是 java.lang.Class类和java.lang.reflect包。

2.1.1 java.lang.Class

java.lang.Class类是实现反射的关键所在,Class类的一个实例表示Java的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和voidvoid无类型,主要用于方法返回值类型声明,表示不需要返回值。Class没有公有的构造方法,Class实例是由JVM在类加载时自动创建的。

在程序代码中获得Class实例可以通过如下代码实现;

//1.通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
//2.通过对象的getClass()方法
Class clz2 = str.getClass();

每一种类型包括类和接口等,都有一个class静态变量可以获得Class实例。另外,每一个对象都有getClass()方法可以获得Class实例,该方法是由Object类提供的实例方法。

Class类提供了很多方法可以获得运行时对象的相关信息,下面的程序代码展示了其中一些方法。

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 获得Class实例
        // 1.通过类型class静态变量
        Class clz1 = String.class;

        String str = "Hello";
        // 2.通过对象的getClass()方法
        Class clz2 = str.getClass();
        //获得int类型Class实例
        Class clz3 = int.class;                         
        //获得Integer类型Class实例
        Class clz4 = Integer.class;                    

        System.out.println("clz2类名称:" + clz2.getName());
        System.out.println("clz2是否为接口:" + clz2.isInterface());
        System.out.println("clz2是否为数组对象:" + clz2.isArray());
        System.out.println("clz2父类名称:" + clz2.getSuperclass().getName());

        System.out.println("clz2是否为基本类型:" + clz2.isPrimitive());
        System.out.println("clz3是否为基本类型:" + clz3.isPrimitive());
        System.out.println("clz4是否为基本类型:" + clz4.isPrimitive());

    }
}

运行结果如下:

clz2类名称:java.lang.String
clz2是否为接口:false
clz2是否为数组对象:false
clz2父类名称:java.lang.Object
clz2是否为基本类型:false
clz3是否为基本类型:true
clz4是否为基本类型:false
 

注意上述代码第行和第行的区别,intInteger的区别在于int是基本数据类型,所以输出结果为trueInteger是类,是引用类型。可见Class可以描述int等基本数据类型运行时实例。

2.1.2 java.lang.reflect

java.lang.reflect包提供了反射中用到类,主要的类说明如下:

Constructor类:提供类的构造方法信息。

Field类:提供类或接口中成员变量信息。

Method类:提供类或接口成员方法信息。

Array类:提供了动态创建和访问Java数组的方法。

Modifier类:提供类和成员访问修饰符信息。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class HelloWorld {
    public static void main(String[] args) {
        try {
            // 动态加载xx类的运行时对象
            Class c = Class.forName("java.lang.String");         
            // 获取成员方法集合
            Method[] methods = c.getDeclaredMethods();           
            // 遍历成员方法集合
            for (Method method : methods) {                      
                // 打印权限修饰符,如publicprotectedprivate
                System.out.print(Modifier.toString(method.getModifiers()));   
                // 打印返回值类型名称
                System.out.print(" " + method.getReturnType().getName() + " ")
                // 打印方法名称
                System.out.println(method.getName() + "();");              
            }
        } catch (ClassNotFoundException e) {                                          
            System.out.println("找不到指定类");
        }
    }
}

上述代码第行是通过Class的静态方法forName(String)创建某个类的运行时对象,其中的参数是类全名字符串,如果在类路径中找不到这个类则抛出ClassNotFoundException异常,见代码第行。

代码第行是通过Class的实例方法getDeclaredMethods()返回某个类的成员方法对象数组。代码第行是遍历成员方法集合,其中的元素是Method类型。

代码第行的method.getModifiers()方法返回访问权限修饰符常量代码,是int类型,例如1代表public,这些数字代表的含义可以通过Modifier.toString(int)方法转换为字符串。代码第行通过MethodgetReturnType()方法获得方法返回值类型,然后再调用getName()方法返回该类型的名称。代码第method.getName()返回方法名称。

2.2 创建对象

Java反射机制提供了另外一种创建对象方法,Class类提供了一个实例方法newInstance(),通过该方法可以创建对象,使用起来比较简单,下面两条语句实现了创建字符串String对象。

Class clz = Class.forName("java.lang.String");
String str = (String) clz.newInstance();
这两条语句相当于String str = new String()语句。另外,需要注意newInstance()方法有可以会抛出InstantiationExceptionIllegalAccessException异常,InstantiationException不能实例化异常,IllegalAccessException是不能访问构造方法异常。

2.2.1 调用构造方法

调用方法newInstance()创建对象,这个过程中需要调用构造方法,上面的代码只是调用了String的默认构造方法。如果想要调用非默认构造方法,需要使用Constructor对象,它对应着一个构造方法,获得Constructor对象需要使用Class类的如下方法:

  1. Constructor[] getConstructors():返回所有公有构造方法Constructor对象数组。
  2. Constructor[] getDeclaredConstructors():返回所有构造方法Constructor对象数组。
  3. Constructor getConstructor(Class... parameterTypes):根据参数列表返回一个共有Constructor对象。参数parameterTypesClass数组,指定构造方法的参数列表。
  4. Constructor getDeclaredConstructor(Class... parameterTypes):根据参数列表返回一个Constructor对象。参数parameterTypes同上。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Constructor;
public class HelloWorld {
    public static void main(String[] args) {
        try {
            Class clz = Class.forName("java.lang.String");
            // 调用默认构造方法
            String str1 = (String) clz.newInstance();                       

            // 设置构造方法参数类型
            Class[] params = new Class[1];                              
            // 第一个参数是String
            params[0] = String.class;                                   

            // 获取与参数对应的构造方法
            Constructor constructor = clz.getConstructor(params);           
            // 为构造方法传递参数
            Object[] argObjs = new Object[1];                            
            // 第一个参数传递"Hello"
            argObjs[0] = "Hello";                                      

            // 调用非默认构造方法,构造方法第一个参数是String类型
            String str2 = (String) constructor.newInstance(argObjs);         
            System.out.println(str2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上述代码第行是通过调用ClassnewInstance()方法创建String对象,这个过程中使用String的默认构造方法public String()

代码第~行通过反射机制调用Stringpublic String(String original)构造方法。代码第行和第行是设置构造方法参数类型,参数有可能有多个需要Class数组类型。代码第行是构造方法Constructor对象。代码第行和第行是为构造方法准备参数值,参数值放到Object数组中,与第行的参数类型是一一对应的。

代码第行是通过调用Constructor对象的newInstance(Object... initargs)方法创建String对象。

2.2.2 案例:依赖注入实现

Java反射机制能够在运行时动态加载类,而不是在编译期。在一些框架开发中经常将要实例化的类名保存到配置文件中,在运行时从配置文件中读取类名字符串,然后动态创建对象,建立依赖关系1。采用new创建对象依赖关系是在编译期建立的,反射机制能够将依赖关系推迟到运行时建立,这种依赖关系动态注入进来称为依赖注入。

依赖关系是一种非常普遍的关系,如果在A中使用了BB变化会引起A的变化,A依赖于B

例如:如4-1所示有三个类,StudentWorker继承自Person,在HelloWorld类的main()方法中会创建Person子类实例,至于依赖哪一个类,Student还是Worker,可以在运行时从配置文件中读取,然后创建对象。

2-1 Person继承类图

下面介绍一下这个案例实现过程,在Eclipse项目的根目录下创建一个文本文件 Configuration.ini,文件内容如下:

com.a51work6.Student

文件中只有一行字符串,前后没有空格,它是要配置的类全名,根据自己情况修改配置信息。

HelloWorld类代码如下:

//HelloWorld.java文件
package com.a51work6;

… …

public class HelloWorld {

    public static void main(String[] args) {

        try {
            String className = readClassName();                
            Class clz = Class.forName(className);             
            // 指定参数类型
            Class[] params = new Class[3];
            // 第一个参数是String
            params[0] = String.class;
            // 第二个参数是int
            params[1] = int.class;
            // 第三个参数是String
            params[2] = String.class;

            // 获取对应参数的构造方法
            Constructor constructor = clz.getConstructor(params);
            // 设置传递参数
            Object[] argObjs = new Object[3];
            // 第一个参数传递"Tony"
            argObjs[0] = "Tony";
            // 第二个参数传递18
            argObjs[1] = 18;
            // 第三个参数传递"清华大学"
            argObjs[2] = "清华大学";

            // 调用非默认构造方法
            Object p = constructor.newInstance(argObjs);        
            System.out.println(p);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Configuration.ini文件中读取类名
    public static String readClassName() {

        FileInputStream readfile = null;
        InputStreamReader ir = null;
        BufferedReader in = null;
        try {
            readfile = new FileInputStream("Configuration.ini");
            ir = new InputStreamReader(readfile);
            in = new BufferedReader(ir);
            // 读取文件中的一行数据
            String str = in.readLine();
            return str;
        } catch (FileNotFoundException e) {
            System.out.println("处理FileNotFoundException...");
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("处理IOException...");
            e.printStackTrace();
        }
        return null;
    }

}

上述代码第行通过调用readClassName()方法从Configuration.ini文件中读取类名,读取Configuration.ini文件内容,采用Java I/O技术,关于I/O流这里不再赘述。

代码第行通过从配置文件Configuration.ini中读取的字符串创建Class对象。代码第行是调用三个参数构造方法创建对象,这个对象是哪个类的实例,与你的Configuration.ini文件中配置字符串有关。

2.3 调用方法

通过反射机制还可以调用方法,这与调用构造方法类似。调用方法需要使用Method对象,它对应着一个方法,获得Method对象需要使用Class类的如下方法:

  1. Method[] getMethods():返回所有公有方法Method对象数组。
  2. Method[] getDeclaredMethods():返回所有方法Method对象数组。
  3. Method getMethod(String name, Class... parameterTypes):通过方法名和参数类型返回公有方法Method对象。参数parameterTypesClass数组,指定方法的参数列表。
  4. Method getDeclaredMethod(String name, Class... parameterTypes):通过方法名和参数类型返回方法Method对象。参数parameterTypes同上。

现有一个Person类,它的代码如下:

//Person.java文件
package com.a51work6;

public class Person {
    private String name;
    private int age;

    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;
    }

    public void setNameAndAge(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

现在编写一个程序通过反射机制调用PersonsetNameAndAgesetName方法,具体代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Method;
public class HelloWorld {
    public static void main(String[] args) {
        try {
            Class clz = Class.forName("com.a51work6.Person");
            // 调用默认构造方法
            Person person = (Person) clz.newInstance();
            System.out.println(person);

            // 指定参数类型
            Class[] params = new Class[2];
            // 第一个参数是String
            params[0] = String.class;
            // 第二个参数是int
            params[1] = int.class;

            // 获取setNameAndAge方法对象
            Method method = clz.getMethod("setNameAndAge", params);        
            // 设置传递参数
            Object[] argObjs = new Object[2];
            // 第一个参数传递"Tony"
            argObjs[0] = "Tony";
            // 第二个参数传递18
            argObjs[1] = 18;
            //调用setNameAndAge方法
            method.invoke(person, argObjs);                                
            System.out.println(person);

            // 获取getName方法对象
            method = clz.getMethod("getName");                            
            // 调用getName方法
            Object result = method.invoke(person);                           
            System.out.println(result);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果如下:

Person [name=null, age=0]
Person [name=Tony, age=18]
Tony

上述代码实现了调用PersonsetNameAndAgesetName方法。代码第行是获取setNameAndAge方法对象,params参数是指定参数类型,这个过程与构造方法类似。代码第method.invoke(person, argObjs)语句是调用setNameAndAge方法,person是要调用的对象,argObjs是设置要传递的参数值。

代码第行是获取getName方法对象,该方法没有参数。代码第method.invoke(person)语句是调用persongetName方法,invoke方法会返回一个Object对象,它是调用目标方法的返回数据,本例中相当于调用getName方法返回的String类型数据。

2.4 调用成员变量

通过反射机制还可以调用成员变量,调用方法需要使用Field对象,它对应着一个方法,获得Field对象需要使用Class类的如下方法:

  1. Field[] getFields():返回所有公有成员变量Field对象数组。
  2. Field[] getDeclaredFields():返回所有成员变量Field对象数组。
  3. Field getField(String name):通过指定公共成员变量名返回Field对象。
  4. Field getDeclaredField(String name):通过指定成员变量名返回Field对象。

现有一个Person类,它的代码如下:

//Person.java文件
package com.a51work6;

public class Person {
    private String name = "";
    private int age;
    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;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

Person类有两个私有的成员变量nameage

提示 Java的反射机制非常强大,可在类外部调用类的私有成员变量和成员方法。这种功能看似强大,事实上却破坏了面向对象封装性。

现在编写一个程序通过反射机制调用Person类的私有成员变量nameage,具体代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Field;

public class HelloWorld {
    public static void main(String[] args) {
        try {
            Class clz = Class.forName("com.a51work6.Person");
            // 调用默认构造方法
            Person person = (Person) clz.newInstance();

            // 返回成员变量名为nameField对象
            Field name = clz.getDeclaredField("name");             
            //设置成员变量accessible标志为true
            name.setAccessible(true);                            
            //为成员变量name赋值
            name.set(person, "Tony");                            

            // 返回成员变量名为ageField对象
            Field age = clz.getDeclaredField("age");                
            //设置成员变量accessible标志为true
            age.setAccessible(true);                             
            //为成员变量age赋值
            age.set(person, 18);                                 

            // 获取成员变量保存的数据
            System.out.printf("[name:%s, age:%d]",
                    name.get(person), age.get(person));          

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:

Person [name=, age=0]
Person [name=Tony, age=18]

上述代码第行和第行是通过名字返回成员变量Field对象,注意这里是有的getDeclaredField方法,而不是getField方法,因为这两个成员变量都是私有的。

代码第行和第行是设置成员变量accessible标志为trueaccessible是可访问性标志,值为 true 则指示反射的对象在使用时应该取消Java语言访问检查。值为false则指示反射的对象应该实施Java语言访问检查。不仅是成员变量,方法和构造方法也可以通过setAccessible(true)设置,实现对私有方法和构造方法的访问。

代码第行和第行是调用Fieldvoid set(Object obj, Object value)方法为成员变量赋值,其中obj要访问的目标对象,value是要赋给成员变量的数据。

代码第行通过调用FieldObject get(Object obj)方法获取成员变量保存的数据,其中obj要访问的目标对象,方法返回值是成员变量的保存的数据。

2.5 注解(Annotation

Java 5之后可以在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),例如在方法覆盖中使用过的@Override注解,注解都是@符号开头的。

提示 Annotation可以翻译为“注解”或“注释”,笔者推荐翻译为“注解”,因为“注释”一词已经用于说明//、/**...*/和/*...*/等符号,这里的“注释”是英文Comment翻译。

注解并不能改变程序运行的结果,不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有的注解可以在运行时读写字节码文件信息。

提示 使用注解对于代码实现功能没有任何的影响。程序员即便是不知道注解,也完全可以编写Java程序代码。

2.5.1 基本注解

无论是哪一种注解,本质上都一种数据类型,是一种接口类型。到Java 8为止Java SE提供11种内置注解。其中有5是基本注解,它们来自于java.lang包。有6个是元注解1Meta?Annotation),它们来自于java.lang.annotation包,自定义注解会用到元注解元注解就是负责注解其他的注解。

基本注解包括:@Override@Deprecated@SuppressWarnings@SafeVarargs@FunctionalInterface。下面逐一介绍一下。

@Override

@Override只能用于方法,子类覆盖父类方法(或者实现接口的方法)时可以@Override注解。编译器会检查被@Override注解的方法,确保该方法父类中存在的方法,否则会有编译错误。

使用@Override注解示例代码如下:

     //Person.java文件
     package com.a51work6;

     public class Person {

         private String name = "";
         private int age;

         ... ...

         @Override
         public String t0String() { //toString()        
             return "Person [name=" + name
                     + ", age=" + age + "]";
         }
     }
 

在代码第行的方法toString()是覆盖Object类的方法,该方法使用@Override注解。如果toString()被错误成了t0String(),那么程序会发生编译错误。会有如下的代码提示:

类型为 Person 的方法t0String()必须覆盖或实现超类型方法
注意 当然如果该方法前面不加@Override注解,即便是方法写错误了,也不会有编译错误,但是Object父类的toString()方法并没有被覆盖。这会引起程序出现Bug(缺陷)。

@Deprecated

@Deprecated用来指示API已经过时了,@Deprecated可以用来注解类、接口、成员方法和成员变量。

使用@Deprecated注解示例代码如下:

 //Person.java文件
package com.a51work6;
     @Deprecated
     public class Person{                                         
         @Deprecated
         protected String name;                                   ②
         private int age;
         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;
         }

         @Deprecated
         public void setNameAndAge(String name, int age) {         
             this.name= name;
             this.age= age;
         }

         @Override
         public String toString() {
             return "Person [name=" + name+ ", age=" + age + "]";
         }
     }

上述代码第行类Person、第行的成员变量name和第行的setNameAndAge方法都被@Deprecated注解。在Eclipse中这些被注解的API都画上删除线。调用这些API代码也会有删除线,示例代码如下。

     //HelloWorld.java文件
     package com.a51work6;

     public class HelloWorld {

         public static void main(String[] args) {
Person p = new Person();
            p.setNameAndAge("Tony", 20);
           p.name= "Tom";
         }
     }

不仅代码中有删除线,而且还有编译警告。

@SuppressWarnings

@SuppressWarnings注解用来抑制编译器警告,如果你确认程序中的警告没有问题,可以不用理会。但是就是不想看到这些警告,可以使用@SuppressWarnings注解消除这些警告。

使用@SuppressWarnings注解示例代码如下:

     //HelloWorld.java文件
     package com.a51work6;

     import java.util.ArrayList;
     import java.util.List;

     public class HelloWorld {

         @SuppressWarnings({ "deprecation" })            
         public static void main(String[] args) {

         Person p = new Person();                        
         p.setNameAndAge("Tony", 20);                  
         p.name= "Tom";                               
         }
     }

上述代码第行使用@SuppressWarnings({ "deprecation" })注解了main方法,这是由于代码第~行是编译警告,因为这些API已经过时,见上一节Person代码。@SuppressWarnings注解中的deprecation表示要抑制API已经过时。使用了@SuppressWarnings注解后会发现程序代码的警告没有了。

@SafeVarargs

在介绍@SafeVarargs注解用法之前,先来看看如下代码:

     //HelloWorld.java文件
     package com.a51work6;

     public class HelloWorld {

         public static void main(String[] args) {

             // 传递可变参数,参数是泛型集合
             display(10, 20, 30);                            
             // 传递可变参数,参数是非泛型集合
             display("10", 20, 30);// 会有编译警告            

         }

         public static <T> void display(T... array) {        
             for (T arg : array) {
                 System.out.println(arg.getClass().getName() + ":" + arg);
             }
         }
     }

代码第行声明了一种可变参数方法displaydisplay方法参数个数可以变化,它可以接受不确定数量的相同类型的参数。可以通过在参数类型名后面加入...的方式来表示这是可变参数。可变参数方法中的参数类型相同,为此声明参数是需要指定泛型。

但是调用可变参数方法时,应该提供相同类型的参数,代码第行调用时没有警告,而代码第行调用时则会发生警告,这个警告是unchecked“未检查不安全代码,就是由于将非泛型变量赋值给泛型变量所发生的。

那么如何抑制编译器警告可用使用@SafeVarargs注解,修改代码如下:

     //HelloWorld.java文件
     package com.a51work6;

     public class HelloWorld {

         public static void main(String[] args) {

             // 传递可变参数,参数是泛型集合
             display(10, 20, 30);
             // 传递可变参数,参数是非泛型集合
             display("10", 20, 30);// 没有@SafeVarargs会有编译警告

         }

         @SafeVarargs
         public static <T> void display(T... array) {
             for (T arg : array) {
                 System.out.println(arg.getClass().getName() + ":" + arg);
             }
         }
     }

在可变参数display前添加@SafeVarargs注解。当然可用也可以使用@SuppressWarnings("unchecked")注解,但@SafeVarargs注解更适合。

@FunctionalInterface

@FunctionalInterface注解是Java 8增加的,用于接口的注解,声明接口是函数式接口,在前面讲解Lambda表达式时介绍过函数式接口,有关@FunctionalInterface注解不再赘述。

5.2.2 元注解

元注解包括:@Documented@Target@Retention@Inherited@Repeatable@Native。元注解是为其他注解进行说明的注解,当自定义一个新的注解类型时,其中可以使用元注解。这一节先介绍一下这几种元注解含义,下一节在自定义注解中详细介绍它们的使用的。

@Documented

如果在一个自定义注解中引用@Documented注解,那么该注解可以修饰代码元素(类、接口、成员变量和成员方法等),javadoc等工具可以提取这些注解信息。

@Target

@Target注解用来指定一个新注解的适用目标。@Target注解有一个成员(value)用来设置适用目标,valuejava.lang.annotation.ElementType枚举类型的数组,ElementType描述Java程序元素类型,它有10个枚举常量,如表4-1所示。

2-1 ElementType枚举类型中的枚举常量

@Retention

@Retention注解用来指定一个新注解的有效范围,@Retention注解有一个成员(value)用来设置保留策略,valuejava.lang.annotation.RetentionPolicy枚举类型,RetentionPolicy描述注解保留策略,它有3个枚举常量,如表4-2所示。

2-2 RetentionPolicy枚举类型中的枚举常量

 

@Inherited

@Inherited注解用来指定一个新注解可以被继承。假定一个类A被该新注解修饰,那么这个A类的子类会继承该新注解。

@Repeatable

@Repeatable注解是Java 8新增加的,它允许在相同的程序元素中重复注释,可重复的注释必须使用@Repeatable进行注释。

@Native

@Native注解一个成员变量,指示这个变量可以被本地代码引用。常常被代码生成工具使用。

5.2.3 自定义注解

如果前面的Java SE提供的11内置注解不能满足你的需求,可以自定义注解,注解本质是一种接口,它是java.lang.annotation.Annotation接口的子接口,是引用数据类型。

5.2.3.1 声明注解

声明自定义注解可以使用@interface关键字实现,最简单形式的注解示例代码如下:

     // Marker.java文件
     package com.a51work6;

     public @interface Marker{

     }

上述代码声明一个Marker注解,@interface声明一个注解类型,它前面的访问限定修饰符与类一样有两种:公有访问权限和默认访问权限。

注意 关于注解源程序文件与类一样,一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的,源程序文件命名与公有访问权限的注解名一致。

Marker注解中不包含任何的成员,这种注解称为标记注解(Marked Annotation),基本注解中的@Override就属于标记注解。根据需要注解中可以包含一些成员,示例代码如下:

     //Marker.java文件
     package com.a51work6;
     //单值注解
     @interface MyAnnotation {
         String value();
     }

代码中声明MyAnnotation 注解,它有一个成员value,注意value后面是有一对小括号,value前面的是数据类型。成员也可以有访问权限修饰符,但是只能是公有权限和默认权限。

注解中的成员也可以有默认值,示例代码如下:

     //Marker.java文件
     package com.a51work6;

     //带有默认值注解
     @interface MyAnnotation1 {

         String value() default "注解信息";

         int count() default 0;
     }

通过关键字default指定默认值。使用这些注解示例代码如下:

     //HelloWorld.java文件
     package com.a51work6;

     @Marker                                        
     public class HelloWorld {

         @MyAnnotation(value = "Annotation")        
         private String info = "";

         @MyAnnotation1(count = 10)                 
         public static void main(String[] args) {

         }
     }

默认情况下注解可以修饰任意的程序元素(类、接口、成员变量、成员方法和数据类型等)。代码第行使用@Marker注解修饰类。代码第行是@MyAnnotation(value = "Annotation")注解修饰成员变量,其中value = "Annotation"是为value成员提供数值。代码第行是@MyAnnotation1(count = 10) 注解修饰成员方法,@MyAnnotation1有两个成员,但是只为count成员赋值,另外一个成员value使用默认值。

5.2.3.2 案例:使用元注解

上一节声明注解只是最基本形式的注解,对于复杂的注解可以在声明注解时使用元注解。下面通过一个案例介绍一下在自定义注解中使用元注解,在本案例中定义了两个注解。

首先看看第一个注解MyAnnotation,它用来修饰类或接口,MyAnnotation代码如下:

     //MyAnnotation.java文件
     package com.a51work6;

     import java.lang.annotation.ElementType;
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
     import java.lang.annotation.Documented;
     import java.lang.annotation.Target;

     @Documented                                 
     @Target({ ElementType.TYPE })                       
     @Retention(RetentionPolicy.RUNTIME)                  
     public @interface MyAnnotation {                 
         String description();                           
     }

上述代码第行是声明注解类型MyAnnotation,其中使用了三个元注解修饰MyAnnotation注解,代码第行使用@Documented指定MyAnnotation注解信息可以被javadoc工具读取。代码第行使用@Target({ ElementType.TYPE })指定MyAnnotation注解用于修饰类和接口等类型。代码第@Retention(RetentionPolicy.RUNTIME)指定MyAnnotation注解信息可以在运行时被读取。代码第行的descriptionMyAnnotation注解的成员。

提示 Eclipse工具不仅可以创建类和接口,还可以创建注解,在Eclipse中选择菜单文件”→“新建”→“注释,打开如图4-1所示对话框,可以根据自己的需要添加@Retention@Target@Documented注解。注意Eclipse工具汉化包将Annotation翻译为注释

2-2 Eclipse工具创建注解对话框

第二个注解MemberAnnotation,它用来类中成员变量和成员方法,MemberAnnotation代码如下:

     //MemberAnnotation.java文件
     package com.a51work6;

     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;

     @Documented
     @Retention(RetentionPolicy.RUNTIME)                   
     @Target({ ElementType.FIELD, ElementType.METHOD })    
     public @interface MemberAnnotation {                  
         Class<?> type() default void.class;               
         String description();                             
     }

上述代码第行是声明注解类型MemberAnnotation,其中也使用了三个元注解修饰MemberAnnotation注解,代码第行的@Retention(RetentionPolicy.RUNTIME)指定MemberAnnotation注解信息可以在运行时被读取。代码第@Target({ ElementType.FIELD, ElementType.METHOD })指定MemberAnnotation注解用于修饰类中成员。

代码第行和第行是声明两个成员,type类型是Class<?>,默认值是void.classvoid.classvoid类型表示方式。description类型是String,没有设置默认值。

提示 代码第行中Class<?>类型,表示Class的泛型,?是泛型通配符,可以是任何类型。泛型多数情况下尖括号中指定的都某个具体类型,泛型也是为此而设计的。但是有时确实不需要知道具体类型,或者说什么类型都可以,此时可以使用?作为占位符。

使用了MyAnnotationMemberAnnotation注解是Person类,Person类代码如下:

     //Person.java文件
     package com.a51work6;

     @MyAnnotation(description = "这是一个测试类")                           
     public class Person {

         @MemberAnnotation(type = String.class, description = "名字")       
         private String name;

         @MemberAnnotation(type = int.class, description = "年龄")           
         private int age;

         @MemberAnnotation(type = String.class, description = "获得名字")    
         public String getName() {
             return name;
         }

         @MemberAnnotation(type = int.class, description = "获得年龄")       
         public int getAge() {
             return age;
         }

         @MemberAnnotation(description = "设置姓名和年龄")                   
         public void setNameAndAge(String name, int age) {
             this.name = name;
             this.age = age;
         }

         @Override
         public String toString() {
             return "Person [name=" + name + ", age=" + age + "]";
         }
     }

使用注解时如果当前类与注解不在同一个包中,则需要将注解引入。代码第@MyAnnotation注解只能Person之前,修饰Person类型。代码第行和第行是使用@MemberAnnotation注解修饰成员变量。代码第行、第行和第行是使用@MemberAnnotation注解修饰成员方法。

5.2.3.3 案例:读取运行时注解信息

注解是为工具读取信息而准备的。有些工具可以读取源代码文件中的注解信息;有的可以读取字节码文件中的注解信息;有的可以在运行时读取注解信息。但是读取这些注解信息的代码都是一样的,区别只在于自定义注解中@Retention的保留策略不同。

读取注解信息需要反射相关APIClass类如下方法:

  1. <A extends Annotation> A getAnnotation(Class<A> annotationClass):如果此元素存在annotationClass类型的注解,则返回注解,否则返回null
  2. Annotation[] getAnnotations():返回此元素上存在的所有注解。
  3. Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与getAnnotations()区别在于该方法将不返回继承的注解
  4. boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):如果此元素上存在annotationClass类型的注解,则返回true,否则返回false
  5. boolean isAnnotation():如果此Class对象表示一个注解类型则返回true

读者运行时Person类中注解信息代码如下:

   //HelloWorld.java文件
   package com.a51work6;

   import java.lang.reflect.Field;
   import java.lang.reflect.Method;

   public class HelloWorld {

      public static void main(String[] args) {
          try {
              Class<?> clz = Class.forName("com.a51work6.Person");            

              // 读取类注解
              if (clz.isAnnotationPresent(MyAnnotation.class)) {              
                  MyAnnotation ann = (MyAnnotation) clz.getAnnotation(MyAnnotation.class);        
                  System.out.printf("%s,读取注解描述: %s \n",
                                  clz.getName(), ann.description());        
                 }

              // 读取成员方法的注解信息
              Method[] methods = clz.getDeclaredMethods();                 
              for (Method method : methods) {
                  if (method.isAnnotationPresent(MemberAnnotation.class)) {   
                      MemberAnnotation ann = method.getAnnotation(MemberAnnotation.class);     
                      System.out.printf("方法%s,读取注解描述: %s \n",
                                   method.getName(), ann.description());   
                  }
              }

              // 读取成员变量的注解信息
              Field[] fields = clz.getDeclaredFields();                       
              for (Field field : fields) {
                  if (field.isAnnotationPresent(MemberAnnotation.class)) {    
                      MemberAnnotation ann = field.getAnnotation(MemberAnnotation.class);
                      System.out.printf("成员变量%s,读取注解描述: %s \n",
                                   field.getName(), ann.description());
                  }
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  }

运行结果如下:

     类com.a51work6.Person,读取注解描述: 这是一个测试类
     方法getName,读取注解描述: 获得名字
     方法getAge,读取注解描述: 获得年龄
     方法setNameAndAge,读取注解描述: 设置姓名和年龄
     成员变量name,读取注解描述: 名字
     成员变量age,读取注解描述: 年龄

上述代码第行是创建Person类对应的Class对象,代码第行是判断Person类是否存在MyAnnotation注解,如果存在则通过代码第行的getAnnotation方法将MyAnnotation注解实例返回。代码第行中ann.description()表达式读取MyAnnotation注解中description成员内容。

代码第行是获得所有成员方法对象数组,通过遍历方法对象数组,在代码第行判断方法中是否存在MemberAnnotation注解,如果存在则通过代码第行的getAnnotation方法将MemberAnnotation注解实例返回。代码第行中ann.description()表达式读取MemberAnnotation注解中description成员内容。

代码第行是获得所有成员变量对象数组,代码第行是判断成员变量中是否存在MemberAnnotation注解。其他的处理与成员方法类似,这里不再赘述。


0 条 查看最新 评论

没有评论
暂时无法发表评论