如何在Java中创建父/子/子优先ClassLoader,或如何覆盖已在父CL中加载的旧Xerces版本?

时间:2011-03-26 21:40:53

标签: java classloader

我想创建一个parent-last / child-first类加载器,例如一个类加载器,它首先在子类loder中查找类,然后只委托它的父类ClassLoader来搜索类。

澄清:

我现在知道要获得完整的ClassLoading分离,我需要使用类似URLClassLoader的东西,因为它的父级传递null,感谢this answer到我之前的问题

然而,目前的问题来帮我解决这个问题:

  1. 我的代码+依赖的jar正被加载到现有系统中,使用ClassLoader将System的ClassLoader设置为它的父级(URLClassLoader)

  2. 该系统使用的某些版本的库与我需要的版本不兼容(例如,旧版本的Xerces,不允许我运行我的代码)

  3. 如果单独运行,我的代码运行完全正常,但如果从该ClassLoader运行则会失败

  4. Howerver我确实需要访问父类ClassLoader中的许多其他类

  5. 因此我想允许我使用自己的Override,父类加载器“jars”:如果在子类加载器中找到我调用的类(例如,我用自己的jar提供了一个更新版本的Xerces ,而不是加载我的代码和罐子的ClassLoader的一个用户。

  6. 这是加载我的代码+ Jars的系统代码(我无法更改此代码)

    File addOnFolder = new File("/addOns"); 
    URL url = addOnFolder.toURL();         
    URL[] urls = new URL[]{url};
    ClassLoader parent = getClass().getClassLoader();
    cl = URLClassLoader.newInstance(urls, parent);
    

    这是“我的”代码(完全取自Flying Sauser“Hello World”代码演示):

    package flyingsaucerpdf;
    
    import java.io.*;
    import com.lowagie.text.DocumentException;
    import org.xhtmlrenderer.pdf.ITextRenderer;
    
    public class FirstDoc {
    
        public static void main(String[] args) 
                throws IOException, DocumentException {
    
            String f = new File("sample.xhtml").getAbsolutePath();
            System.out.println(f);
            //if(true) return;
            String inputFile = "sample.html";
            String url = new File(inputFile).toURI().toURL().toString();
            String outputFile = "firstdoc.pdf";
            OutputStream os = new FileOutputStream(outputFile);
    
            ITextRenderer renderer = new ITextRenderer();
            renderer.setDocument(url);
            renderer.layout();
            renderer.createPDF(os);
    
            os.close();
        }
    }
    

    这可以独立运行(运行main)但在通过父CL加载时失败并显示此错误:

      

    org.w3c.dom.DOMException:NAMESPACE_ERR:尝试进行   以某种方式创建或更改对象   这是不正确的   命名空间。

    可能是因为父系统使用旧版本的Xerces,即使我在/ addOns文件夹中提供了正确的Xerces jar,因为它的类已经被父系统加载和使用,它不允许我自己的由于委托的方向,使用我自己的jar的代码。我希望这会让我的问题更清楚,我确信它已经被问到了 之前。 (也许我没有问正确的问题)

6 个答案:

答案 0 :(得分:28)

今天是你的幸运日,因为我必须解决这个问题。我警告你,班级装载的内脏是一个可怕的地方。这样做让我觉得Java的设计者从未想过你可能想要一个父级的最后一个类加载器。

使用只提供包含类或jar的URL列表,以便在子类加载器中可用。

/**
 * A parent-last classloader that will try the child classloader first and then the parent.
 * This takes a fair bit of doing because java really prefers parent-first.
 * 
 * For those not familiar with class loading trickery, be wary
 */
private static class ParentLastURLClassLoader extends ClassLoader 
{
    private ChildURLClassLoader childClassLoader;

    /**
     * This class allows me to call findClass on a classloader
     */
    private static class FindClassClassLoader extends ClassLoader
    {
        public FindClassClassLoader(ClassLoader parent)
        {
            super(parent);
        }

        @Override
        public Class<?> findClass(String name) throws ClassNotFoundException
        {
            return super.findClass(name);
        }
    }

    /**
     * This class delegates (child then parent) for the findClass method for a URLClassLoader.
     * We need this because findClass is protected in URLClassLoader
     */
    private static class ChildURLClassLoader extends URLClassLoader
    {
        private FindClassClassLoader realParent;

        public ChildURLClassLoader( URL[] urls, FindClassClassLoader realParent )
        {
            super(urls, null);

            this.realParent = realParent;
        }

        @Override
        public Class<?> findClass(String name) throws ClassNotFoundException
        {
            try
            {
                // first try to use the URLClassLoader findClass
                return super.findClass(name);
            }
            catch( ClassNotFoundException e )
            {
                // if that fails, we ask our real parent classloader to load the class (we give up)
                return realParent.loadClass(name);
            }
        }
    }

    public ParentLastURLClassLoader(List<URL> classpath)
    {
        super(Thread.currentThread().getContextClassLoader());

        URL[] urls = classpath.toArray(new URL[classpath.size()]);

        childClassLoader = new ChildURLClassLoader( urls, new FindClassClassLoader(this.getParent()) );
    }

    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        try
        {
            // first we try to find a class inside the child classloader
            return childClassLoader.findClass(name);
        }
        catch( ClassNotFoundException e )
        {
            // didn't find it, try the parent
            return super.loadClass(name, resolve);
        }
    }
}

