【问题标题】:how Java classloaders work for "regular" circumstances (nonexplicit use of classloaders)Java 类加载器如何在“常规”情况下工作(类加载器的非显式使用)
【发布时间】:2009-08-19 16:58:47
【问题描述】:

我正在研究类路径的动态修改。我找到了one solution that works nicely,但它是通过显式调用 addURL() 来实现的。 (大概在启动时)

但是,如果默认类加载器似乎无法找到类,我想在运行时拦截类加载过程以定位类。我尝试将ClassLoader 子类化,因此它只是将findClass()loadClass() 委托给默认值,并打印出一条调试行,告诉我这些方法已被调用,但是当我的类使用依赖类时,它们似乎从未被调用隐式类加载,例如

// regular object instantiation with 'new'
BrowserLauncher launcher;
launcher = new BrowserLauncher();

// static methods
Foobar.doSomethingOrOther();

// Class.forName()
Class cl = Class.forName("foo.bar.baz");

// reflection on a Class object obtained statically
Class<Foobar> cl = Foobar.class;
// do something with cl, like call static methods or newInstance()

类加载在这些情况下如何工作?(与显式调用 Classloader.loadClass() 的简单情况相比)

下面是我对自定义类加载器的尝试。如果我使用带有{"some.package.SomeClass", "foo", "bar", "baz"} 参数列表的 DynClassLoader0.main(),并且 some.package.SomeClass 使用上面列出的方法之一引用了在外部 .jar 文件中找到的其他类,为什么我的 DynClassLoader0 的 findClass()和 loadClass() 被调用?唯一一次调用 loadClass 是在下面的 main() 函数中显式调用 loadClass。

package com.example.test.classloader;

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

public class DynClassLoader0 extends ClassLoader {
    public DynClassLoader0()
    {
        super();
    }
    public DynClassLoader0(ClassLoader parent)
    {
        super(parent);
    }
    public void runMain(String classname, String[] args) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
    {
    // [***] here we explicitly use our classloader.
        Class<?> cl = loadClass(classname);
        Method main = cl.getMethod("main", String[].class);
        main.invoke(null, new Object[] {args});
    }

