reflection

第十六章 反射机制(Reflection)

1 简介

在计算机编程语言的理论中,反射(Reflection)通常包含两种能力: 自省(introspection)和调解(intercession)。自省(introspection)是指在运行时,程序有洞察自身的能力,例如:查看自身的运行状态;而调解(intercession)是指程序能够改变自身状态的能力。目前,Java语言的反射机制(Reflection)仅支持自省(introspection),即Java程序能实时查看程序运行的状态。这些状态信息包括线程信息(Thread)、调用栈(Call Stack)、类(Class)、成员方法(Method)、成员变量(Member Variable)、标注(Annotation)等。Java尚不支持修改运行时的状态。

本章将逐一介绍如何使用Java反射机制获取实时的类、成员方法、成员变量、标注、线程和调用栈等信息。这些信息组成了整个Java的动态系统(Java Dynamics)。我们还会在后续章节中,介绍Java类的动态加载(Class Loading)、Java类的动态生成(Class Generation)等相关内容。

2 Java虚拟机的内部结构

本小节介绍一个简略版的Java虚拟机内部结构。详细的Java虚拟机内部结构会在后续章节中介绍。之所以说这是一个简略版的内部结构是因为本小节将重点放在了Java虚拟机运行时对象之间的关系,而省略了许多Java虚拟机的其他重要部件。学习本章的内容能帮助读者掌握Java虚拟机在运行时是如何管理对象,为开发人员提供对象的动态特性的。

如图一所示,Java虚拟机是一个多线程的进程。这个多线程环境包含开发人员创建的线程和Java虚拟机的系统线程。每一个Java线程包含了一个内部函数调用栈,用来跟踪和记录Java实时的调用状态。一个Frame对象用来表达一个函数调用的状态,其中包含当前函数使用的局部变量等信息。站在开发人员的角度,顶层的Frame,即图中的Frame 1,记录着main函数的调用状态。底层的Frame,即图中的Frame M,记录着当前运行的函数状态。

开发人员可由Thread.getAllStackTraces()获得所有启动的线程以及其内的调用栈信息。每一个StackTraceElement表示栈中的Frame,从其可获得这个Frame对应的函数名,所在的类,文件名等信息。

如果当前函数定义了一个临时变量j,Integer类型,则j是在调用栈中指向一个Integer对象的引用。而真实的Integer对象是在Java虚拟机的堆中。从这个Integer对象出发,开发人员能通过Object.getClass()方法获得Integer类Class<Integer>对象。

在获得类对象后,开发人员就可以通过一系列的方法获得该类的基类(Super Class)、实现的接口(Interface)、成员方法(Member Method)、成员变量(Member Variable)、以及类的标注(Annotation)。在获得了成员方法之后,开发人员可以动态调用该方法。在后续的小节中会逐一介绍具体的使用方法。

图一 Java虚拟机内部结构

图一 Java虚拟机内部结构

3 描述Java类的类Class<T>

每当开发人员定义了一个类时,在编译过程中,Java编译器会为这个类生成一个.class文件。类java.lang.Class<T>就是这个.class文件在内存中的一份拷贝。换句话说,当类加载器(Class Loader)将一个.class文件加载到内存中后,Java虚拟机就会增加一个Class<T>类的对象,用于表达这个类的信息。例如:如果开发人员编写了一个Vehicle类,那么,在程序运行时,Java虚拟机会加载Vehicle.class文件,并创建一个类型为Class<Vehicle>的对象。这个对象可通过Vehicle.class获得,或者由Class.forName()函数从已加载的类对象中查找得到。这两种方式的区别是,Vehicle.class是由编译器处理得到Class<Vehicle>的,Class.forName()是在运行时得到Class<Vehicle>的。

public class Vehicle {
    public static void main(String[] args){
        try {
            Vehicle v = new Vehicle();
            
            // 此行运行时得到Class<Vehicle>,此处可以强制类型转换为Class<Vehicle>
            Class cls1 = v.getClass();
            
            // 此行在编译时得到Class<Vehicle>
            Class<Vehicle> cls2 = Vehicle.class;
            
            // 此行在Java虚拟机中寻找Class<Vehicle>
            Class cls3 = Class.forName("Vehicle");
    
            // 此行打印 "The class name is Vehicle"
            System.out.println("The class name is " + cls1.getName());
    
            // 此行打印 "The class name is Vehicle"
            System.out.println("The class name is " + cls2.getName());
            
            // 此行打印 true
            System.out.println(cls1.equals(cls2));
    
            // 此行打印 true
            System.out.println(cls1.equals(cls3));
        } catch (ClassNotFoundException ex) {}
    }
}

在得到Class对象后,开发人员就可以获得该类相关的信息了,例如:父类、父接口、成员函数、成员变量、类的标注等。

package com.littlewaterdrop.example;

public class Vehicle {
    public String brand = null;
    
