Skip to main content
  1. Posts/
  2. sec/

深入浅出 Java反射

·387 words·2 mins· loading
Java
bu44er
Author
bu44er
Table of Contents

反射的概念
#

对于任意一个类,都能够得到这个类的所有属性和方法; 对于任意一个对象,都能够调用它的任意属性和方法; 这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。

对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有,比如Runtime类),且拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性


Java 中的 Class
#

反射实现的关键在于 Class类

在 Java 中定义的类(Class)本身也是一个对象,即 java.lang.Class 类的实例。

  • 这个 Class 类的用处在于可以描述其他类(获取Dog类的元数据)
  • 用对象记录类
  • Class 对象一般称为类对象
  • Class 类是 java.lang 包的一部分,而 java.lang 包是自动被所有Java应用程序导入的。这意味着你不需要显式地导入 Class 类,它已经默认可用

理解创建一个 Class 对象
#

对其他类创建一个 Class 对象,创建的是一个描述那个其他类本身的对象,不是那个类本身所实例化的对象。

当然,可以在后续通过 newInstance 的方法实例化得到那个类的对象,需要注意 newInstance 方法返回的是 Object 类型,需要强制转换成需要的对象类型。

// 获取Dog类的元数据(dogClass这个Class对象 本质上描述了 Dog这个对象)
Class<?> dogClass = Dog.class;

// 使用Class对象创建实例(对象)
Dog newDog = (Dog) dogClass.getDeclaredConstructor().newInstance();

反射的实现
#

反射⾥极为重要的⽅法:

  • 获取类: forName
  • 实例化类对象: newInstance
  • 获取函数: getMethod、getConstructor
  • 执⾏函数: invoke
public void execute(String className, String methodName) throws Exception {
    // 获取类
    Class<?> clazz = Class.forName(className);
    // 创建实例
    Object instance = clazz.getDeclaredConstructor().newInstance();  
    // 在实例上调用方法
    clazz.getMethod(methodName).invoke(instance);  
}

通过getMethod反射执行代码
#

通过 getMethod 调用runtime类的公共静态方法 getRuntime 来获取runtime类的实例:

public class Exec {
    public static void main(String[] args) throws Exception {
      
      //java.lang.Runtime.getRuntime().exec("calc");
      Class runtimeClass = Class.forName("java.lang.Runtime");
      
      // getRuntime是静态方法,invoke时不需要传入对象
      Object runtime = runtimeClass.getMethod("getRuntime").invoke(null);
      runtimeClass.getMethod("exec", String.class).invoke(runtime,"open /System/Applications/Calculator.app");
    }
}

getConstructor 方法
#

如果一个类没有无参构造方法,也没有类似 getRuntime 的公共静态方法,我们怎样通过反射实例化该类呢?

使用 getConstructor 获取指定参数类型的公共(public)构造函数,再实例化。

getConstructor 方法的功能是调用构造方法,可配合 newInstance 实现实例化。

  • 参数是构造函数列表类型
    • e.g. class44.getConstructor(List.class)
    • 因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。
    • getConstructor 和 getMethod 的区别是 getConstructor 只获取构造方法

获取构造函数
#

Constructor m = clazz.getConstructor();  // 获取构造函数对象
Object instance = m.newInstance();  // 使用构造函数创建实例
  • Constructor 类的 newInstance() 方法就是专门用来创建新实例的

关于早期版本 newInstance() 的说明!!!
#

==早期 Java 中可以通过 class.newInstance() 直接调用无参构造方法进行实例化==,但是从 Java9 开始这个用法已经被弃用了,不管构造方法有无参数,都要通过 getConstructor 方法(调用构造方法)来实例化。

class.newInstance() // deprecated since Java9

// Java9 之后正确的实例化过程,可以是无参构造
Class.forName("全类名").getDeclaredConstructor(<构造函数参数>).newInstance(<函数参数>); 

关于 getDeclaredConstructor
#

getDeclaredConstructor 相比 getConstructor的区别在于:

  1. getDeclaredConstructor 可以获取类中指定参数类型的任何构造函数,包括 public、protected、private 或默认包访问。
  2. getConstructor 只能获取 public 的构造函数

在 Java9 之后,使用 getDeclaredConstructor 获取私有构造方法需要额外设置:

  1. 设置 setAccessible:constructor.setAccessible(true);
  2. 执行时额外加上参数: java --add-opens java.base/java.lang=ALL-UNNAMED Exec

以 Runtime 类为例说明这个问题,

import java.lang.reflect.Constructor;

public class Exec{
    public static void main(String[] args) throws Exception{
		// 获取类
        Class class44 = Class.forName("java.lang.Runtime");
        // 获取私有构造函数
        Constructor constructor44 = class44.getDeclaredConstructor();
        // 由于是私有,所以需要设置 setAccessible
        constructor44.setAccessible(true); 
        // 由构造函数实例化,并执行实例的方法 exec 执行命令
        class44.getMethod("exec", String.class).invoke(constructor44.newInstance(), "open -a Calculator.app");
    }
}

为什么要用 invoke
#

invoke 可以实现强制类型转换。

假如不用 invoke,需要额外写一个强制类型转换才能不报错:

 ((Runtime)constructor44.newInstance()).exec("open -a Calculator");

但是,实战中一般不会提供强制类型转换,所以需要通过 invoke 绕过类型检查,invoke 允许我们在运行时动态调用方法,而不需要在编译时知道确切的类型。

Runtime类 和 ProcessBuilder类
#

这是两个常见的用于命令执行的类:

  • Runtime 类需要调用 exec 方法执行命令,命令参数传给 exec 方法
  • ProcessBuilder 类需要调用 start 方法执行命令,命令参数直接在实例化的时候传给实例

另外,ProcessBuilder 的构造函数是 public 的,所以直接用 getConstructor 即可。

看一下 ProcessBuilder 执行命令和上一节 Runtime 的区别:

import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;

public class Exec{
    public static void main(String[] args) throws Exception{
		// 获取类
        Class class44 = Class.forName("java.lang.ProcessBuilder");
        // 获取一个参数类型为list的构造函数
        Constructor constructor44 = class44.getConstructor(List.class);
        
        // 命令参数直接在实例化的时候传给实例
        class44.getMethod("start").invoke(constructor44.newInstance(Arrays.asList("open", "-a", "Calculator")));
    }
}

当然,可以把三句写成两句,更简短一点:

Class class44 = Class.forName("java.lang.ProcessBuilder");
class44.getMethod("start").invoke(class44.getConstructor(List.class).newInstance(Arrays.asList("open", "-a", "Calculator")));

当然,也可以用 String[] 类型的构造函数:

public class Exec{
    public static void main(String[] args) throws Exception{
        Class class44 = Class.forName("java.lang.ProcessBuilder");
        Object object44 = class44.getConstructor(String[].class).newInstance((Object) new String[]{"open", "-a", "Calculator.app"});
        class44.getMethod("start").invoke(object44);
    }
}

在 Java18 之后,Runtime类的 exec 方法被弃用,高版本推荐使用 ProcessBuilder。


参考
#