Java反射机制探究

0x00 什么是反射

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

一句话概括:反射可以实现在运行时可以知道任意一个类的属性和方法。

0x01 优点与缺点

优点

灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 &获取对象实例。

编译方式说明:

  • 静态编译:在编译时确定类型 & 绑定对象。如常见的使用new关键字创建对象

  • 动态编译:运行时确定类型 & 绑定对象。动态编译体现了Java的灵活性、多态特性 & 降低类之间的藕合性

缺点

执行效率低,因为反射的操作 主要通过JVM执行,所以时间成本会 高于 直接执行相同操作。

  • 因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。

  • 编译器难以对动态调用的代码提前做优化,比如方法内联。

  • 反射需要按名检索类和方法,有一定的时间开销。

容易破坏类结构,因为反射操作饶过了源码,容易干扰类原有的内部逻辑。

0x02 基本使用方法

获取Class对象有三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//第一种方式:
//Ar1引用的对象代表整个Person类
Class Ar1 = Class.forName("top.sh1yan.reflect.Person");

//第二种方式:
//java中每个类型都有 class 属性.
Class Ar2 = Person.class;

//第三种方式:
//java语言中任何一个java对象都有getClass 方法
Person Ar3 = new Person();
Class Br1 = Ar3.getClass();

//因为Person这个类在JVM中只有一个,所以Ar1,Ar2,Br1的内存地址是相同的,指向堆中唯一的Class对象.
System.out.println(Ar1==Ar2); //true
System.out.println(Ar2==Br1); //true

常用的反射方法:

样例代码(下列所有示例均在该样例代码中演示):

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
package top.sh1yan.test1;

public class blogbody {

public String name = "shiyan";
public int age = 1;

public static void main(String[] args) {

}

public blogbody() {
System.out.println("无参构造函数");
}

public blogbody(String name,int age) {
System.out.println("我的名字叫"+name+",我今年"+age+"岁了。");
}

public void addPrintName(String name) {
System.out.println("我的名字叫:"+ name );
}

public void addPrintAge(int age) {
System.out.println("我今年"+ age +"岁了");
}

}

1、获取成员方法Method

1
2
3
4
5
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到该类所有的方法,不包括父类的
public Method[] getDeclaredMethods() // 得到该类所有的方法,不包括父类的
public Method getMethod(String name, Class<?>... parameterTypes) // 得到该类所有的public方法,包括父类的
public Method[] getMethods() // 得到该类所有的public方法,包括父类的
Method.getName() //获取函数名 例如:addPrintAge

getDeclaredMethods()方法展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
步骤代码:

Class Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Method[] Ar2 = Ar1.getDeclaredMethods();
for(Method Ar3 : Ar2) {
System.out.println(Ar3);
}

输出结果:

public static void top.sh1yan.test1.blogbody.main(java.lang.String[]) throws java.lang.ClassNotFoundException
public void top.sh1yan.test1.blogbody.addPrintName(java.lang.String)
public void top.sh1yan.test1.blogbody.addPrintAge(int)

getMethods()方法展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
步骤代码:

Class Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Method[] Ar2 = Ar1.getMethods();
for(Method Ar3 : Ar2) {
System.out.println(Ar3);
}

输出结果:

public static void top.sh1yan.test1.blogbody.main(java.lang.String[]) throws java.lang.ClassNotFoundException
public void top.sh1yan.test1.blogbody.addPrintName(java.lang.String)
public void top.sh1yan.test1.blogbody.addPrintAge(int)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

Method.getName() 方法演示

1
2
3
4
5
6
7
8
9
10
11
12
步骤代码:

Class Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Method[] Ar2 = Ar1.getDeclaredMethods();
for(Method Ar3 : Ar2) {
System.out.println(Ar3.getName());
}
输出结果:

main
addPrintName
addPrintAge

getDeclaredMethod()方法展示:

1
2
3
4
5
6
7
8
9
10
11
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody"); //先生成class
Object Br1 = Ar1.newInstance(); //newInstance可以初始化一个实例
Method Ar2 = Ar1.getDeclaredMethod("addPrintName",String.class); //获取方法
Ar2.invoke(Br1, "shiyan"); //通过invoke调用该方法,参数第一个为实例对象,后面为具体参数值

输出结果:

无参构造函数
我的名字叫:shiyan

这里需要注意的是,输出结果中的“无参数构造函数”是有Br1这里产生的,这里我们初始化实例了这个类。

getMethod()展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody"); //先生成class
Object Br1 = Ar1.newInstance(); //newInstance可以初始化一个实例
System.out.println("-----------");
Method Ar2 = Ar1.getMethod("hashCode"); //获取方法
System.out.println(Ar2.invoke(Br1)); //通过invoke调用该方法,参数第一个为实例对象

