文章目录

  • 写在前面
  • 什么是java的反射机制?
  • 通过Singleton获取实例
  • Runtime类
  • 利用反射机制获取类对象
  • 利用反射机制获取函数
  • 执行方法
  • 小结
  • 通过Constructor获取实例
  • 私有方法的调用
  • 更灵活的调用方式
  • 小结
  • Reference


写在前面

本文是学习了 啦啦菌NODE 大神的文章 Java反射机制 后根据自己的理解进行的记录和整理,希望拜读原文的请大家移步大神的博客。

什么是java的反射机制?

在Java中的反射机制是指在运行状态中,**任意一个类都,能知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法。这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。**简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。1

通过Singleton获取实例

Runtime类

在每一个JVM进程里面都会存在有一个Runtime类的对象,这个类的主要功能是取得一些与运行时有关的环境属性,或者创建新的进程.

直接调用的方式为Runtime.getRuntime().exec("open -a Calculator"),这条语句调用本机的计算器程序,并创建进程。

验证poc的时候通常会调起一个系统计算器进程,open -a Calculator是mac在terminal中打开计算器的命令,windows中通常为calc.exe

上述语句,如果采用反射机制根据类的名字和方法的名字实现,应该怎样编写呢?

  • 利用反射机制获取Runtime类;
  • 利用反射机制获取exec这个方法
  • 传入参数并执行exec方法

首先我们来解决第一个问题

利用反射机制获取类对象

获取java.lang.class类对象的三种方法

  1. obj.getClass():如果上下文中存在某个类的实例obj,那么我们可以直接通过obj.getClass()获取它的类。
  • obj是实例对象:通过obj.getClass()可以获取该实例的类对象。(如Runtime.getRuntime().getClass()的结果是class java.lang.Runtime类)
  • obj是类对象:通过obj.getClass()可以获取java.lang.class
  1. obj.class:当obj是一个已经加载的类时,直接通过.class参数获取它的java.lang.Class对象。
  2. Class.forName:如果知道类的名字,可以直接使用forname来获取java.lang.Class初始化对象。

* A call to {@code forName("X")} causes the class named * {@code X} to be initialized.

很明显,我们是要根据类的名字获取类对象,因此这里要用到Class.forName()的方式。

Class.forName有两种调用方式

  • Class<?> forName(String className)
  • Class<?> forName(String name, boolean initialize, ClassLoader loader)

前一种调用其实是后面的封装, 下面这两种调用是等价的

Class.forName("Foo") Class.forName("Foo", true, this.getClass().getClassLoader()

第一个问题到这里就有答案了,我们可以通过

Class<?> runtimeCls = Class.forName("java.lang.Runtime");

获取Runtime类

利用反射机制获取函数

getMethod(String name, Class<?>... parameterTypes)是通过反射获取某个类的public方法。

/**
     * Returns a {@code Method} object that reflects the specified public
     * member method of the class or interface represented by this
     * {@code Class} object. The {@code name} parameter is a
     * {@code String} specifying the simple name of the desired method. The
     * {@code parameterTypes} parameter is an array of {@code Class}
     * objects that identify the method's formal parameter types, in declared
     * order. If {@code parameterTypes} is {@code null}, it is
     * treated as if it were an empty array.
     */

因此在获取类对象后,可以进一步使用getMethod方法来获取类对象的函数。这里我们使用getMethod来获取Runtime的exec方法。exec的参数应为字符类型,即String.class,因此可以以如下方式获取exec方法

Method exec = runtimeCls.getMethod("exec", String.class);

执行方法

getMethod返回的是一个Method对象,而Method类下存在方法invoke(),它的作用是传入参数,执行方法。

从invoke的注释中,可以了解到以下信息。

public Object invoke(Object obj, Object... args)

  • @param obj: 执行方法的类对象实例。
  • 如果要执行的方法时普通方法,传入调用这个方法的类对象实例
  • 如果要执行的方法是静态方法,那么obj可以忽略,传入Null即可。
  • 如果要执行的方法是静态方法,且类对象还未初始化,那么在调用invoke的时候会执行初始化。
  • @param args: 执行方法所需的参数。如果没有参数的话,args可以为长度0的Array或者Null。

这里我们要执行的exec是一个普通方法。因此第一个参数需要传入执行exec的类象实例,即Runtime实例。根据前面所学,我们可以用Class.forName("java.lang.Runtime")获取类对象,但是需要注意的是这里仅仅是获取了类对象,而不是实例。如果以这种方式构造成后面介绍的完整语句并且调用,会报错object is not an instance of declaring class。因此获取类对象后要继续获取类实例。Runtime可以用静态方法getRuntime()获取Runtime的实例,这个函数也不需要参数,因此我们可以用getmethod配合invoke来获取Runtime的实例, 也就是

Object runtimeObj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);

第二个参数传入exec要执行的命令,即open -a Calculator

String execParam = "open -a Calculator";

小结

