Java的反射机制

Java的反射机制

今天来研究一下Java的反射机制。反射机制是很多的框架与插件的基础.这部分由于会涉及到JVM的底层机制,会比较晦涩,还是要慢慢学习的

Java的类加载机制

在讲反射之前,我们先来了解一下Java的类加载机制。在运行状态下JVM会将类的信息提取之后在内存内生成一个Class对象,这个对象全局唯一

,我们对static资源的操作实际上就是对Class对象的操作.但是,Class对象与一般的对象不同的是它hi记录了类的各种信息包括类所属的父类,实现的接口,所在的包等等

反射机制是什么

Java的反射机制(Reflection)是一种动态获取程序内部信息和操作对象的能力。通过反射,程序可以在运行时查看类的结构、字段、方法、构造函数等元数据,并对这些元素进行操作。反射是通过 java.lang.reflect 包中的类来实现的。

或许你现在仍然无法理解反射机制有什么用,但请耐心的学下去,很快你就能意识到它的强大功能。

让我们先从Class对象学起

Class对象的获取

首先我们要获取Class对象

1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {
Class<String> clazz = String.class; //使用class关键字,通过类名获取
Class<?> clazz2 = Class.forName("java.lang.String"); //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
Class<?> clazz3 = new String("cpdd").getClass(); //通过实例对象获取
}

有一点需要注意,尽管后两种方法的返回都是泛型的,但事实上这三个方法返回的都是同一个Class对象

我们都知道,如果希望对基本数据类型的数据进行对象操作,我们要将其转换为对应的包装类,但是基本数据类型也存在自身的Class对象

1
2
3
4
public static void main(String[] args) {
Class<?> clazz = int.class;
Class<?> clazz2 = Integer.class;
}

我们可以发现,基本数据类型的Class是定义在其对应的包装类中的

1
2
3
4
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {

public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

所以我们也可以通过

1
Class<?> clazz2 = Integer.TYPE;

来获取包装类对应的基本类型的Class对象(注意,包装类的Class对象与基本数据类型的Class对象是两个不同的对象)

此外,类对应的数组类型也拥有单独的Class对象

1
Class<?> clazz2 = int[].class;

Class类与类型比较

一般的情况下我们要判断一个对象是否属于某个类型可以用instanceof关键字来进行比较,现在我们也可以这么比较

1
2
3
People p = new People();
System.out.println(p instanceof People);
System.out.println(p.getClass()== People.class);

此外,我们如果希望判断某个对象是否为某个接口/抽象类的实现或者是某个父类的子类,可以这么做

1
p.getClass().asSubclass(People.class)

进一步的我们可以获取这个对象所属类的父类的Class对象

1
2
p.getClass().getSuperclass();//获取父类的Class对象
p.getClass().getGenericSuperclass();//获取父类的基本数据类型的Class对象

同样的可以用来获取父接口

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Integer i = 10;
for (Class<?> anInterface : i.getClass().getInterfaces()) {//每次只返回一个接口,需要通过循环获取
System.out.println(anInterface.getName());
}

for (Type genericInterface : i.getClass().getGenericInterfaces()) {
System.out.println(genericInterface.getTypeName());
}
}

创建类对象

我们在前面提到过,我们创建对象的模版实际上是Class对象而不是类,那么我们是不是可以反过来直接创建出某个Class对象,再用这个Class去创建对象,哪怕实际的代码中不存在这样的类?当然可以

1
2
3
4
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Class<?> clazz = People.class;
Object people=clazz.newInstance();
}

注意,newInstance方法或抛出两个异常,如果使用带参构造,则抛出InstantiationException,这个方法只支持无参构造,如果原来的类是非public的,则抛出IllegalAccessException.但是这个方法现在已经不再被推荐

现在主要的创建对象的方式是通过构造器,此时是支持带参构造的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
Class<?> clazz = People.class;
try {
Object people=clazz.getConstructor(String.class).newInstance("你好");//String.class是提前声明构造器所需要的参数类型
} catch ( InvocationTargetException | NoSuchMethodException |InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}

}
}
public class People {
public String name;
public People(String name) {
this.name = name;
}
}