输出结果:

无参构造函数
-----------
1311053135

2、获取成员变量Field

类的成员变量也是一个对象,它是java.lang.reflect.Field的一个对象,所以我们通过java.lang.reflect.Field里面封装的方法来获取这些信息。

1
2
3
4
public Field getDeclaredField(String name) // 获得该类自身声明的所有变量,不包括其父类的变量
public Field getField(String name) // 获得该类自所有的public成员变量,包括其父类变量
public Field[] getDeclaredFields() //获得该类自身声明的所有变量,不包括其父类的变量
public Field[] getFields() // 获得该类自所有的public成员变量,包括其父类变量

getFields()方法展示:

1
2
3
4
5
6
7
8
9
10
11
12
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Field[] Ar2 = Ar1.getFields();
for(Field Ar3 : Ar2) {
System.out.println(Ar3.getName());
}

输出结果:

name
age

因为父类中无公开的成员变量,所以只输出了我们自己原有的参数。

getField()方法展示:

1
2
3
4
5
6
7
8
9
10
11
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Object Br1 = Ar1.newInstance();
Field Ar2 = Ar1.getField("name");
System.out.println(Ar2.get(Br1));

输出结果:

无参构造函数
shiyan

3、获取构造函数Constructor

类的成构造函数也是一个对象,它是java.lang.reflect.Constructor的一个对象,所以我们通过java.lang.reflect.Constructor里面封装的方法来获取这些信息。

1
2
3
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) //  获得该类所有的构造器,不包括其父类的构造器
public Constructor<T> getConstructor(Class<?>... parameterTypes) // 获得该类所以public构造器,包括父类
public Constructor[] getDeclaredConstructors() // 获得该类所有的构造器,不包括其父类的构造器

getDeclaredConstructor()方法展示:

1
2
3
4
5
6
7
8
9
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Constructor Ar2 = Ar1.getConstructor(String.class,int.class);
Ar2.newInstance("老P",2);

输出结果:

我的名字叫老P,我今年2岁了。

注意:Class的newInstance方法,只能创建只包含无参数的构造函数的类,如果某类只有带参数的构造函数,那么就要使用另外一种方式:

fromClass.getDeclaredConstructor(String.class).newInstance(“tengj”);

getConstructors()方法演示:

1
2
3
4
5
6
7
8
9
10
11
12
步骤代码:

Class<?> Ar1 = Class.forName("top.sh1yan.test1.blogbody");
Constructor[] Ar2 = Ar1.getConstructors();
for(Constructor Ar3 : Ar2) {
System.out.println(Ar3);
}

输出结果:

public top.sh1yan.test1.blogbody(java.lang.String,int)
public top.sh1yan.test1.blogbody()

常用方法名汇总:

获取构造函数:

getConstructors()//获取所有公开的构造函数

getConstructor(参数类型)//获取单个公开的构造函数

getDeclaredConstructors()//获取所有构造函数

getDeclaredConstructor(参数类型)//获取一个所有的构造函数

获取名字:

可以反射类名。

getName()//获取全名 例如:com.test.Demo

getSimpleName()//获取类名 例如:Demo

获取方法:

getMethods()//获取所有公开的方法

获取字段:

getFields()//获取所有的公开字段

getField(String name)//参数可以指定一个public字段

getDeclaredFields()//获取所有的字段

getDeclaredField(String name)//获取指定所有类型的字段

设置访问属性:

默认为false,设置为true之后可以访问私有字段。

Field.setAccessible(true)//可访问

Field.setAccessible(false)//不可访问

以及Method类的invoke方法

invoke(Object obj, Object… args) //传递object对象及参数调用该对象对应的方法

0x03 了解反射原理机制

:该章节主要是参考学习了bravo1988大佬写的浅谈反射机制的文章内容。

JVM构建实例过程→.class文件解读→类加载器流程→Class类解读→反射API

1、JVM构建实例过程

创建对象:blogbody Ar1 = new blogbody();

编译程序:blogbody.java → blogbody.class

加载类:ClassLoader加载.class文件到内存;*执行静态代码块和静态初始化语句

blogbody.class→ClassLoader()加载进内存→内存方法区[生成Class对象;Clas\对象]→创建实例对象到内存中的堆

执行new:申请一片内存空间

调用构造器:创建一个空白对象

程序执行:子类调用父类构造器

构造器执行:执行构造代码块和初始化语句\构造器内容

2、.class文件解读

.class 文件主要为把Java代码编译成JVM可识别的字节码文件,可以理解该类文件中的内存均为JVM可以读取执行的指令集。

1
2
3
4
5
6
7
8
9
10
11
12
反编译举例:

