【注解与反射】7 - 2 反射

§7-2 反射

7-2.1 反射的定义

Java 技术性文档中对反射的解释是:

Reflection enables Java code to discover information about the fields, methods and constructors of the loaded classes, and to use reflected fields, methods and constructors to operate on their underlying counterparts, within security restrictions.

反射允许 Java 代码获取已加载类的字段、方法和构造器的信息,并能够使用这些信息在安全限制内调用其底层的代码。

即反射允许对已加载类的字段、方法和构造器的信息进行编程访问。

这样看来,反射的操作主要有两个:获取和调用。获取指的是获取类的 .class 字节码文件(java.lang.Class 对象),然后,基于获取的对象信息访问类的字段、方法和构造器的有关信息。

7-2.2 获取 Class 对象

获取 class 对象的方式有三种:

获取方式 应用场景
调用 Class.forName 方法获取 最为常用
使用 class 关键字 常作为参数传递(如同步锁锁对象)
调用类对象的 getClass() 方法 仅在有类的对象时调用

以一个标准 JavaBean 为例:

package com.reflections;

public class Example {
    private String name;
    private int num;
    
    public Example() {}
    
    public Example(String name, int num) {
        this.name = name;
        this.num = num;
    }
    
    // 省略 Getters and Setters 以及 toString() 方法
}

获取 Class 对象示例:

public static void main(String[] args) throws ClassNotFoundException {
    // 第一种:注意填写全类名。最常用。
    Class<?> aClass = Class.forName("com.reflections.Example");

    // 第二种:常用于参数传递(如同步锁锁对象)
    Class<Example> exampleClass = Example.class;

    // 第三种:仅在有类的对象时使用
    Example zebt = new Example("Zebt", 9328);
    Class<? extends Example> zebtClass = zebt.getClass();

    // 比较
    System.out.println(aClass == exampleClass);		// true
    System.out.println(exampleClass == zebtClass);	// true
}

反射获取类的有关信息,这些信息存储在 Class 类的一个对象中。对于同一个类,无论以何种方式获取的其 Class 对象,这些 Class 对象都是指向同一个对象。

7-2.3 反射获取类的构造方法

反射可以获取类的构造器,每个构造器的信息存储在类 java.lang.reflect.Constructor 中。一旦获取了构造器对象,就可以查看构造器的签名、修饰符、形参个数、形参列表、注解、抛出异常等信息。

Class 类获取构造器对象

方法 描述
Constructor<T> getConstructor(Class<?>... parameterTypes) 根据所给形参类型返回对应的公共构造器
Constructor<?>[] getConstructors() 返回含有类的所有公共构造器的数组
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 根据所给形参类型返回对应的构造器
Constructor<?>[] getDeclaredConstructors() 返回含有所有已声明的构造器的数组

注意

  • getDeclaredConstructor(Class<?>...)getDeclaredConstructors() 方法:方法忽略构造器修饰符,返回指定形参类型的构造器对象(数组);
  • getConstructor(Class<?>...)getConstructors() 方法:方法仅适用于获取公共构造器,否则会抛出异常;
  • 方法接收的参数类型是构造器形参类型所对应的 Class 对象,可调用 class 关键字传递。

Constructor 成员方法查看构造器信息

方法 描述
<T extends Annotation> getAnnotation(Class<T> annotationClass) 若指定注解存在则返回该注解,否则为 null
Annotation[] getDeclaredAnnotations() 返回直接注解在元素上的注解
Class<T> getDeclaringClass() 返回声明该构造器的类对象
Class<?>[] getExeptionTypes() 返回该构造器所声明抛出的所有异常类对象
int getModifiers() 返回构造器的 Java 语言修饰符
String getName() 返回构造器名称
Annotation[][] getParameterAnnotations() 按声明顺序返回参数注解的二维数组
int getParameterCount() 返回构造器形参数目
Parameter[] getParameters() 返回含有构造器所有形参的数组
Class<?>[] getParameterTypes() 返回构造器形参类型数组
boolean isVarArgs() 测试构造器是否接受参数
T newInstance(Object... initargs) 使用构造器实例化对象
void setAccessible(boolean flag) 将构造器对象的 accessible 标志设为指定值

