共计 5501 个字符,预计需要花费 14 分钟才能阅读完成。
前言
Java 反射真是太伟大了哈哈哈哈哈哈

为了更加系统性学习 Java,我应该会把学习笔记记录下来发到博客慢慢记录我学 Java 的过程
什么是 Java 反射?
在常规的 Java 开发中,我们必须先有类(.class 文件),然后才能在代码里通过 new 来创建对象。而 反射 的出现,本质上是为了让程序能够 在运行状态中,知道一个类的所有属性和方法,并能动态地调用它们。
这么说可能有点抽象,我们来举个例子
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
// Main.java
import com.itranswarp.learnjava.Person;
public class Main {String getFullName(Person p) {return p.getFirstName() + " " + p.getLastName();}
}
那我问你,假设你需要调用一个你不知道的实例怎么办?
为了解决这种问题诞生了反射
我们用一个实际场景来演示反射的作用
- 场景:你在开发一个图像处理软件,支持多种滤镜(黑白、复古、模糊)。
- 需求: 用户只需要在配置文件里改个名字,程序就能自动切换滤镜,而 不需要修改主程序的代码,也不需要重新编译。
不用反射的情况下:
public class ImageProcessor {public void applyFilter(String filterName) {
// 如果没有反射,你必须预知所有的滤镜类
if (filterName.equals("BlackWhite")) {new BlackWhiteFilter().execute();} else if (filterName.equals("Retro")) {new RetroFilter().execute();} else {System.out.println("未知滤镜");
}
}
}
你用了一堆 if-else 写死情况
使用反射:
import java.lang.reflect.Method;
public class ImageProcessor {public void applyFilter(String className) {
try {
// 1. 根据字符串动态加载类(即便这个类现在还没写出来)Class clazz = Class.forName(className);
// 2. 动态创建对象
Object filterInstance = clazz.getDeclaredConstructor().newInstance();
// 3. 动态获取并执行方法
Method method = clazz.getMethod("execute");
method.invoke(filterInstance);
} catch (Exception e) {System.out.println("滤镜加载失败:" + e.getMessage());
}
}
}
第一种方法叫正射,通过 new 关键字来直接初始化新建实例。
第二种方法就是反射,动态生成类。
通过反射调用方法
上面的演示代码中有四个十分重要的方法
- 获取类的⽅方法:forName
- 实例例化类对象的⽅方法:newInstance
- 获取函数的⽅方法:getMethod
- 执⾏行行函数的⽅方法:invoke
调用非静态方法的时候都必须要传入一个实例
刚刚的演示我们使用的是 forName 来获取 Class 类,除此之外还有 getClass() 方法或者已经加载了这个类的话还可以用 Test.Class 来获取这个类
package org.example;
public class getclz {public static void main(String[] args) {
try {
String abc = "abc";
Class clz1 = abc.getClass();
Class clz2 = String.class;
Class clz3 = Class.forName("java.lang.String");
System.out.println(clz1 == clz2);
System.out.println(clz1 == clz3);
System.out.println(clz1);
System.out.println(clz2);
System.out.println(clz3);
}catch (Exception e){}}
}
/*
true
true
class java.lang.String
class java.lang.String
class java.lang.String
*/
反射在 Java 安全中是十分重要的,我们后面学到的 Java 反序列化以及命令执行等都需要用到 Java 反射
通过 Java 反射进行命令执行
假设我们现在有一个 String 的对象, 如何利用反射实现命令执行?
package org.example;
public class getclz {public static void main(String[] args) {
try {
String abc = "abc";
System.out.println(abc.getClass().getClass().forName("java.lang.Runtime").getMethod("exec", String.class).invoke(abc.getClass().getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null), "calc"
)
);
} catch (Exception e) {}}
}
总的来说我们反正要先获取 class 类,实例化对象然后调用 exec 方法(传入获取的对象和命令)
在上面的 payload 中我们使用了 abc.getClass().getClass() 获取 class 类,再使用 forName 与 getMethod 成功获取 Runtime 类以及调用 exec 方法,但是在 Java 中除了静态方法都需要传入实例对象作为参数。
所以 exec 的第一个参数是通过 abc.getClass().getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null) 获取的程序的实例,然后第二个参数才是我们要执行的命令
ClassLoader 机制
之前我们用到了 forName()方法获取我们想要的类,但翻一下 Java 开发手册就知道,forName 还有一个三参数的重载方法

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
其中有三个参数
1. String name(类全限定名)
这是最基础的参数,代表你想要加载的类的名字。
- 格式要求 :必须是 全限定名(Fully Qualified Name),例如
java.lang.String或com.mysql.cj.jdbc.Driver。 - 注意:如果是内部类,需要使用
$符号。例如OuterClass$InnerClass。
2. boolean initialize(是否初始化)
这个参数决定了类在加载后,是否立即执行 初始化(Initialization)。
在 Java 类加载的过程中,主要分为:加载 -> 链接(验证、准备、解析)-> 初始化。
true(默认值):加载类后,立即执行类的 静态代码块 (static {})并为 静态变量 赋初始值。false:只将类加载到 JVM 中并完成链接,但不执行静态代码块。
为什么这个参数很重要?
以 JDBC 驱动加载为例:当调用
Class.forName("com.mysql.cj.jdbc.Driver")时,默认initialize为true,这会触发驱动类里的静态代码块,从而自动向DriverManager注册自己。
3. ClassLoader loader(类加载器)
这个参数指定使用哪个类加载器来加载该类。
- 作用 :在 Java 中,类的唯一性是由 类全限定名 + 类加载器 共同决定的。同一个类文件如果被不同的类加载器加载,在 JVM 中会被视为两个不同的类。
-
常见选择:
Thread.currentThread().getContextClassLoader():当前线程上下文加载器(最常用)。TargetClass.class.getClassLoader():使用某个已知类的加载器。null:使用根类加载器(Bootstrap ClassLoader)。
我们来结合实际来讲一下剩下两个参数的作用
initialize
public class ForNameDemo {public static void main(String[] args) throws Exception {
String className = "SecretClass";
System.out.println("--- 场景 A: initialize = false ---");
// 只加载类,不初始化
Class<?> clazz1 = Class.forName(className, false, ForNameDemo.class.getClassLoader());
System.out.println("类已加载:" + clazz1.getName());
System.out.println("n--- 场景 B: initialize = true ---");
// 加载并初始化
Class<?> clazz2 = Class.forName(className, true, ForNameDemo.class.getClassLoader());
System.out.println("类已加载:" + clazz2.getName());
}
}
class SecretClass {
static {
// 如果这个块运行了,说明类被“初始化”了
System.out.println("【警报】SecretClass 的静态代码块被执行了!");
}
}
/*
--- 场景 A: initialize = false ---
类已加载: SecretClass
--- 场景 B: initialize = true ---【警报】SecretClass 的静态代码块被执行了!类已加载: SecretClass
*/
ClassLoader
//LoaderDemo.java
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class LoaderDemo {public static void main(String[] args) throws Exception {
String className = "SecretPayload";
// 1. 获取当前标准的 AppClassLoader
ClassLoader appLoader = LoaderDemo.class.getClassLoader();
// 2. 创建一个自定义加载器,指向我们存放 class 的秘密目录
File file = new File("D:/test/"); // 这里改为你存放 class 的实际路径
URL[] urls = new URL[]{file.toURI().toURL()};
ClassLoader customLoader = new URLClassLoader(urls);
System.out.println("--- 场景 A:使用默认加载器 ---");
try {Class.forName(className, true, appLoader);
} catch (ClassNotFoundException e) {System.err.println("失败:默认加载器找不到这个类,因为它不在项目的 Classpath 里。");
}
System.out.println("n--- 场景 B:使用自定义加载器 (模拟 RCE) ---");
try {
// 这里我们传入了 customLoader
Class<?> clazz = Class.forName(className, true, customLoader);
System.out.println("成功:使用自定义加载器找到了类" + clazz.getName());
} catch (ClassNotFoundException e) {System.err.println("失败:路径配置错误,请检查 D:/test/ 是否有 SecretPayload.class");
}
}
}
//SecretPayload.java
public class SecretPayload {
static {System.out.println("n[!!!] 远程代码执行成功!");
System.out.println("加载器 ID:" + SecretPayload.class.getClassLoader());
}
}
将 SecretPayload.java 编译成 class 后移动到目标目录(不要在 classpath 或者和 LoaderDemo.class 在同一目录)
执行后会有
/*
--- 场景 A:使用默认加载器 ---
失败:默认加载器找不到这个类,因为它不在项目的 Classpath 里。--- 场景 B:使用自定义加载器 (模拟 RCE) ---
[!!!] 远程代码执行成功!加载器 ID: java.net.URLClassLoader@626b2d4a
成功:使用自定义加载器找到了类 SecretPayload
*/
ClassLoader 这里有很多的利用空间,这里暂时不做过多讲解。