Java反射
yearnxyl Lv2

反射:将类的各个组成部分封装为其他对象。

Java代码在计算机中经历了三个阶段:源代码阶段->Class对象阶段->Runtime运行时阶段。

image

源代码阶段:在编辑器中写的便是源代码。以.java做后缀。

image

使用javac命令可以将.java文件编译为字节码文件,以.class结尾。此时认为仍处于源代码阶段。如下为class文件,具体含义暂不做分析。

image

Class类对象阶段:使用javac编译成的字节码文件经过jvm中ClassLoader的加载,在内存中生成Class类对象。Class类对象由成员变量Field、构造方法Constructor、成员方法Method组成。以上面Person对象为例,经过ClassLoader加载,name和age两个成员变量封装为Field[]对象,Person(){}方法封装为Constructor[]对象,eat(){}方法封装为Methods[]对象。而这个封装的过程便是反射机制。

Runtime阶段:实例化类对象,在内存中生成实例化对象。

获取Class类对象的四种方法

反射的作用:获取Class类对象。

针对Java代码在计算机的不同阶段,存在不同的方法来获取Class类对象。

  1. 源代码阶段:Class.forName(“全类名”):将字节码文件加载进内存,返回Class类对象。
  2. Class类对象阶段:类名.class:通过类名的属性class获取。
  3. 实例化对象阶段:对象.getClass():getClass()方法在Object类中定义着。
  4. 类加载器ClassLoader.getSystemClassLoader().loadClass(“全类名”)

编写Person类

image

获取Class类对象

image

同一个字节码文件(*.class),在一次程序运行过程中,只会被加载一次。因此获取的四个Class类对象是相等的。

这里有一个容易混淆的地方,我们介绍了四种方法来获取Class类对象,观察其输出结果,均为org.domain.Person。而Java内置的java.lang.Class也叫Class类对象,两者有什么区别和联系?

java.lang.Class不只是类对象,还是一个模板,org.domain.Person便是根据该模板生成的,因此org.domain.Person本质上也是java.lang.Class类对象。org.domain.Person下存在一个getClass()方法来获取java.lang.Class对象。

image

获取Class类成员变量

获取Class类成员变量主要是为了对其数值进行更改。

  1. Field[] getFields()Field getField(String name)获取public修饰的成员变量
  2. Field[] getDeclaredFields()Field getDeclaredField(String name)获取全部成员变量
  3. void set(Object obj,Object value)设置field值
  4. Object get(Object obj)获取值
  5. 针对非public修饰符的成员变量,在单独反射时,需要将其setAccessible(true);

实例:

首先编写Person类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package org.domain;

public class Person {
private String name;
private int age;
public String a;
protected String b;
String c;
private String d;
public Person(){
}
public Person(String name,String a){
this.name=name;
this.a=a;
}
public Person( String name,int age){
this.name=name;
this.age=age;
}
public Person(String a,String... name){
this.a=a;
for(String n:name){
System.out.println(n);
}
}
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 +
", a='" + a + '\'' +
", b='" + b + '\'' +
", c='" + c + '\'' +
", d='" + d + '\'' +
'}';
}
public void eat(){
System.out.println("eat...");
}
public void eat(String food){
System.out.println("eat..."+food);
}
}

使用反射机制获取Class类成员变量并修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.ReflectDemo;
import org.domain.Person;

import java.lang.reflect.Field;

public class ReflectDemo2 {
public static void main(String[] args) throws Exception{
Class cls =Person.class;
//Field[] getFields()
Field[] fields=cls.getFields();
for(Field field:fields){
System.out.println(field);
}
System.out.println("----------------");

Field a=cls.getField("a");
System.out.println(a);
//获取a的值。get(Object obj)
Person p =new Person();
Object value=a.get(p);
System.out.println(value);
//设置a的值。set(Object obj,Object value)
a.set(p,"张三");
System.out.println(p);
System.out.println("================");

//获取全部成员变量。Field[] getDeclaredFields()
Field[] fields1= cls.getDeclaredFields();
for(Field field:fields1){
System.out.println(field);
}
//获取指定私有成员变量。Field getDeclaredField()
Field d=cls.getDeclaredField("d");
//忽略访问权限修饰符的安全检查
//暴力反射
d.setAccessible(true);
Object value2=d.get(p);
System.out.println(value2);

}
}

