Java安全-反射

Java安全-反射

初步认识

反射是大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性

一段代码,改变其中的变量,将会导致这段代码产生功能性的变化,可以称之为动态特性

PHP本身拥有很多动态特性,所以可以通过“一句话木马”来执行各种功能;Java虽不像PHP那么灵活, 但其提供的“反射”功能,也是可以提供一些动态特性。

1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
  • 获取类的方法: forName
  • 实例化类对象的方法: newInstance
  • 获取函数的方法: getMethod getConstructor
  • 执行函数的方法: invoke

对反射的理解

正射与反射

正射

我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。

1
2
Student student = new Student();
student.doHomework("数学");

反射

反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。我们以这一段经典的反射代码为例说明。

1
2
3
4
public static void main(String[] args) throws Exception{  
Person person = new Person();
Class c = person.getClass();
}

我们注意到在代码块中出现了大写 C 开头的 Class;

理解反射的第一步就必须先搞清楚 Class 是什么。

Java Class 对象理解

我们程序在运行的时候会编译生成一个 .class 文件,而这个 .class 文件中的内容就是相对应的类的所有信息,比如这段程序当中:

1
2
3
4
public static void main(String[] args) throws Exception{  
Person person = new Person();
Class c = person.getClass();
}

其实 person.class 就是 Class,Class 也就是描述类的类。

Class 类的对象作用是运行时提供或获得某个对象的类型信息。

所以反射其实就是操作 Class,看清楚了,是大 C

反射的使用方法

1.实例化对象

实例化对象的核心原因是:要在内存里产生一个具体的实体,才能存储和操作该类的属性、调用它的非静态方法

非静态方法需要对象才能调用

  • 静态方法(static)跟类绑定,不需要对象就能用。
  • 非静态方法依赖实例数据,所以必须先有一个对象(实例)才能执行

对于普通用户我们可以采用以下方法创建实例:

1
Person test = new Person();

而我们在创建 Class 类的实例对象却不能使用上述方法,运行会抛出错误

1
Class test = new Class();

因为 Class 类是 private 私有属性,我们也无法通过创建对象的方式来获取 class 对象,那么我们怎样才能够获取到 class 对象呢?一般我们获取 class 对象就有以下三种方法,我们来逐一看看。

方法一、实例化对象的getClass()方法

如果上下⽂中存在某个类的实例 obj,那么我们可以通过 obj.getClass 来获取它的类。

1
2
TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();

方法二、 使用类的 .class 方法

如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。

1
Class class2 = TestReflection.class;

方法三、Class.forName(String className):

如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取,后续要利用的话是需要实例化的。

1
Class class1 = Class.forName("reflection.TestReflection");

2. 获取成员变量 Field

获取成员变量Field位于 java.lang.reflect.Field 包中

Field[] getFields() :获取所有 public 修饰的成员变量

Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符

Field getField(String name) 获取指定名称的 public 修饰的成员变量

Field getDeclaredField(String name) 获取指定的成员变量

3. 获取成员方法 Method(都不包含构造函数)

1
2
3
4
5
6
7
8
9
Method getMethod(String name, 类<?>... parameterTypes) //返回该类所声明的public方法

Method getDeclaredMethod(String name, 类<?>... parameterTypes) //返回该类所声明的所有方法

//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型

Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

Method[] getDeclaredMethods() // 获取该类中的所有声明方法,包括私有的,不包括父类继承的

Person.java 中添加如下代码

1
2
3
4
5
6
7
public void study(String s) {  
System.out.println("学习中..." + s);
}
private String sleep(int age) {
System.out.println("睡眠中..." + age);
return "sleep";
}

并在 ReflectionTest02.java 中添加如下

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
package src;  

import com.sun.xml.internal.ws.encoding.MtomCodec;

import java.lang.reflect.Method;

public class ReflectionTest02 {
public static void main(String[] args) throws Exception{
Class c1 = Class.forName("src.Person");// 创建 Class 对象
Method[] methods1 = c1.getDeclaredMethods();// 获取所有该类中的所有方法
Method[] methods2 = c1.getMethods();// 获取所有的 public 方法,包括类自身声明的 public 方法,父类中的 、实现的接口方法

for (Method m:methods1){
System.out.println(m);
}
System.out.println("-------分割线---------");

for (Method m:methods2) {
System.out.println(m);
}

System.out.println("-------分割线---------");

Method methods3 = c1.getMethod("study", String.class);// 获取 Public 的 study 方法
System.out.println(methods3);
System.out.println("-------分割线---------");

Method methods4 = c1.getDeclaredMethod("sleep", int.class); // 获取 Private 的 sleep 方法
System.out.println(methods4);
}

}