综上,我们可以以如下方式利用反射机制使用runtime的exec方法启动一个进程

// 获取exec方法
Class<?> runtimeCls = Class.forName("java.lang.Runtime");
Method exec = runtimeCls.getMethod("exec", String.class);
// 利用getmethod获取runtime实例
Method getMethod = runtimeCls.getMethod("getRuntime");
Object runtimeObj = getMethod.invoke(null);
// exec参数
String execParam = "open -a Calculator";
// 执行exec
exec.invoke(runtimeObj, execParam);

如果写成一行语句的话,

Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"open -a Calculator");

以上我们完成了通过类内置的静态方法获取类的实例,进一步调用一个public方法。但是假如一个类没有无参构造方法(即不能class.newInstance()),也没有单例模式(只存在一个实例)的静态方法(即不能像getRuntime一样获取实例),那我们该如何实例化这个类呢?

通过Constructor获取实例

ProcessBuilder用来创建操作系统的进程。可以通过以下方式调用一个进程

List<String> paramList = new ArrayList<>();
  paramList.add("open");
  paramList.add("-a");
  paramList.add("Calculator");
  ProcessBuilder pb = new ProcessBuilder(paramList);
  pb.start();

Constructor<T> getConstructor(Class<?>... parameterTypes)函数反射类的Constructor对象,然后利用Constructor.newInstance(Object ... initargs)方法获取实例化的对象。

newInstance()返回的是T对象,因此要执行ProcessBuilder的方法还需要进行一次强制类型转化。

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

进一步利用上面getMethod的方式调用start方法,

Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(Class.forName("java.lang.ProcessBuilder").getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

成功调起计算器进程。

ProcessBuilder除了上述的Constructor外,还有另外一个Constructor

ProcessBuilder(String... command)

以这种方式调用进程的方式是

ProcessBuilder pb = new ProcessBuilder("open", "-a", "Calculator");
pb.start();

传入的参数类型不是List<String>而是String...,即String[],因此采用这种Constructor的时候,getConstructor()的参数应为String[].class

最终构造的语句为

Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(Class.forName("java.lang.ProcessBuilder").getConstructor(String[].class).newInstance((Object[])new String[][]{{"open", "-a", "Calculator"}}));

注意到newInstance()中传入的参数是(Object[]) String [][]类型的,并不是String[]。这是为什么呢?

一开始尝试传入String[]{"open", "-a", "Calculator"},报错java.lang.IllegalArgumentException: wrong number of arguments。于是调试比较两个Constructor在newInstance时传入的参数有何不同。

下图是List.class时传入的参数

java 反射 遍历 List java反射和序列_java 反射 遍历 List


下图是String[].class时传入的参数[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8vLntIG-1639315281205)(Img/image-20211212145333040.png)]


java 反射 遍历 List java反射和序列_java 反射 遍历 List_02

可以注意到传递到newInstance的时候,少了一层,所以构建的时候在外面添加一层列表new String[][]{{"open", "-a", "Calculator"}}

java 反射 遍历 List java反射和序列_反射机制_03

但是这样传递发现还是报错java.lang.IllegalArgumentException: argument type mismatch。注意到编译器有warning,Type String[][] of the last argument to method newInstance(Object...) doesn't exactly match the vararg parameter type. Cast to Object[] to confirm the non-varargs invocation, or pass individual arguments of type Object for a varargs invocation.

于是把添加强制类型转化为(Object[])new String[][]{{"open", "-a", "Calculator"}}。可以成功运行


私有方法的调用

上面两个例子都是调用的Public方法,如果要获取private方法呢?

前面我们使用了getMethod来获取public方法,getConstructor来获取构造器,下面来学习getDeclaredmethodgetDeclaredConstructor两个方法。这两个方法获取的是当前类中声明的方法,包扩所有私有的方法但不包括从父类继承的方法。

还以Runtime为例,Runtime的构造方法是一个private方法。我们利用getDeclaredConstructor获取它的constructor并调用一个计算器进程。

Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getDeclaredConstructor().newInstance(), "open -a Calculator");

运行上述语句,得到一个报错

java.lang.IllegalAccessException: Class RuntimeExec2 can not access a member of class java.lang.Runtime with modifiers "private"

虽然得到了私有方法,但是还需要进一步打破对私有方法的调用,这里使用setAccsessible(true)来打破访问限制。

Constructor<?> con = Class.forName("java.lang.Runtime").getDeclaredConstructor();
con.setAccessible(true);
        Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(con.newInstance(), "open -a Calculator");

运行上述语句成功调起计算器。

更灵活的调用方式

如果不允许使用Class.forName("java.lang.Runtime").getMethod(),这个时候怎么办呢?

可以使用反射机制来获取getMethod方法。

Class.forName("java.lang.Runtime").getMethod("getRuntime")

Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String, Class[].class}).invoke(Class.forName("java.lang.Runtime"), "getRuntime", new Class[0]);

小结

到此为止,我们已经学习了如何利用反射机制调用任意类的任意方法。