编辑:Sergio和ɹoƃı已经指出,如果你使用相同的类名调用.loadClass,你将得到一个LinkageError。虽然这是真的,但是这个类加载器的正常用例是将它设置为线程的类加载器Thread.currentThread().setContextClassLoader()或通过Class.forName(),并按原样运行。

但是,如果直接需要.loadClass(),则可以在顶部的ChildURLClassLoader findClass方法中添加此代码。

                Class<?> loaded = super.findLoadedClass(name);
                if( loaded != null )
                    return loaded;

答案 1 :(得分:18)

以下代码是我使用的代码。它具有优于其他答案的优点,它不会破坏父链(您可以遵循getClassLoader().getParent())。

它还优于tomcat的WebappClassLoader,它不重新发明轮子而不依赖于其他对象。它尽可能地重用URLClassLoader中的代码。

(它还没有尊重系统类加载器,但是当我得到修复时我会更新答案)

它尊重系统类加载器(对于java。*类,背书目录等)。当安全性打开且类加载器无法访问其父级时,它也有效(是的,这种情况很奇怪,但可能)。

public class ChildFirstURLClassLoader extends URLClassLoader {

    private ClassLoader system;

    public ChildFirstURLClassLoader(URL[] classpath, ClassLoader parent) {
        super(classpath, parent);
        system = getSystemClassLoader();
    }

    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            if (system != null) {
                try {
                    // checking system: jvm classes, endorsed, cmd classpath, etc.
                    c = system.loadClass(name);
                }
                catch (ClassNotFoundException ignored) {
                }
            }
            if (c == null) {
                try {
                    // checking local
                    c = findClass(name);
                } catch (ClassNotFoundException e) {
                    // checking parent
                    // This call to loadClass may eventually call findClass again, in case the parent doesn't find anything.
                    c = super.loadClass(name, resolve);
                }
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

    @Override
    public URL getResource(String name) {
        URL url = null;
        if (system != null) {
            url = system.getResource(name); 
        }
        if (url == null) {
            url = findResource(name);
            if (url == null) {
                // This call to getResource may eventually call findResource again, in case the parent doesn't find anything.
                url = super.getResource(name);
            }
        }
        return url;
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        /**
        * Similar to super, but local resources are enumerated before parent resources
        */
        Enumeration<URL> systemUrls = null;
        if (system != null) {
            systemUrls = system.getResources(name);
        }
        Enumeration<URL> localUrls = findResources(name);
        Enumeration<URL> parentUrls = null;
        if (getParent() != null) {
            parentUrls = getParent().getResources(name);
        }
        final List<URL> urls = new ArrayList<URL>();
        if (systemUrls != null) {
            while(systemUrls.hasMoreElements()) {
                urls.add(systemUrls.nextElement());
            }
        }
        if (localUrls != null) {
            while (localUrls.hasMoreElements()) {
                urls.add(localUrls.nextElement());
            }
        }
        if (parentUrls != null) {
            while (parentUrls.hasMoreElements()) {
                urls.add(parentUrls.nextElement());
            }
        }
        return new Enumeration<URL>() {
            Iterator<URL> iter = urls.iterator();

            public boolean hasMoreElements() {
                return iter.hasNext(); 
            }
            public URL nextElement() {
                return iter.next();
            }
        };
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        URL url = getResource(name);
        try {
            return url != null ? url.openStream() : null;
        } catch (IOException e) {
        }
        return null;
    }

}

答案 2 :(得分:11)

通过阅读Jetty或Tomcat的源代码,两者都提供了父类最后一个类加载器来实现webapp语义。

https://github.com/apache/tomcat/blob/7.0.93/java/org/apache/catalina/loader/WebappClassLoaderBase.java

也就是说,通过覆盖findClass课程中的ClassLoader方法。但是,当你可以偷走它时,为什么要重新发明轮子?

阅读各种更新,我发现您遇到了XML SPI系统的一些经典问题。

一般问题是:如果你创建一个完全孤立的类加载器,那么很难使用它返回的对象。如果允许共享,则当父包含错误的版本时,您可能会遇到问题。

这是为了解决OSGi被发明的所有这些疯狂,但这是一个难以接受的大药丸。

即使在webapps中,类加载器也会从“本地优先”处理中豁免某些包,前提是容器和webapp必须就它们之间的API达成一致。

答案 3 :(得分:2)

(请参阅下方有关我找到的解决方案的更新)

似乎AntClassLoader支持父级第一个/最后一个,(尚未测试)

http://svn.apache.org/repos/asf/ant/core/trunk/src/main/org/apache/tools/ant/AntClassLoader.java

这是一个片段

/**
 * Creates a classloader for the given project using the classpath given.
 *
 * @param parent The parent classloader to which unsatisfied loading
 *               attempts are delegated. May be <code>null</code>,
 *               in which case the classloader which loaded this
 *               class is used as the parent.
 * @param project The project to which this classloader is to belong.
 *                Must not be <code>null</code>.
 * @param classpath the classpath to use to load the classes.
 *                  May be <code>null</code>, in which case no path
 *                  elements are set up to start with.
 * @param parentFirst If <code>true</code>, indicates that the parent
 *                    classloader should be consulted  before trying to
 *                    load the a class through this loader.
 */
public AntClassLoader(
    ClassLoader parent, Project project, Path classpath, boolean parentFirst) {
    this(project, classpath);
    if (parent != null) {
        setParent(parent);
    }
    setParentFirst(parentFirst);
    addJavaLibraries();
}

<强>更新

发现this,作为最后的手段,我开始猜测谷歌中的类名(这是ChildFirstURLClassLoader制作的) - 但它似乎不正确

更新2:

第一个选项(AntClassLoader)与Ant非常耦合(需要一个项目上下文,并且不容易将URL[]传递给它

第二个选项(来自OSGI project in google code)并不是我所需要的,因为它在系统类加载器之前搜索父类加载器(Ant类加载器正确地执行它)。我看到的问题是,你认为你的父类加载器包含一个不在JDK 1.4但在1.5中添加的功能的jar(它应该没有),这对于父类最后一个类加载器没有任何害处(常规委托模型,例如URLClassLoader)将始终首先加载JDK的类,但是这里的第一个天真实现似乎揭示了父类加载器中的旧的冗余jar,影响了JDK / JRE自己的实现。

我还没有找到一个经过认证,经过充分测试,成熟的Parent Last / Child First正确实现,它没有与特定解决方案(Ant,Catalina / Tomcat)相结合

更新3 - 我找到了它! 我在寻找错误的地方,

我所做的就是添加META-INF/services/javax.xml.transform.TransformerFactory并恢复JDK的com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl而不是旧的Xalan org.apache.xalan.processor.TransformerFactoryImpl

我还没有“接受我自己的答案”的唯一原因是,我不知道META-INF/services方法是否具有与常规类相同的类加载器委托(例如,它是父级优先/子级-last或parent-last / child-first?)

答案 4 :(得分:0)

您可以覆盖findClass()loadClass()来实现子级第一类加载器:


/**
 * Always throws {@link ClassNotFoundException}. Is called if parent class loader
 * did not find class.
 */
@Override
protected final Class findClass(String name)
        throws ClassNotFoundException
{
    throw new ClassNotFoundException();
}

@Override
protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)){
        /*
         * Check if we have already loaded this class.
         */
        Class c = findLoadedClass(name);

        if (c == null){
            try {
                /*
                 * We haven't previously loaded this class, try load it now
                 * from SUPER.findClass()
                 */
                c = super.findClass(name);
            }catch (ClassNotFoundException ignore){
                /*
                 * Child did not find class, try parent.
                 */
                return super.loadClass(name, resolve);
            }
        }

        if (resolve){
            resolveClass(c);
        }

        return c;
    }
}

答案 5 :(得分:0)

URLClassLoader具有此构造函数public URLClassLoader(URL[], ClassLoader),该构造函数使您可以重写URLClassLoader的父类加载器。您可以通过带有重写的父类加载器的URLClassLoader加载类加载器。