Java 源码中经常会使用的 forName 去加载类。在学习 Java 安全中,会遇到各种形式的 forName 去获取类,但是却不知所云,为什么这种形式写法可以加载到目标类,但另一种形式写法不能加载到?
经在网上搜索,没有找到明确指出 forName 是如何加载类,且所能加载类的范围的文章。
所以本篇将介绍 forName 的作用域。首先将介绍几种类加载器 ,并不过多赘述,但确是必要的;之后将直接进入正题。
有不对之处,请各位师傅指教斧正
类加载器的文章,网上有很多,不再赘述,但学习本文,我们需要知道类加载器的种类和关系:
类加载器的关系
类加载器种类
<JAVA_HOME>\lib
目录中的,或被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机可识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。由于是C/C++代码实现的,故启动类加载器无法被 Java 程序直接引用。简单来说,这三种类加载器加载的 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在 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.createValue
public 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 作用域,即其所能加载的类的范围,取决于加载器的加载范围。其中加载器一般有以下三个取值:
且对于 forName 的第一种重载形式,加载器是ClassLoader.getClassLoader(caller)
,即调用类的加载器;对于 forName 的第二种重载形式,加载器是参数指定的。