0:iconst_1
1:istore_1
2:iconst_2
3:istore_2
4:iload_1
5:iload_2
6:iadd
7:istore_3
8:返回
...

3、类加载器流程

类加载器中核心方法为loadClass(),输入需要加载的类名,机会进行加载对应的类。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 模板方法模式:如果还是没有加载成功,调用findClass()
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

// 子类应该重写该方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

加载.class文件大致可以分为3个步骤:

① 检查是否已经加载,有就直接返回,避免重复加载
② 当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件
③ 上面两步都失败了,调用findClass()方法加载

需要注意的是,ClassLoader类本身是抽象类,而抽象类是无法通过new创建对象的。所以它的findClass()方法写的很随意,直接抛了异常,反正你无法通过ClassLoader对象调用。也就是说,父类ClassLoader中的findClass()方法根本不会去加载.class文件。

正确的做法是,子类重写覆盖findClass(),在里面写自定义的加载逻辑。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
/*自己另外写一个getClassData()
通过IO流从指定位置读取xxx.class文件得到字节数组*/
byte[] datas = getClassData(name);
if(datas == null) {
throw new ClassNotFoundException("类没有找到:" + name);
}
//调用类加载器本身的defineClass()方法,由字节码得到Class对象
return defineClass(name, datas, 0, datas.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类找不到:" + name);
}
}

defineClass()是ClassLoader定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。

类加载器流程导图:

4、Class类解读

每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。

Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。

所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。

方法名 说明
forName() (1)获取Class对象的一个引用,但引用的类还没有加载(该类的第一个对象没有生成)就加载了这个类。 (2)为了产生Class引用,forName()立即就进行了初始化。
Object-getClass() 获取Class对象的一个引用,返回表示该对象的实际类型的Class引用。
getName() 取全限定的类名(包括包名),即类的完整名字。
getSimpleName() 获取类名(不包括包名)
getCanonicalName() 获取全限定的类名(包括包名)
isInterface() 判断Class对象是否是表示一个接口
getInterfaces() 返回Class对象数组,表示Class对象所引用的类所实现的所有接口。
getSupercalss() 返回Class对象,表示Class对象所引用的类所继承的直接基类。应用该方法可在运行时发现一个对象完整的继承结构。
newInstance() 返回一个Oject对象,是实现“虚拟构造器”的一种途径。使用该方法创建的类,必须带有无参的构造器
getFields() 获得某个类的所有的公共(public)的字段,包括继承自父类的所有公共字段。 类似的还有getMethods和getConstructors。
getDeclaredFields 获得某个类的自己声明的字段,即包括public、private和proteced,默认但是不包括父类声明的任何字段。类似的还有getDeclaredMethods和getDeclaredConstructors。

5、反射API

这里有一点需要先了解下就是newInstance(),newInstance()底层就是调用无参构造对象的newInstance()。

本质上Class对象要想创建实例,其实都是通过构造器对象。如果没有空参构造对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造对象然后调用构造对象的newInstance()。

其次,需要再理解下Class、Field、Method、Constructor四个对象的关系:

Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述:

关于反射API,主要需要了解两个问题原理:1个是“为什么根据Class对象获取Method时,需要传入方法名+参数的Class类型”、另一个是“调用method.invoke(obj, args);时为什么要传入一个目标对象”。

到这里,我们就把两个重点难点的反射API了解了。

0x04 反射的一些利用特性

1、常规的反射使用方法

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

import java.lang.reflect.Method;

public class forname1 {

public static void main(String[] args) throws Exception {

Class<?> cls = Class.forName("java.lang.Runtime");
Method getruntime = cls.getMethod("getRuntime", null);
Object runtime = getruntime.invoke(null);
Method exec = cls.getMethod("exec", String.class);
exec.invoke(runtime, "calc");

}

}

以上,我们正常启动了一个计算器。

2、在int数据组中添加字符串类型内容

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
package top.sh1yan.test1;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class forname3 {

public static void main(String[] args) throws Exception {
// TODO 自动生成的方法存根
List<Integer> list = new ArrayList<Integer>();
list.add(1);
addString(list);
}

public static void addString(List<Integer> Ar1)throws Exception{
Object obj = null;
Class clazz = Ar1.getClass();
Method method = clazz.getMethod("add", Object.class);
obj = method.invoke(Ar1, "shiyan");
obj = method.invoke(Ar1, "男");
for(Object i : Ar1) {
System.out.println("val:"+ i);
}
}

}

0x05 参考链接

[1] https://zhuanlan.zhihu.com/p/66853751

[2] https://mp.weixin.qq.com/s/OMXrFc7uUN8wGv6yHno3Lg

[3] http://tengj.top/2016/04/28/javareflect/

[4] https://blog.csdn.net/carson_ho/article/details/80921333