类加载之forName作用域
前言 Java 源码中经常会使用的 forName 去加载类。在学习 Java 安全中,会遇到各种形式的 forName 去获取类,但是却不知所云,为什么这种形式写法可以加载到目标类,但另一种形式写法不能加载到? 经在网上搜索,没有找到明确指出 forName 是如何加载类,且所能加载类的范围的文章。 所以本篇将介绍 forName 的作用域。首先将介绍几种类加载器 ,并不过多赘述,但确是必要的;
前言
Java 源码中经常会使用的 forName 去加载类。在学习 Java 安全中,会遇到各种形式的 forName 去获取类,但是却不知所云,为什么这种形式写法可以加载到目标类,但另一种形式写法不能加载到?
经在网上搜索,没有找到明确指出 forName 是如何加载类,且所能加载类的范围的文章。
所以本篇将介绍 forName 的作用域。首先将介绍几种类加载器 ,并不过多赘述,但确是必要的;之后将直接进入正题。
有不对之处,请各位师傅指教斧正
类加载器
类加载器的文章,网上有很多,不再赘述,但学习本文,我们需要知道类加载器的种类和关系:
类加载器的关系

类加载器种类
- BootstrapClassLoader,启动类加载器/根加载器。负责将存放在
<JAVA_HOME>\lib目录中的,或被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机可识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。由于是C/C++代码实现的,故启动类加载器无法被 Java 程序直接引用。 - ExtClassLoader,扩展类加载器,位于sun.misc.Launcher$ExtClassLoader。负责将 <java_home>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,库名通常以 javax 开头,开发者可以直接使用扩展类加载器。</java_home>
- AppClassLoader,应用程序类加载器(系统类加载器)。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
- 自定义类加载器
简单来说,这三种类加载器加载的 jar 包中的类,应该是我们执行代码时所能使用的全部的类。
我们可以通过如下代码查看类加载器所加载的jar包:
import java.net.URL;
import java.net.URLClassLoader;
public class printPath {
public static void main(String[] args) {
URL[] urls;
System.out.println("BootstrapClassLoader Load Path:");
urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
System.out.println("ExtClassLoader Load Path:");
urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
System.out.println("AppClassLoader Load Path:");
urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
}

如图,按照上文所说:bootstrapClassloader加载 lib 目录下的jar包;extClassLoader加载 lib/ext 目录下的jar包;appClassLoader加载 classPath 参数指定的jar包。(这里我们可以简单的将各个 jar 包内的类按加载器进行分类,这将在后文中提到。)
并且我们可以还通过双亲委派机制获取这三种类加载器(这也将在后文用到)
public class getClassLoader {
public static void main(String[] args) {
ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader extClassLoader = Thread.currentThread().getContextClassLoader().getParent();
ClassLoader bootStrapClassLoader = Thread.currentThread().getContextClassLoader().getParent().getParent();
System.out.println(appClassLoader);
System.out.println(extClassLoader);
System.out.println(bootStrapClassLoader);
}
}

forName
forName在 Class.java 中有两种重载
第一种
public static Class<?> forName(String className)

第二种
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

因为第一种重载是第二种重载的一种用法的简写,所以我们重点关注第二种重载。
重载二
在不关注initialize参数的情况下,第二种有三种用法:
用法一,使用BootstrapClassloader加载器
public static Class<?> forName(String name, true, BootstrapClassloader)
用法二,使用ExtClassloader加载器
public static Class<?> forName(String name, true, ExtClassloader)
用法三,使用AppClassloader加载器
public static Class<?> forName(String name, true, ExtClassloader)
我们直接说结果:forName这三种用法的作用域,是其对应加载器的作用域(注意存在双亲委派机制)
举个例子:
public class forNameTest {
public static void main(String[] args) {
ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader extClassLoader = Thread.currentThread().getContextClassLoader().getParent();
ClassLoader bootStrapClassLoader = Thread.currentThread().getContextClassLoader().getParent().getParent();
System.out.println(appClassLoader);
System.out.println(extClassLoader);
System.out.println(bootStrapClassLoader);
String name1 = "apple.applescript.AppleScriptEngine";
String name2 = "sun.security.ec.CurveDB";
String name3 = "com.intellij.rt.ant.execution.AntMain2";
String name = name1;
try{
Class.forName(name,false, bootStrapClassLoader);
}catch (Exception E){
System.out.println("bootStrapClassLoader加载器加载失败");
}
try{
Class.forName(name,false, extClassLoader);
}catch (Exception E){
System.out.println("extClassLoader加载器加载失败");
}
try{
Class.forName(name,false,appClassLoader);
}catch (Exception E){
System.out.println("appClassLoader加载器加载失败");
}
}
}
字符串变量 name1(AppleScriptEngine)是 bootstrapClassloader 加载的 rt.jar 包中的类全称
字符串变量 name2(CurveDB)是 extClassloader 加载的 sunec.jar 包中的类全称
字符串变量 name3(AntMain2)是 appclassLoader加载的 idea_rt.jar 包中的类全称。
令 name = name1,结果如下:

因为 name1(AppleScriptEngine)归属的 rt.jar 包由 bootstrapClassloader 加载。根据类加载的双亲委派机制,extClassLoader 加载 name1,会让父加载器 bootstrapClassloader 加载;同理 appClassLoader 加载 name1,会让父加载器 extCLassLoader 加载,extClassLoader 再让父加载器 bootstrapClassLoader 加载,故均成功。
令 name = name2,结果如下:

因为 name2(CurveDB)归属的 sunec.jar 包由 extClassLoader 加载。根据双亲委派机制,forName 调用其父加载器 bootstrapClassloader 无法加载该类,故报错;appClassLoader 加载器 name2(CurveDB)会调用其父加载器 extClassLoader 加载该类,会成功。
令 name = nam3,结果如下:

原理同上,name3(AntMain2)归属的的 idea_rt.jar 由 appclassLoader 加载。根据双亲委派机制,父加载器 extClassLoader及bootstrapClassLoader均无法加载该类,故报错。
重载一
现在我们关注第一种重载,只有两行代码
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
重点是ClassLoader.getClassLoader(caller),简单来说就是获取调用类的加载器,然后使用该加载器去加载类。根据不同的类加载器,其加载类的范围与重载二一节中所描述的情况类似。
实例
在学习 0ctf2022 onlyjdk 时,看到文章提到:
然后就是一个大坑。。。
因为classLoader的原因 ,在SwingLazyValue这里只能加载rt.jar 里面的类而我找的jdk.nashorn.internal.codegen.DumpBytecode.dumpBytecode 位于nashorn.jar 里面
无法加载,在这里卡了很久 ,后来看到了个 ProxyLazyValue.createValue
这里无法加载 DumpBytecode 类的原因即是 classLoader 为null,即 bootstrapClassLoader 的情况下,forName 无法加载nashorn.jar 这个被扩展类加载器 ExtClassLoader 加载的 jar 包中的类文件。
文章作者也给出了解决办法
无法加载,在这里卡了很久 ,后来看到了个
ProxyLazyValue.createValuepublic Object createValue(final UIDefaults table) { if (acc == null && System.getSecurityManager() != null) { throw new SecurityException("null AccessControlContext"); } return AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { Class<?> c; Object cl; if (table == null || !((cl = table.get("ClassLoader")) instanceof ClassLoader)) { cl = Thread.currentThread(). getContextClassLoader(); if (cl == null) { cl = ClassLoader.getSystemClassLoader(); } } ReflectUtil.checkPackageAccess(className); c = Class.forName(className, true, (ClassLoader)cl); SwingUtilities2.checkAccess(c.getModifiers()); if (methodName != null) { Class[] types = getClassArray(args); Method m = c.getMethod(methodName, types); return MethodUtil.invoke(m, c, args); } else {获取到classLoader ,所以就能正常加载jdk 里面nashorn.jar 这些里面的类了
ProxyLazyValue.createValue 方法内获取到的 classLoader 为 AppClassLoader

根据双亲委派机制,可以加载 ExtClassLoader 加载的 nashorn.jar 包中的类 DumpBytecode
总结
forName 作用域,即其所能加载的类的范围,取决于加载器的加载范围。其中加载器一般有以下三个取值:
- null,即 bootstrapCLassLoader,启动类加载器
- extClassLoader 对象,即扩展类加载器
- appClassLoader 对象,即系统类加载器
且对于 forName 的第一种重载形式,加载器是ClassLoader.getClassLoader(caller),即调用类的加载器;对于 forName 的第二种重载形式,加载器是参数指定的。