    public void setBrand(String brand) {
        this.brand = brand;
    }
    
    public static void main(String[] args){
        Vehicle v = new Vehicle();
        Class cls = v.getClass();
        try {
            // 此行打印 Vehicle的基类 java.lang.Object
            System.out.println(cls.getSuperclass().getName());

            // 此行打印 Vehicle 类实现的接口个数,
            // 实际上cls.getInterfaces()返回的是一个数组,每个元素代表着一个Vehicle实现的接口
            System.out.println(cls.getInterfaces().length);

            // 此行打印 Vehicle 所在包的名字com.littlewaterdrop.example
            System.out.println(cls.getPackage().getName());

            // 此行打印 Vehicle 的成员变量的个数
            // 实际上cls.getFields()返回的是一个数组,每个元素代表着一个成员变量
            System.out.println(cls.getFields().length);
            // 还可以通过getField()方法根据名字查找成员变量
            System.out.println(cls.getField("brand").getName());

            // 此行打印 Vehicle 的成员方法的个数
            // 实际上cls.getMethods()返回的是一个数组,每个元素代表着一个成员方法
            System.out.println(cls.getMethods().length);
            // 还可以通过成员方法名称和参数查找成员方法
            System.out.println(cls.getMethod("setBrand", String.class).getName());

            // 此行打印 Vehicle 的标注个数
            // 实际上cls.getAnnotations()返回的是一个数组,每个元素代表着一个标注
            System.out.println(cls.getAnnotations().length);
        } catch (NoSuchFieldException | NoSuchMethodException ex) {}
    }
}

另外,Class对象还提供了自查和动态创建对象的功能。

public class Vehicle {
    public static void main(String[] args){
        Vehicle v1 = new Vehicle();
        Class cls = v1.getClass();

        try {
            //创建另一个Vehicle对象
            Vehicle v2 = (Vehicle)cls.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {}
        
        // 测试v1对象是否是数组对象,打印false
        System.out.println(cls.isArray());

        // 测试v1对象是否是枚举对象,打印false
        System.out.println(cls.isEnum());

        // 测试v1对象是否是接口对象,打印false
        System.out.println(cls.isInterface());

        // 测试v1对象是否是标注对象,打印false
        System.out.println(cls.isAnnotation());

        // 测试v1对象是否是匿名类的对象,打印false
        System.out.println(cls.isAnonymousClass());
    }
}

3 描述Java方法的类Method

类java.lang.reflect.Method用来表达类的成员方法或者静态方法。通过Method对象,开发人员能获得方法的名字,入参个数和类型,返回类型,标注,声明可能抛出的异常等信息。如下面的例子所示。

import java.lang.reflect.Method;

public class Vehicle {
    // 实现一个成员方法 toString()
    public String toString() {
        return "Vehicle";
    }
    
    // 实现一个静态方法 buildVehicles(Integer count)
    public static Vehicle[] buildVehicles(Integer count) {
        return new Vehicle[count];
    }

    public static void main(String[] args){
        try {
            Vehicle v = new Vehicle();
            Class cls = v.getClass();

            // 查找成员方法Vehicle.toString
            Method toStringMethod = cls.getMethod("toString");
            // 动态调用 Vehicle.toString 方法
            // 因为toString是成员方法,所以,第一个参数需指向所调用的对象
            toStringMethod.invoke(v);

            // 查找静态方法Vehicle.buildVehicles()
            Method buildVehicleMethod = cls.getMethod("buildVehicles", Integer.class);
            // 动态调用 Vehicle.buildVehicles 方法
            // 因为buildVehicles是静态方法,第一个参数需传入null,不指向任何对象
            buildVehicleMethod.invoke(null, 5);

            // 打印Vehicle.toString方法的标注个数
            // 实际上getDeclaredAnnotations()返回的是一个数组,每个元数代表着一个标注
            System.out.println(toStringMethod.getDeclaredAnnotations().length);

            // 打印Vehicle.buildVehicle方法的参数个数
            System.out.println(buildVehicleMethod.getParameterCount());
            // 实际上getParameterTypes()返回的是一个数组,每个元数代表着一个参数的类型
            System.out.println(buildVehicleMethod.getParameterTypes().length);

            // 打印Vehicle.buildVehicle方法的返回类型
            System.out.println(buildVehicleMethod.getReturnType().getName());

            // 打印Vehicle可能抛出异常的个数
            // 实际上getExceptionTypes()返回的是一个数组,每个元数代表着一个可能抛出的异常
            System.out.println(buildVehicleMethod.getExceptionTypes().length);
        } catch (Exception ex) {}
    }
}

4 描述成员变量的类Field

类java.lang.reflect.Field用来标识对象的成员变量。由Field对象,开发人员可以获得成员变量的类型和值。如下面的例子所示。

import java.lang.reflect.Field;

public class Vehicle {
    public String brand = null;