    @Override protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        System.out.println("findClass("+name+")");
        return super.findClass(name);
    }

    @Override public Class<?> loadClass(String name) throws ClassNotFoundException
    {
        System.out.println("loadClass("+name+")");
        return super.loadClass(name);
    }

    static public void main(String[] args)
    {
        // classname, then args
        if (args.length >= 1)
        {
            String[] classArgs = new String[args.length-1];
            System.arraycopy(args, 1, classArgs, 0, args.length-1);

            ClassLoader currentThreadClassLoader
             = Thread.currentThread().getContextClassLoader();
            DynClassLoader0 classLoader = new DynClassLoader0(currentThreadClassLoader);
            // Replace the thread classloader - assumes
            // you have permissions to do so
            Thread.currentThread().setContextClassLoader(classLoader);

            try {
                classLoader.runMain(args[0], classArgs);
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        else
        {
            System.out.println("usage: DynClassLoader {classname} [arg0] [arg1] ...");
        }
    }
}

编辑:我已经看过这些问题了:

编辑: 我认为下面 kdgregory 所说的是正确的,一旦我明确使用我的类加载器(参见代码中带有 [***] 作为注释的行),所有从该代码执行的代码class 将导致来自同一个类加载器的隐式类加载。然而我的 DynClassLoader0.loadClass() 永远不会被调用,除非在最外层的显式调用期间。

【问题讨论】:

  • 我刚刚意识到为什么你没有看到你所期望的:你的 DynClassLoader0 实际上并没有加载任何类。您从 ClassLoader 子类化,然后简单地委托给超级实现。但是 ClassLoader 本身不加载类,它代表树。因此,您尝试加载的类位于类路径中的其他位置,实际上是由其他类加载器加载的。

标签: java classloader


【解决方案1】:

引用ClassLoader JavaDoc

的方法和构造函数 类加载器创建的对象可能 参考其他类。确定 所指的类,Java 虚拟机调用 loadClass 类加载器的方法 最初创建了这个类。

换句话说,一旦您加载了一个类,该类就会尝试通过加载它的类加载器加载其他类。在一个普通的 Java 应用中,就是系统类加载器,代表传递给 JVM 的类路径,或者是引导类加载器,用于加载 JVM 运行时。

根据您的需要,Class.forName() 有一个变体,它将类加载器作为参数。如果您使用它来加载特定类,则该类中的引用应使用指定的类加载器。


编辑:我开始跟踪您的示例,但决定提供我自己的示例会更容易。如果你要编写自己的类加载器,我建议从现有的URLClassLoader 开始,因为它处理了很多幕后的东西。

因此,MyClassLoader 采用单个 JARfile/目录并单独为该目录加载类。我重写了加载类时调用的三个方法,并简单地记录了它们的调用(使用 System.err,因为它不缓冲输出,与 System.out 不同)。

我的示例使用了我目前正在开发的库;这很方便,但你可以选择任何你想要的库只要它不在你的类路径中

main() 方法通过 MyLoader 加载一个类。然后我在那个类上调用一个方法,我知道它会抛出一个异常,该异常也是库的一部分。请注意,我通过反射调用该方法:由于该库不在我的 Eclipse 类路径中,因此我无法使用显式引用对其进行编译。

当我运行这个程序时(在 Sun JDK 1.5 for Linux 下),我看到很多对 loadClass() 的调用,无论是对于我的库中的类还是类路径中的类。这是意料之中的:ParseUtil 类引用了许多其他类,并将使用 MyLoader(即它的类加载器)来加载它们。对于那些 MyLoader 在本地找不到的类,它会委托加载器树。

抛出异常,当我打印出它的类加载器时,我看到它与我创建的 MyLoader 实例相同。我还打印出 Exception.class 的加载器,它是空的——Class.getClassLoader() 的 JavaDoc 表示它是引导类加载器。

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;


public class ClassLoaderExample
{
    private static class MyClassLoader
    extends URLClassLoader
    {
        public MyClassLoader(String path)
        throws Exception
        {
            super(new URL[] { new File(path).toURL() });
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException
        {
            System.err.println("findClass(" + name + ")");
            return super.findClass(name);
        }

        @Override
        protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            System.err.println("loadClass(" + name + "," + resolve + ")");
            return super.loadClass(name, resolve);
        }

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException
        {
            System.err.println("loadClass(" + name + ")");
            return super.loadClass(name);
        }
    }


    public static void main(String[] argv)
    throws Exception
    {
        ClassLoader myLoader = new MyClassLoader("/home/kgregory/Workspace/PracticalXml-1.1/target/classes/");
        System.out.println("myLoader = " + myLoader);

        Class<?> parseUtilKlass = myLoader.loadClass("net.sf.practicalxml.ParseUtil");
        Method parseMethod = parseUtilKlass.getDeclaredMethod("parse", String.class);

        try
        {
            parseMethod.invoke(null, "not at all valid XML");
        }
        catch (InvocationTargetException e)
        {
            Throwable ee = e.getCause();
            System.out.println("exception:" + ee);
            System.out.println("exception loader = " + ee.getClass().getClassLoader());

            System.out.println("Exception.class loader = " + Exception.class.getClassLoader());
        }
    }
}

根据今天的 cmets 编辑 #2。

类加载器应该在尝试自己完成请求之前将请求委托给它的父级(这在 ClassLoader JavaDoc 中)。这种做法有几个好处,最重要的是您不会无意中加载同一类的不兼容实例。

J2EE 类加载器修改了这个模型:用于加载 WAR 的类加载器将尝试在包含 EAR 的加载器之前解析类,而后者又尝试在容器的类加载器之前解析类。这里的目标是隔离:如果 WAR 和它的 EAR 都包含相同的库,这可能是因为两者需要不同的版本(或者他们有一个草率的构建过程)。即使在 J2EE 案例中,我也相信容器类加载器以标准方式进行委托。

【讨论】:

  • "换句话说,一旦你加载了一个类,该类就会尝试通过加载它的类加载器加载其他类。"这就是我的想法,但它似乎不起作用 8-( 我不知道我做错了什么。
  • 谢谢——我会尝试运行你的程序,看看我的程序有何不同!
  • 啊:我想我可以理解这个问题。如果我使用您的方法并且我加载了一个尚未在类路径中的类,它会正常工作并通过类加载器递归加载所有依赖类。如果我使用你的方法并加载一个已经在类路径上的类,它不会递归加载依赖类。
  • 有谁知道为什么会这样?
【解决方案2】:

在您的代码中,对super.loadClass() 的调用将类的加载委托给父类加载器(只需查看java.lang.ClassLoader#loadClass 的实现)。所以加载类的不是DynClassLoader0 的实例,而是作为构造函数参数传递给DynClassLoader0currentThreadClassLoader(取自Thread.currentThread().getContextClassLoader())。当加载的类引用其他类时,它们也会被该类加载器加载,而不是你的 DynClassLoader0

【讨论】:

  • JVM 不会通过上下文类加载器来加载新的类——我原本以为是这样,但 ClassLoader JavaDoc 说不然,并且测试程序失败了。
  • 我知道我通过调用 super.loadClass() 委托给我的 DynClassLoader0.loadClass() 中的父类加载器——我的问题是为什么 DynClassLoader0.loadClass() 不会被调用。跨度>
  • 等待:我想我开始理解这个困境了。查看 kdgregory 答案中的引用(“类加载器创建的对象的方法和构造函数可能引用其他类。为了确定引用的类,Java 虚拟机调用最初创建的类加载器的 loadClass 方法类。”)有什么方法可以使用默认的类加载器来实现类创建魔法,但是告诉 JVM,不要查看我的类加载器以获取将来可能需要加载的下游类?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-22
  • 2021-12-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多