image

获取Class类构造方法

获取构造方法后,可以使用构造方法创建实例化对象

  1. Constructor<?>[] getConstructors()Constructor<T> getConstructor(Class<?>... parameterTypes)获取public构造方法
  2. Constructor<?>[] getDeclaredConstructors()Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)获取全部构造方法
  3. constructor.newInstance() 使用构造方法创建实例化对象。
  4. 当使用无参数构造方法创建实例化对象时,可以简化为使用Class类对象操作:class.newInstace()
  5. setAccessible()方法适用

实例:Person类保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package org.ReflectDemo;

import org.domain.Person;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class ReflectDemo3 {
public static void main(String[] args) throws Exception {
Class cls=Person.class;
//Constructor<?>[] getConstructors()
Constructor[] constructors=cls.getConstructors();
for (Constructor constructor:constructors){
System.out.println(constructors);
}
//Constructor<?> getConstructor(Class<?>... parameterTypes )
Constructor<?> constructor=cls.getConstructor(String.class,int.class);
System.out.println(constructor);
//创建对象
Object person=constructor.newInstance("张三",23);
System.out.println(person);
System.out.println("-----------------");
Constructor<?> constructor1=cls.getConstructor();
System.out.println(constructor1);
Object person1=constructor1.newInstance();
System.out.println(person1);
//简化
Object person2=cls.newInstance();
System.out.println(person2);
}
}

image

获取Class类成员方法

  1. Method[] getMethods()Method getMethod(String name,Class<?>... parameterTypes)获取public类成员方法。注意这里获取全部public类成员方法的时候会获取Object类的public成员方法
  2. Method[] getDeclaredMethods()Method getDeclaredMethod(String name,Class<?>... parameterTypes)获取全部类成员方法,当然也包括Object类
  3. String method.getName() 获取方法名
  4. String class.getName()获取类名
  5. method.invoke(Object obj,Class<?>...parameterTypes)执行类成员方法
  6. setAccessible()方法适用

实例:Person类保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.ReflectDemo;

import com.sun.security.jgss.GSSUtil;
import org.domain.Person;

import java.lang.reflect.Method;

public class ReflectDemo4 {
public static void main(String args[])throws Exception{
Class cls=Class.forName("org.domain.Person");
//获取指定名称的方法
Method eat_method=cls.getMethod("eat");
System.out.println(eat_method);
//输出方法名
String eat_methodName=eat_method.getName();
System.out.println(eat_methodName);
//执行方法
Person person=new Person();
eat_method.invoke(person);
Method eat_method2=cls.getMethod("eat",String.class);
System.out.println(eat_method2);
eat_method2.invoke(person,"meet");
//获取全部指定类成员方法
Method[] methods=cls.getMethods();
for(Method method:methods){
System.out.println(method);
}

String className=cls.getName();
System.out.println(className);

}
}

image

Java反射与安全

P牛:Java反射可以给Java这门静态语言赋予动态特性。“⼀段代码,改变其中的变量,将会导致这段代码产⽣功能性的变化,我称之为动态特性 ”。

如下代码中execute方法,根据传入的ClassName参数不同,获取不同的Class类对象。再根据MethodName不同,获取该类对象下的方法。最后使用invoke方法来执行该方法。

当不知道传入的参数是什么的时候,这段代码最终实现的功能是未知的。这便是动态特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ReflectDemo;

import java.util.Scanner;

public class ReflectDemo5 {
public void execute(String ClassName,String MethodName)throws Exception{
Class cls=Class.forName(ClassName);
cls.getMethod(MethodName).invoke(cls.newInstance());
}

public static void main(String args[]) throws Exception {
Scanner sc =new Scanner(System.in);
String ClassName=sc.next();
String MethodName=sc.next();
ReflectDemo5 ref=new ReflectDemo5();
ref.execute(ClassName,MethodName);
}
}