注意

  • int getModifiers() 方法:返回的修饰符常量可在 java.lang.reflect.Modifiers 类中找到,结合位运算可得出修饰符;
  • void setAccessible() 方法:可用于暴力反射,用于启用和禁用访问安全检查;
    • 该方法在 Constructor, MethodField 对象中都存在;
    • 设为 true 时意味着反射对象在使用时取消 Java 语言安全检查;
    • 设为 false 时意味着反射对象在使用时应当实施 Java 语言安全检查;
    • 这可以提高反射效率,若代码必须使用反射,而该语句需要被频繁调用,则请设为 true
    • 使得原本无法访问的私有成员也可以访问。
  • Parameter[] getParameters() 方法:方法会返回带有形参类型(含泛型实际类型)以及形参名称的形参数组;
  • Class<?>[] getParameterTypes() 方法:方法会返回形参的类型(擦除了泛型),并指明该类型属于类/接口/注解等信息;
  • 相较于直接调用对象的方法,使用反射调用对象的方法速度会稍慢些。使用反射方式调用方法时,若禁用安全检查(setAccessible(true)),速度会有较为明显的提升;

修改上述的 JavaBean:

package com.reflections;

import java.util.UUID;

public class Example {
    private String name;
    private int num;
    
    public Example() {
        this.name = UUID.randomUUID().toString();
        this.num = (int) (Math.random() * 100);
    }

    public Example(String name, int num) {
        this.name = name;
        this.num = num;
    }

    protected Example(String name) {
        this.name = name;
        this.num = (int) (Math.random() * 100);
    }

    private Example(int num) {
        this.name = UUID.randomUUID().toString();
        this.num = num;
    }
    
    // Getters & Setters 以及 toString() 方法
}

在测试类中获取私有构造方法:

public static void main(String args[]) {
    Class<?> exampleClass = Class.forName("com.reflections.Example");
    
    // 获取私有构造器
    Constructor privateConstructor exampleClass.getDeclaredConstructor(int.class);
    
    // 禁用安全检查并调用该构造器实例化对象
    privateConstructor.setAccessible(true);		// 必须,否则无法调用该构造器
    Example exampleInst = (Example) privateConstructor.newInstance(9328);
    System.out.println(exampleInst);
}

7.1.5 反射获取类的成员变量

反射还可用于获取类的字段,每个字段的信息存储在 java.lang.reflect.Field 中。一旦获取了字段对象,就可以查看字段修饰符、数据类型、名称、当前值等信息,且可以修改字段的值。

Class 类获取类的字段对象

方法 描述
Field getField(String name) 返回指定名称的公共字段
Field[] getFields() 返回含有所有公共字段的数组
Field getDeclaredField(String name) 返回指定名称的字段
Field getDeclaredFields() 返回含有所有字段的数组

Field 成员方法查看字段信息

方法 描述
Object get(Object obj) 从给定的对象中读取字段的值
<T extends Annotation> getAnnotation(Class<T> annotationClass) 若注解存在,返回指定类型的元素注解,否则返回 null
boolean getBoolean(Object obj)
byte getByte(Object obj)
char getChar(Object obj)
double getDouble(Object obj)
float getFloat(Object obj)
int getInt(Object obj)
long getLong(Object obj)
short getShort(Object obj)
返回实例或静态指定类型的变量值,或返回其他可扩宽转换的变量值
Class<T> getDeclaringClass() 返回声明此字段的类或接口
int getModifiers() 获取字段的访问修饰符
String name() 获取字段名称
Class<?> getType() 获取字段数据类型
void set(Object obj, Object value) 修改指定对象中字段的值
void setAccessible(boolean flag) 修改字段的 accessible 标志

7-2.6 反射获取类的方法

反射也可以获取类的方法,方法信息存储在 java.lang.reflect.Method 中。一旦获取了方法类对象,就可以查看方法修饰符、名称、形参、返回值、抛出异常等信息,还可以调用该方法。

Class 类获取方法对象

方法 描述
Method getMethod(String name, Class<?>... parameterTypes) 返回指定名称和具有指定形参类型的公共方法
Method[] getMethods() 返回含有所有公共方法的数组
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回指定名称和具有指定形参类型的方法
Method[] getDeclaredMethods() 返回含有所有声明方法的数组

注意

  • getMethod(String, Class<?>...)getMethods() 方法:方法会返回包含继承自父类的方法;
  • getDeclaredMethod(String, Class<?>...)getDeclaredMethods() 方法:方法仅返回类所声明的方法;

Method 成员方法查看方法信息