4. 获取构造函数

1
2
3
4
5
6
7
Constructor<?>[] getConstructors() :只返回public构造函数

Constructor<?>[] getDeclaredConstructors() :返回所有生命的构造函数

Constructor<> getConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的public构造函数

Constructor<> getDeclaredConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的构造函数

反射涉及的方法

forName方法

forName 不不是获取“类”的唯一途径,通常来说我们有如下三种方式获取一个“类”,也就 是java.lang.Class对象:

  • obj.getClass() 如果上下文中存在某个类的实例obj ,那么我们可以直接通过obj.getClass() 来获取它的类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个方法其实不属于反射。
  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使用 forName 来获取

在安全研究中,我们使用反射的一大目的,就是绕过某些沙盒。比如,上下文中如果只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许可以这样(伪代码):

1
1.getClass().forName("java.lang.Runtime")

关于绕沙盒,之前Code-Breaking 2018p神出了一道SpEL的题目(学习SpEL注入时还用来做实例记录了)

Code-Breaking Puzzles — javacon WriteUp – Ruilin

当时的payload(弹出计算器)

1
#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}

forName有两个函数重载:

  • Class forName(String name)
  • Class forName(String name, boolean initialize, ClassLoader loader)

第一个就是我们最常见的获取class的方式,其实可以理解为第二种方式的一个封装

1
2
3
Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

默认情况下, forName 的第一个参数是类名;第二个参数表示是否初始化;第三个参数就 是ClassLoader

第三个参数ClassLoader

ClassLoader 就是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的ClassLoader是根据类名加载类,这个类名是类完整路径,如 java.lang.Runtime 。

第二个参数initialize:

在 forName 的时候,构造函数并不会执行,即使我们设置initialize=true。

那么这个初始化究竟指什么呢?

可以将这个“初始化”理解为类的初始化。我们先来看看如下这个类:

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

三个“初始化”方法有什么区别,调用顺序是什么,在安全上有什么价值?

首先调用的是static {},其次是{},最后是构造函数。

其中,static {}就是在“类初始化”的时候调用的,而{}中的代码会放在构造函数的super()[调用父类的构造方法]后面, 但在当前构造函数内容的前面。所以说,forName中的initialize=true其实就是告诉Java虚拟机是否执行”类初始化“。

那么,假设我们有如下函数,其中函数的参数name可控

1
2
3
4
5
6
7
8
public void ref(String name) throws Exception {
Class.forName(name);
}
---
静态方法调用,直接用类名调用。
作用:根据类的全限定名(包名 + 类名),加载并初始化类(会执行静态代码块)。
name 必须是类的全限定名,例如 "com.example.MyClass"
Class.forName() 属于 java.lang.Class,是一个静态方法,不需要对象实例。

我们就可以编写一个恶意类,将恶意代码放置在static {}中,从而执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。

newInstance方法(无参构造函数,单例模式静态方法)

我们经常在一些源码里看到,类名的部分包含$符号,比如fastjson在checkAutoType 时候就会先将$替换成 . :

1
String className = typeName.replace('$', '.');

$ 的作用是查找内部类。

Java的普通类 C1 中支持编写内部类C2,而在编译的时候,会生成两个文件: C1.class 和 C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName(“C1$C2”) 即可加载这个内部类。

获得类以后,我们可以继续使用反射来获取这个类中的属性,方法,也可以实例化这个类,并调用方法。

class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数是私有的

最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但 我们不能直接这样来执行命令:

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

会报错

原因是 Runtime 类的构造方法是私有的

单例模式

类的构造方法是私有的

比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取

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
 public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}


//private static TrainDB instance = new TrainDB();
// ↑ 这里在类加载时自动调用私有构造方法
//调用构造方法
public class Main {
public static void main(String[] args) {
// 正确获取实例的方式
TrainDB db1 = TrainDB.getInstance();
TrainDB db2 = TrainDB.getInstance();

// 验证确实是同一个实例
System.out.println(db1 == db2); // 输出 true

// 错误尝试(无法编译):
// TrainDB db3 = new TrainDB();
}
}