这里有一个容易忽略的点,forNamejava.lang.Class类的static方法,只要我们能拿到java.lang.Class类便可以执行forName,而在“获取Class类对象的四种方法”小节的最后,我们发现任何Class类都可以通过getClass()方法来获取java.lang.Class对象。

例如存在String a

image

当然在本例中的cls3,通过对java.lang.Class对象进行操作,最后又获取到java.lang.Class对象是没什么意义的,这里仅仅是要了解其方法。

forName有三个函数重载

  • forName(String className)
  • forName(String name, boolean initialize,ClassLoader loader)
  • forName(Module module, String name) //模块部分是JDK9添加的特性,还未学习,暂不考虑学习此方法

第一个是常见的获取Class的方式。查看器源代码会发现其最后会返回forName0(className, true, ClassLoader.getClassLoader(caller), caller)

1
2
3
4
5
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

至于第二个方法,忽略中间代码,发现其最后返回forName0(name, initialize, loader, caller),由此可知,方法二是对方法一的一个封装。默认了initializeloader两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (loader == null) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (ccl != null) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}

至于两个参数的含义,initialize表示是否进行初始化,loader表示ClassLoader,暂不考虑ClassLoader的学习。

initialize=true的时候,使用forName来获取Class对象时,Class对象会自动初始化。

在Java中,存在三种初始化方法。分别是static{}{}和构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ReflectDemo;

public class ReflectDemo7 {
static {
System.out.println("static");
}
{
System.out.println("{}");
}
ReflectDemo7() {
System.out.println("Reflect");
}

public static void main(String[] args) {
ReflectDemo7 rf=new ReflectDemo7();
}
}

那么三者存在什么区别?

Person p = new Person(“zhangsan”,20); 该句话都做了什么事情?

  1. 因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
  2. 执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
  3. 在堆内存中开辟空间,分配内存地址。
  4. 在堆内存中建立对象的特有属性。并进行默认初始化。
  5. 对属性进行显示初始化。
  6. 对对象进行构造代码块初始化。
  7. 对对象进行对应的构造函数初始化。
  8. 将内存地址付给栈内存中的p变量。

从上述可以看出在步骤2就已经运行static{}方法,步骤四运行{}方法,步骤六运行构造代码块。可以运行上面的代码判断顺序是否正确。

image

对比2、4、6三个步骤会发现,static{}方法是对Class类初始化,发生在找到Class类对象的时候。另外两种方法则发生在步骤3分配内存之后。

而我们上面提到的:使用forName来获取Class对象时,Class对象会自动初始化。forName同样存在一个寻找Class类对象的过程,找到对象后执行static方法。但其没有分配内存,也就是后面两种方法并不会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ReflectDemo;

public class ReflectDemo7 {
static {
System.out.println("static");
}
{
System.out.println("{}");
}
ReflectDemo7() {
System.out.println("Reflect");
}

public static void main(String[] args)throws Exception {
Class cls=Class.forName("org.ReflectDemo.ReflectDemo7");
}
}

image

当我们能够操作如下代码中的className参数时:

1
2
3
public void ref(String className)throws Exception{
Class cls=Class.forName(className);
}

可以将className指向我们构造的恶意类。其中的static{}方法为构造的恶意方法。

1
2
3
4
5
6
7
8
9
static {
try {
Runtime rt = Runtime.getRuntime();
String command = new String("calc");
Process pr = rt.exec(command);
pr.waitFor();
} catch (Exception e) {
}
}

除了使用forName()方法指向恶意类外,也可以使用forName()指向Java的内置类,通过这种方法可以省去import。获取Java内置类之后,若存在无参数构造方法,只要使用class.newInstance(),便可将其实例化(上面获取Class类构造方法提到过),而后利用其属性或方法。例如,如果我们可以获取到Runtime类实例,便可以使用exec()方法来执行任意命令。

但是很不幸的是,很多时候class.newInstance()会失效。主要原因:

  • 类不存在无参数构造方法
  • 类无参数构造方法是私有的

例如Runtime类的无参数构造方法便是private,当需要获取Runtime实例化对象时,需要调用其静态方法getRuntime来返回实例化对象。