方法 描述
<T extends Annotation> getAnnotation(Class<T> annotationClass) 若注解存在,返回指定类型的元素注解,否则返回 null
Annotation[] getAnnotations() 返回直接声明的注解
Class<?> getDeclaringClass() 返回声明该方法的类或接口
Class<?>[] getExceptionTypes() 返回含有方法所有抛出异常的数组
int getModifiers() 返回方法的访问修饰符
String name() 返回方法名称
Annotation[][] getParameterAnnotations() 按声明顺序,返回形参注解的二维数组
int getParameterCount() 获取方法形参数目
Parameter[] getParameters() 返回含有方法所有形参的数组
Class<?>[] getParameterTypes() 返回方法形参类型
Class<?> getReturnType() 返回方法返回值类型
Object invoke(Object obj, Object... args) 通过指定对象调用该方法,并传递参数并返回结果
boolean isVarArgs() 测试方法是否接受参数
void setAccessible(boolean flag) 修改方法的 accessible 标志

7-2.7 反射获取方法泛型信息

反射也可以用于获取方法上的泛型信息。这些方法定义在 java.lang.reflect.Method 中。

方法 描述
Type[] getGenericExceptionTypes() 获取方法所声明抛出的异常类型,带泛型信息
Type[] getGenericParameterTypes() 获取方法形参的类型,带泛型信息
Type getGenericReturnType() 返回方法返回值的类型,带泛型信息

注意

  • 这里的带泛型信息指的是,相较于对应的 getExceptionTypes(), getParameterTypes()getReturnType(),这些方法能返回完整的泛型信息,包括形参类型,以及形参中使用到的泛型的具体类型。如 java.util.Map<java.lang.String, com.reflections.Example>
  • 非泛型的类型也能返回,只是对应的类型不具有泛型信息。如 int
  • 若返回的类型本身是个泛型类型(带有泛型信息),则该 Type 对象的运行时类本身属于其子类 java.lang.reflect.ParameterizedType,即该类型属于参数化类型,类型中携带泛型的类型参数。

对于类型化参数,可以进一步获取其类型参数中所声明的参数类型:

方法 描述
Type[] getActualTypeArguments() 获取参数化类型中泛型参数所指的实际类型

注意

  • 为了通过反射操作泛型类型,除了上述的 ParameterizedType 外,还有 GenericArrayType, TypeVariableWildcardType 这几种不能被归一到 Class 中但又和原始类型齐名的类型。
    • ParameterizedType:参数化类型,如 Collection<String>
    • GenericArrayType:一种元素类型是参数化类型或类型变量的数组类型;
    • TypeVariable:各种类型变量的公共父接口;
    • WildcardType:一种通配符类型表达式。

示例:一个演示获取方法泛型信息的测试类:

package com.reflections;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

public class ReflectedGenericsDemo {
    public static void test01(Map<String, User> userMap, List<User> userList, int num) {
        System.out.println("test01");
    }

    public static Map<String, User> test02() {
        System.out.println("test02");
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException {
        Method test01 = ReflectedGenericsDemo.class.getDeclaredMethod("test01", Map.class, List.class, int.class);
        Method test02 = ReflectedGenericsDemo.class.getDeclaredMethod("test02");

        // 获取方法形参的泛型信息
        Type[] genericParameterTypes = test01.getGenericParameterTypes();
        System.out.println("test01() 的泛型形参类型:");
        for (Type genericParameterType : genericParameterTypes) {
            System.out.println(genericParameterType);
            if (genericParameterType instanceof ParameterizedType parameterizedType) {
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("\t该形参中的类型参数:");
                    System.out.println("\t" + actualTypeArgument);
                }
            }
        }
        System.out.println();

        // 获取方法的返回值泛型信息
        Type genericReturnType = test02.getGenericReturnType();
        System.out.println("test02() 的返回值泛型信息:");
        System.out.println(genericReturnType);
        if (genericReturnType instanceof ParameterizedType parameterizedType) {
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                System.out.println("\t该返回值的类型参数:");
                System.out.println("\t" + actualTypeArgument);
            }
        }
        System.out.println();

    }
}

7-2.8 练习

练习 1:利用反射将任一 JavaBean 的数据以 .properties 文件写出。格式要求:

field = value

className = 全类名

过程:首先需要获取被写出对象的 Class 对象,获取其所有的已声明字段,然后调用 Properties.setProperty(String, String) 方法为每个字段设置键值对,最后将文件写出。

/**
 * 将给定的对象保存至 .properties 文件中。
 * @param obj
 * @throws NullPointerException 若 {@code obj} 为空
 */