这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建立多个数据库连接。

Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。我们将上述Payload进行修改即可正常执行命令了:

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",
String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),
"calc.exe");

getMethod方法(公有方法)

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。Java中 支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用getMethod的时候,我们需要传给他你需要获取的函数的参数类型列表。

比如这里的 Runtime.exec 方法有6个重载:

我们使用第一个,它只有一个参数,类型是String,所以我们使用 getMethod(“exec”, String.class) 来获取 Runtime.exec 方法。

invoke方法

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]…),其实在反射里就是 method.invoke([1], [2], [3], [4]…)。 所以我们将上述命令执行的Payload分解一下就是:

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

getConstructor方法(构造函数)

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。

获取到构造函数后,我们使用 newInstance 来执行。

比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start() 来执行命令:

ProcessBuilder有两个构造函数:

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

下面用到了第一个形式的构造函数,所以在 getConstructor 的时候传入的是 List.class 。

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

通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。

可变长参数 varargs

那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反射执行呢?

这又涉及到Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变的”。

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):

1
2
3
4
5
public void hello(String[] names) {}
public void hello(String...names) {}
-----
String[] names = {"hello", "world"};
hello(names);

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。

所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();

完全反射编写

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(
new String[][]{{"calc.exe"}}));

getDeclared 系列的反射

如果一个方法或构造方法是私有方法,我们是否能执行它呢?

getDeclared 系列的反射,与普通的 getMethod 、 getConstructor 区别是:

  • getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和 getConstructor 类似

eg. Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例化对象

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。

Java 命令执行的三种方式

1. 调用 Runtime 类进行命令执行

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

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

// 使用 Runtime 类进行命令执行
public class RuntimeExec {
public static void main(String[] args) throws Exception {
InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();
byte[] cache = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}
//�ʵ۵��»�\zhr_0
  1. 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。
  2. 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。
  3. 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream

我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中

这一段代码用来保存运行结果

1
2
3
4
5
6
7
8
9
byte[] cache = new byte[1024];  
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
/**
* readLen用于存储每次读取输入流的长度
*/
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}

2. ProcessBuilder

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


import java.io.ByteArrayOutputStream;
import java.io.InputStream;

// 使用 ProcessBuilder 进行命令执行操作
public class ProcessBuilderExec {
public static void main(String[] args) throws Exception{
InputStream inputStream = new ProcessBuilder("ipconfig").start().getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((readLen = inputStream.read(cache)) != -1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}

3. 使用 ProcessImpl

ProcessImpl 是更为底层的实现,RuntimeProcessBuilder 执行命令实际上也是调用了 ProcessImpl 这个类,对于 ProcessImpl 类我们不能直接调用,但是可以通过反射来间接调用 ProcessImpl 来达到执行命令的目的。

  • 因为 ProcessImpl 是私有的方法
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 src.CommandExec;  


import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Map;

// 使用 ProcessImpl 进行命令执行
public class ProcessImplExec {
public static void main(String[] args) throws Exception{
String[] cmds = new String[]{"whoami"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class,
ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
InputStream inputStream = e.getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((readLen = inputStream.read(cache)) != -1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}

Java 反射修改 static final 修饰的字段

private

PrivatePerson.java

1
2
3
4
5
6
7
  public class PrivatePerson {  
private StringBuilder name = new StringBuilder("1");

public void printName() {
System.out.println(name);
}
}

PrivateReflect.java

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

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PrivateReflect {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, InvocationTargetException {
Class c = Class.forName("src.PrivatePerson");
Object m = c.newInstance();
Method PrintMethod = c.getMethod("printName");
PrintMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m, new StringBuilder("111"));
PrintMethod.invoke(m);
}
}

static

StaticPerson.java

1
2
3
4
5
6
7
8
public class StaticPerson {  
private static StringBuilder name = new StringBuilder("1");

public void printInfo() {
System.out.println(name);

}
}

StaticReflect.java

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

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class StaticReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.StaticPerson");
Object m = c.newInstance();
Method nameMethod = c.getDeclaredMethod("printInfo");
nameMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m,new StringBuilder("static"));
nameMethod.invoke(m);
}
}

final

final 字段能否修改,有且取决于字段是直接赋值还是间接赋值(编译时赋值和运行时赋值的区别)。直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。

直接赋值(无法修改)

FinalStraightPerson.java

1
2
3
4
5
6
7
8
9
10
public class FinalStraightPerson {  

private final String name = "1";
public final int age = 20-2;

public void printInfo() {
System.out.println(name+" "+age);

}
}

FinalStraightReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FinalStraightReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.FinalStraightPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
nameField.setAccessible(true);
ageField.setAccessible(true);
nameField.set(m,"2");
ageField.set(m,"10");

printMethod.invoke(m);
}
}