此外,如果构造方法是非public的,我们也不是没有办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
Class<?> clazz = People.class;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
People people= (People) constructor.newInstance("你好");
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}

}
}
public class People {
public String name;
private People(String name) {
this.name = name;
}
}

调用方法

接下来我们要尝试用反射机制调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class<?> clazz=Class.forName("People");
Constructor<?> constructor=clazz.getConstructor(String.class);
People people= (People) constructor.newInstance("John Doe");
Method setName=clazz.getMethod("setName", String.class);
setName.invoke(people,"aaa");
System.out.println(people.getName());
}
}
public class People {
private String name;
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

同样的如果一个方法是非public的,我们也可以强行调用

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
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class<?> clazz=Class.forName("People");
Constructor<?> constructor=clazz.getConstructor(String.class);
People people= (People) constructor.newInstance("John Doe");
Method method=clazz.getDeclaredMethod("test");
method.setAccessible(true);
method.invoke(people);
}
}
public class People {
public String name;
public People(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void test(){
System.out.println("这里无法被访问");
}
}

如果一个方法需要多个参数,直接用逗号一直加就可以了

如果一个方法中有可变长参数,那么可以用对应类型的数组类型的Class对象声明参数类型

修改类中属性

这段我们就直接点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Class<?> clazz=Class.forName("People");
Constructor<?> constructor=clazz.getConstructor(String.class);
People people= (People) constructor.newInstance("John Doe");
Field[] fields=clazz.getDeclaredFields();//这个方法可以直接获取全部的属性也可以根据输入的字符串返回有对应名称的字符串
for(Field f:fields){
f.setAccessible(true);
System.out.println(f.getName());//获得名称
System.out.println(f.getType());//获得类型
System.out.println(f.get(people));//获得对象中的值
}
fields[0].set(people,"as");//直接无视private修改属性
}
}
public class People {
private String name;
private int age;
public People(String name) {
this.name = name;
}
}

如果愿意,我甚至能够直接修改final字段.但是,具体的方法我就不放在这里了.因为自Java9之后添加了对这种修改的限制,需要修改虚拟机配置才能对final字段进行修改.而Java9之前只要setAccessible(true)就能直接修改

类加载器

如果我们将Main方法写在jdk提供的类中程序是否能正常运行?答案是不行,因为jdk提供的类通过BootstarpClassLoade加载的,而我们自己编写的类是通过AppClassLoade进行加载的,所以AppClassLoade找不到我们的Main方法就会报错

换句话说我们其实可以选择直接用类加载器来创造一个不存在的类(以下代码看看就好,不用太过深究,到时候讲框架相关内容的时候再细讲)

我先创建一个class文件,其反编译的内容为

1
2
3
4
5
6
7
public class Test {
public Test() {
}
public void test() {
System.out.println("hello");
}
}

接下来我可以直接将这个类文件加载到当前的程序中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}//由于默认加载器中的这个方法是protected的,我无法调用,所以通过一重继承关系获得

public class Main {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader=new MyClassLoader();
FileInputStream fis=new FileInputStream("E:\\test12\\out\\production\\test12\\Test.class");
byte[] buffer=new byte[fis.available()];
fis.read(buffer);
fis.close();//读取文件并获得内容
Class<?> clazz=myClassLoader.defineClass("Test", buffer);//直接定义类
Object object= clazz.getConstructor().newInstance();
Method method=clazz.getMethod("test");
method.invoke(object);//尝试使用这个类的方法
}
}

这个套路其实是用来防破解的,在发布的程序中缺失部分关键类,在使用时再通过网络将编译好二进制数据加密后传输给用户,用户再将这个类加载到内存中

结语

反射机制的讲解暂时高一段落,主要是涉及的复杂内容太多了,我一直在努力的把握避免太过复杂的东西塞进来,反射机制的具体作用最好还是先学学后面的内容再来接触,但是不提前了解一点又不太好,所以只好这样七零八落的讲一讲

算了,放张图结束

8ba5aaea c2d1 4bb0 8432 ebf016ee0698


Java的反射机制
http://soulmate.org.cn/2024/12/30/Java的反射机制/
作者
Soul
发布于
2024年12月30日
许可协议