类加载之forName作用域
2023-2-16 09:49:0 Author: xz.aliyun.com(查看原文) 阅读量:16 收藏

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在 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 作用域,即其所能加载的类的范围,取决于加载器的加载范围。其中加载器一般有以下三个取值:

  • null,即 bootstrapCLassLoader,启动类加载器
  • extClassLoader 对象,即扩展类加载器
  • appClassLoader 对象,即系统类加载器

且对于 forName 的第一种重载形式,加载器是ClassLoader.getClassLoader(caller),即调用类的加载器;对于 forName 的第二种重载形式,加载器是参数指定的。


文章来源: https://xz.aliyun.com/t/12170
如有侵权请联系:admin#unsafe.sh