image

这么做的目的涉及到Java的单例模式。

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)

如下代码,通过两次new产生的实例化对象是不同的。

1
2
3
4
5
6
7
8
9
10
package org.ReflectDemo;

public class ReflectDemo8 {
public ReflectDemo8(){};
public static void main(String agrs[]){
ReflectDemo8 rf1=new ReflectDemo8();
ReflectDemo8 rf2=new ReflectDemo8();
System.out.println(rf1==rf2);//false
}
}

但是经过单例模式实例化的对象rf1和rf2是相同的。保证了线程中只存在一个ReflectDemo8实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ReflectDemo;

public class ReflectDemo8 {
private static final ReflectDemo8 reflect=new ReflectDemo8();
public static ReflectDemo8 getInstance(){
return reflect;
}
private ReflectDemo8(){};
public static void main(String agrs[]){
ReflectDemo8 rf1=ReflectDemo8.getInstance();
ReflectDemo8 rf2=ReflectDemo8.getInstance();
System.out.println(rf1==rf2); //true
}
}

回到正题,既然在Runtime类中,getRuntime可以获取实例化对象。那我们直接调用不就好了。

1
2
3
4
5
6
7
8
package org.ReflectDemo;

public class ReflectDemo9 {
public static void main(String[] args)throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");
}
}

这么看有点麻烦。拆开来看。

1
2
3
4
5
6
7
8
9
package org.ReflectDemo;

public class ReflectDemo9 {
public static void main(String[] args)throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Object rt=clazz.getMethod("getRuntime").invoke(clazz);
clazz.getMethod("exec",String.class).invoke(rt,"calc.exe");
}
}

通过forName获取getRuntime方法并调用,获取Runtime实例化对象。再获取exec方法并调用,来达到执行命令的目的。刚看到这里的时候有些疑惑。为什么在调用getRuntime方法时invoke(clazz)但是exec方法需要invoke(rt),一个参数是Class一个参数是Object。查看了一下invoke方法,发现其参数确实是Object。也没有其他的重载。

image

后面查看getRuntime方法和exec方法,发现getRuntime方法是static。查阅资料并尝试后发现确实如此。Gavra-invoke-static-method

除了使用getRuntime方法外。在获取Class类构造方法章节,我们提到过获取private构造方法的方式:getDeclaredConstructors()。恰巧在Runtime类中存在private构造方法。因此利用暴力反射同样可以获取Runtime实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ReflectDemo;

import java.lang.reflect.Constructor;

public class ReflectDemo9 {
public static void main(String[] args)throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Constructor constructor= clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Runtime rt= (Runtime) constructor.newInstance();
clazz.getMethod("exec",String.class).invoke(rt,"calc");
}
}

解决了无参数构造方法私有后,再来解决不存在无参数构造方法的问题。

普通的有参数构造方法在上面的获取Class类构造方法中提到过了。比较难理解的是当参数为数组时该怎么处理。

以常用到的,可以任意命令执行的另一个类:ProcessBuilder为例。其具有两个重载的构造方法。

  • ProcessBuilder(List command)
  • ProcessBuilder(String… command)

第一个好理解,只要传入的参数为List即可

1
2
3
4
5
6
7
8
9
10
11
package org.ReflectDemo;

import java.util.Arrays;
import java.util.List;

public class ReflectDemo10 {
public static void main(String[] args) throws Exception{
Class clazz=Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
}
}

重点在于第二个。其参数为可变长参数,对于可变长参数,java在编译时会编译成数组。也就是说在java中,以下两种方法是等价的,不可重载的

  • ProcessBuilder(String… command)
  • ProcessBuilder(String[] command)

查看newInstance方法,我们可以看到其参数为数组。

image

ProcessBuilder的第二个构造函数参数同样也是数组。两者结合,便成为了二维数组。

1
2
3
4
5
6
7
8
package org.ReflectDemo;

public class ReflectDemo10 {
public static void main(String[] args) throws Exception{
Class clazz=Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();
}
}

至于这里为什么是二维数组。我们可以这么去理解ConstructornewInstance之间的过程

image