这个报错本质上不是 因为 final,而是类型不匹配。

  • 要修改的字段 age 类型是 int基本类型)。

  • 但是用反射 set(m, "10") 传进去的是 String

  • 反射不做自动类型转换,所以直接报错。

修改语句

1
ageField.set(m, Integer.parseInt("10"));

运行代码后看到name与age仍未被修改

JVM 对 final 字段的缓存(Unsafe & 反射限制)

  • Java 的 Field.setAccessible(true) 虽然可以突破访问控制,但对 final 修饰的字段,JVM 可能会在对象创建后把值存到寄存器或内存缓存里。
  • 这意味着即使你用反射修改了字段的底层内存,后续读取可能依旧从缓存里取旧值。
  • 另外,final 字段在 JDK 1.8 里,反射修改默认是无效的(除非用 UnsafeField.modifiers 去掉 final 标志)。

但是使用UnsafeField.modifiers 去掉 final 标志大概率也是不可行的

JVM 可能已经把原值内联优化了,尤其是基本类型(intlong 等)和 String 常量——它们在编译期或类加载后就被当作不可变常量处理了。

在 JDK 8(你用的是 1.8.0_112)里,有几个关键点导致你看不到修改效果:

  1. JIT 常量折叠(constant folding)
    • final 基本类型字段一旦被初始化,JVM 在运行时会直接把它当常量用,调用时直接从寄存器/常量池取,而不是重新读字段值。
    • 所以即便你反射 set 成新值,后面的调用仍然用的是缓存的旧值。
  2. 缓存的 Field 值
    • 某些情况下,JVM 会在第一次访问后把 final 字段的值缓存起来,后续不会再真正访问内存。
  3. 你的 printInfo() 可能直接使用了编译期内联值
    • 如果 printInfo() 方法在同一个类里直接访问了 final int age,编译器在生成字节码时已经把值“写死”了。

间接赋值

InDirectPerson.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InDirectPerson {  
private final StringBuilder sex = new StringBuilder("male");
// 经过逻辑判断产生的变量赋值
public final int age = (null!=null?18:18);
// 通过构造函数进行赋值
private final String name;
public InDirectPerson(){
name = "1";
}

public void printInfo() {
System.out.println(name+" "+age+" "+sex);

}
}

InDirectReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package src;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class InDirectReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.InDirectPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
Field sexField = c.getDeclaredField("sex");
nameField.setAccessible(true);
ageField.setAccessible(true);
sexField.setAccessible(true);
nameField.set(m,"2");
ageField.set(m,180);
sexField.set(m,new StringBuilder("female"));
printMethod.invoke(m);
}
}

成功修改

static + final

使用 static final 修饰符的 name 属性,并且是间接赋值,直接通过反射修改是不可以的。

如果 final 字段是 编译期常量static final 并且是基本类型或 String 且值已知),编译器会直接把值内联到使用它的地方。
即使你用反射改了字段值,原来引用它的地方读的仍然是编译期替换进去的值。

这里我们需要通过反射, 把 nameField 的 final 修饰符去掉,再赋值。

StaticFinalPerson.java

1
2
3
4
5
6
7
8
public class StaticFinalPerson {  
static final StringBuilder name = new StringBuilder("1");

public void printInfo() {
System.out.println(name);

}
}

StaticFinalReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StaticFinalReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticFinalPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
Field nameModifyField = nameField.getClass().getDeclaredField("modifiers");
nameModifyField.setAccessible(true);
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
nameField.set(m,new StringBuilder("2"));
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
printMethod.invoke(m);
}
}


Java安全-反射
http://huang-d1.github.io/2025/08/14/Java安全-反射/
作者
huangdi
发布于
2025年8月14日
许可协议