    public static void main(String[] args){
        try {
            Vehicle v = new Vehicle();
            Class cls = v.getClass();

            // 查找成员变量Vehicle.brand
            Field brandField = cls.getField("brand");
        
            // 打印Vehicle.brand的类型,此处打印java.lang.String
            System.out.println(brandField.getType().getName());
        
            // 打印Vehicle.brand的当前值,此处打印null
            System.out.println((String)brandField.get(v));
        
            // 设置Vehicle.brand的值为“Ford”
            brandField.set(v, "Ford");
        
            // 设置后,再次打印Vehicle.brand的值,此处打印"Ford"
            System.out.println((String)brandField.get(v));
        } catch (Exception ex) {}
    }
}

5 描述标注的接口Annotation

标注Annotation是一种特殊的“记号”,它能标识类,方法,或者参数具有特殊的属性。通过java.lang.annotation.Annotation接口,开发人员可以判断一个Annotation是否标注在类或者方法上。类似的,开发人员还可以查看标注值。例如:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// 定义一个新的标注Developer,用于标识开发人员的姓名和开发日期
// 在运行时(Run-time)保留该标注
@Retention(RetentionPolicy.RUNTIME)
public @interface Developer {
    String author();
    String date();
}

import java.lang.reflect.Method;
import java.lang.annotation.Annotation;

// 在类Vehicle上标注开发人员"Little Waterdrop"和日期"2019-11-21"
@Developer(
    author="Little Waterdrop",
    date="2019-11-21"
)
public class Vehicle {
    // 在成员方法toString()上标注开发人员"Little Waterdrop"和日期"2019-11-22"
    @Developer(
        author="Little Waterdrop",
        date="2019-11-22"
    )
    public String toString() {
        return "Vehicle";
    }

    public static void main(String[] args){
        try {
            Vehicle v = new Vehicle();
            Class cls = v.getClass();

            // 查找Vehicle是否标注了Developer。如果未标注,函数调用返回null
            Annotation annOnVehicle = cls.getAnnotation(Developer.class);
            
            // 查找Developer标注的author方法
            Method authorMethod = Developer.class.getMethod("author");
            
            // 打印Developer标注中author的值
            String developer = (String)authorMethod.invoke(annOnVehicle);
            System.out.println("Vehicle was developed by " + developer);

            // 查找Vehicle.toString方法
            Method toStringMethod = v.getClass().getMethod("toString");
            
            // 从Vehicle.toString方法上获取Developer标注对象
            Annotation annOnMethod = toStringMethod.getAnnotation(Developer.class);
            
            // 从Developer标注对象上查找author方法
            Method authorOnToStringMethod = Developer.class.getMethod("author");
            
            // 打印Developer标注中author的值
            String methodDeveloper = (String)authorOnToStringMethod.invoke(annOnMethod);
            System.out.println("Method toString was developed by " + methodDeveloper);
        } catch (Exception ex) {}
    }
}

6 线程Thread和调用栈

类java.lang.Thread用来描述Java程序当前运行的线程。开发人员可由Thread.currentThread()获得描述当前线程的对象;或者由Thread.getStackTrace()获得当前线程的调用栈信息。在下面的例子中,我们将用Thread.getAllStackTraces()函数说明如何使用Thread对象和StackTraceElement对象。Thread.getStackTrace()返回当前Java虚拟机中所有的线程和相应的调用栈信息。

import java.util.Map;

public class Vehicle {
    public static void main(String[] args){
        // 获得所有的线程和调用栈
        Map<Thread, StackTraceElement[]> threadMap = Thread.getAllStackTraces();
        
        for (Map.Entry<Thread, StackTraceElement[]> entry: threadMap.entrySet()) {
            // 遍历所有的线程,打印每个线程的名字
            System.out.println("This is thread " + entry.getKey().getName() + ": ");
            
            // 遍历线程中调用栈数组,打印所在类名+函数名,或者打印所在文件名+行号
            for (StackTraceElement frame: entry.getValue()) {
                System.out.println("frame at " + frame.getClassName() + ":" + frame.getMethodName());
                System.out.println("   or at " + frame.getFileName() + ":" + frame.getLineNumber());
            }
        }
    }
}

7 结语

本章介绍了Java反射机制中类、成员方法、成员变量、基类、父类接口、标注、线程与调用栈之间的关系,以及它们在Java虚拟机内部的位置。Java语言为了增强其动态性,提供了反射机制,同时也引入了额外的开销。例如,Java虚拟机需要加载这些元数据(Metadata)。这些元信息占用额外的内存空间。当使用Method动态调用成员方法时,性能也比直接调用函数慢。另一方面,Java动态性也为Java程序带来了安全隐患。例如,Java虚拟机可以动态加载.class文件。当运行不被信任的代码(Untrusted Codes)时,这些代码可以洞察Java虚拟机的内部状态,例如:查看java程序使用了哪些第三方代码库等。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.