private static void saveObject(Object obj) {
    Objects.requireNonNull(obj);

    // 链接至属性文件
    Properties properties = new Properties(8);
    // 获取 Class 对象
    Class<?> objClass = obj.getClass();

    // 循环获取字段信息
    Field[] declaredFields = objClass.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        declaredField.setAccessible(true);
        try {
            properties.setProperty(declaredField.getName(), declaredField.get(obj).toString());
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    properties.setProperty("className", objClass.getName());
    properties.setProperty("methodInvoked", "");

    // 写出至文件
    try {
        properties.store(new BufferedWriter(new FileWriter("JavaSE\\files\\saves\\" + UUID.randomUUID() + ".properties", StandardCharsets.UTF_8)), null);
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

练习 2:读取 .properties 配置文件,利用反射创建对象并调用指定方法。

配置文件格式:

className = 全类名
methodInvoked = 方法名

所还原的类以及所调用的方法不接受形参。

过程:调用 Properties.load(Reader) 读取配置文件,调用 Properties.getProperty(String) 获取键值对的值以获取实例化的类和调用的方法名。利用反射,通过获得的对象调用指定方法。

private static void readObject(File file) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    // 读取文件
    Properties properties = new Properties();
    properties.load(new BufferedReader(new FileReader(file, StandardCharsets.UTF_8)));

    // 获取类对象
    Class<?> objClass = Class.forName(properties.getProperty("className"));

    // 获取无参构造器
    Constructor<?> constructor = objClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    // 构建对象并调用方法
    Method methodInvoke = objClass.getDeclaredMethod(properties.getProperty("methodInvoked"));
    methodInvoke.setAccessible(true);
    methodInvoke.invoke(constructor.newInstance());
}

练习 3:基于对象关系映射(ORM, Object Relationship Mapping),利用注解和反射而完成类和表结构的映射关系。

对象关系映射常用于数据库。在 ORM 中,类和表结构对应,属性与字段对应,对象与记录对应。例如:

class Student {					|id		|name	|age	|
	int id;						|001	|zebt	|18		|
	String name;		---> 	|002	|张三    |20     |
	int age;					|003	|李四    |30	   |
}

过程:声明一个标准 JavaBean Student,内含字段 id, name, age。为类以及字段分别声明对应的注解(注意注解的生命周期)。在测试类中利用反射和注解获取映射关系。

适用于类上以声明表结构的注解 @TableType

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableType {
    String value();
}

适用于类字段的注解 @RecordProperty

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordProperty {
    String columnName();
    String type();
    int length();
}

Student 类声明:

@TableType("db_student")
public class Student {
    @RecordProperty(columnName = "db_name", type = "varchar", length = 20)
    private String name;
    @RecordProperty(columnName = "db_age", type = "int", length = 2)
    private int age;
    @RecordProperty(columnName = "db_id", type = "int", length = 8)
    private int id;

    public Student() {
    }

    public Student(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    // 省略 Getters & Setters 以及 toString() 方法
}

测试类:

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
    // 利用反射 + 运行时注解,实现数据库中的对象关系映射(ORM, Object Relationship Mapping)
    Class<?> studentClass = Class.forName("com.annotations.Student");

    // 获取类上的注解
    System.out.println("类上的注解:");
    Annotation[] annotations = studentClass.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation);
    }
    System.out.println("==========");

    // 获取字段上的注解
    Field name = studentClass.getDeclaredField("name");
    Field age = studentClass.getDeclaredField("age");
    Field id = studentClass.getDeclaredField("id");

    name.setAccessible(true);
    age.setAccessible(true);
    id.setAccessible(true);

    RecordProperty nameAnnotation = name.getAnnotation(RecordProperty.class);
    RecordProperty ageAnnotation = age.getAnnotation(RecordProperty.class);
    RecordProperty idAnnotation = id.getAnnotation(RecordProperty.class);

    System.out.println("name 字段上的注解:");
    System.out.println(nameAnnotation.columnName());
    System.out.println(nameAnnotation.type());
    System.out.println(nameAnnotation.length());
    System.out.println("==========");

    System.out.println("age 字段上的注解:");
    System.out.println(ageAnnotation.columnName());
    System.out.println(ageAnnotation.type());
    System.out.println(ageAnnotation.length());
    System.out.println("==========");

    System.out.println("id 字段上的注解:");
    System.out.println(idAnnotation.columnName());
    System.out.println(idAnnotation.type());
    System.out.println(idAnnotation.length());
    System.out.println("==========");
}

运行结果:

类上的注解:
@com.annotations.TableType("db_student")
==========
name 字段上的注解:
db_name
varchar
20
==========
age 字段上的注解:
db_age
int
2
==========
id 字段上的注解:
db_id
int
8
==========
posted @ 2024-01-28 00:44  Zebt  阅读(16)  评论(0)    收藏  举报