Как мне создать ClassLoader для родительского последнего / дочернего первого в Java или Как переопределить старую версию Xerces, которая уже была загружена в родительский CL?

Я хотел бы создать загрузчик классов parent-last / child-first, например загрузчик классов, который сначала будет искать классы в загрузчике дочерних классов, а затем делегировать его родительскому ClassLoader для поиска классов.

Разъяснение:

Теперь я знаю, что для полного разделения ClassLoading мне нужно использовать что-то вроде URLClassLoader, передавая null в качестве родителя, благодаря этому ответу на мой предыдущий вопрос

Однако текущий вопрос помогает мне решить эту проблему:

  1. Мой код + зависимые банки загружаются в существующую систему, используя ClassLoader, который устанавливает ClassLoader этой системы в качестве его родителя (URLClassLoader)

  2. Эта Система использует некоторые библиотеки версии, несовместимой с той, которая мне нужна (например, более старая версия Xerces, которая не позволяет мне запускать мой код)

  3. Мой код работает отлично, если работает автономно, но не работает, если запускается из этого ClassLoader

  4. Однако мне нужен доступ ко многим другим классам в родительском ClassLoader

  5. Поэтому я хочу разрешить мне переопределить родительский загрузчик классов "jar" с моим собственным: если в загрузчике дочернего класса обнаружен вызываемый мной класс (например, я предоставил более новую версию Xerces со своими собственными jar-файлами, а не с одним пользователем) ClassLoader, который загрузил мой код и банки.

Вот код системы, который загружает мой код + банки (я не могу изменить этот)

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

Вот "мой" код (взят полностью из демонстрационной программы "Hello World" от Flying Sauser):

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 более старой версии, и хотя я предоставляю правильный jar-файл Xerces в папке / addOns, поскольку его классы уже загружены и используются родительской системой, она не позволяет использовать мой собственный код мой собственный кувшин из-за направления делегации. Надеюсь, это прояснит мой вопрос, и я уверен, что он задавался раньше. (Возможно, я не задаю правильный вопрос)

7 ответов

Решение

Сегодня твой счастливый день, так как мне пришлось решить именно эту проблему. Я предупреждаю вас, хотя, внутренности загрузки класса - страшное место. Это заставляет меня думать, что разработчики Java никогда не предполагали, что вам может понадобиться последний загрузчик классов для родителей.

Чтобы использовать, просто предоставьте список URL-адресов, содержащих классы или jar-файлы, которые будут доступны в дочернем загрузчике классов.

/**
 * 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);
        }
    }
}

РЕДАКТИРОВАТЬ: Серхио и ɹoƃı указали, что если вы позвоните .loadClass с тем же именем класса, вы получите LinkageError. Хотя это и так, нормальный вариант использования этого загрузчика классов - установить его в качестве загрузчика классов потока. Thread.currentThread().setContextClassLoader() или через Class.forName()и это работает как есть.

Однако если .loadClass() был необходим напрямую, этот код можно было добавить в метод findClass ChildURLClassLoader вверху.

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

Следующий код - это то, что я использую. Он имеет преимущество перед другим ответом, что он не разрывает родительскую цепочку (вы можете следовать getClassLoader().getParent()).

Он также имеет преимущество перед WebcatClassLoader от Tomcat, поскольку не изобретает велосипед и не зависит от других объектов. Он максимально использует код из URLClassLoader.

(он еще не учитывает загрузчик системных классов, но когда я это исправлю, я обновлю ответ)

Он учитывает загрузчик системных классов (для классов java. *, Утвержденных dir и т. Д.). Это также работает, когда защита включена, и загрузчик классов не имеет доступа к своему родителю (да, эта ситуация странная, но возможная).

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;
    }

}

Читая исходный код Jetty или Tomcat, оба из которых предоставляют загрузчики классов для последнего родительского класса для реализации семантики веб-приложения.

http://svn.apache.org/repos/asf/tomcat/tc7.0.x/tags/TOMCAT_7_0_0/java/org/apache/catalina/loader/WebappClassLoader.java

То есть, переопределив findClass метод в вашем ClassLoader учебный класс. Но зачем изобретать велосипед, если ты можешь его украсть?

Читая ваши различные обновления, я вижу, что вы столкнулись с некоторыми классическими проблемами с системой SPI XML.

Общая проблема заключается в следующем: если вы создаете полностью изолированный загрузчик классов, то трудно использовать возвращаемые им объекты. Если вы разрешите общий доступ, у вас могут возникнуть проблемы, когда родитель содержит неправильные версии вещей.

OSGi была изобретена, чтобы справиться со всем этим безумием, но это большая пилюля для проглатывания.

Даже в веб-приложениях загрузчики классов освобождают некоторые пакеты от обработки local-first при условии, что контейнер и веб-приложение должны согласовать API-интерфейс между ними.

(см. внизу обновление для решения, которое я нашел)

Похоже, что 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();
}

Обновить:

Нашел это также, когда в крайнем случае я начал угадывать имена классов в Google (это то, что произвел ChildFirstURLClassLoader) - но это, кажется, неверно

Обновление 2:

1-й вариант (AntClassLoader) очень связан с Ant (требуется контекст проекта и непросто передать URL[] к этому

Второй вариант (из проекта OSGI в google-коде) был не совсем тем, что мне было нужно, так как он искал родительский загрузчик классов перед системным загрузчиком классов (кстати, загрузчик классов Ant делает это правильно). Проблема, на мой взгляд, заключается в том, что ваш родительский загрузчик классов включает в себя jar (которого он не должен иметь) функциональности, которой не было в JDK 1.4, но был добавлен в 1.5, это не повредит как родительский загрузчик последнего класса (модель обычного делегирования, например 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's org.apache.xalan.processor.TransformerFactoryImpl

Единственная причина, по которой я пока не "принимаю свой ответ", заключается в том, что я не знаю, META-INF/services Подход имеет такое же делегирование загрузчика классов, как и обычные классы (например, это родитель-первый / ребенок-последний или родитель-последний / ребенок-первый?)

У URLClassLoader был этот конструктор public URLClassLoader(URL[], ClassLoader)который позволяет вам переопределить родительский загрузчик классов URLClassLoader. Вы можете просто загрузить свой загрузчик классов через URLClassLoader с переопределенным родительским загрузчиком классов.

Вы можете переопределить 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;
    }
}

Немного опоздал на вечеринку, но ни один из ответов не помог моей проблеме, поскольку я получал ошибки связи. Вот что сработало для меня:

      import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;


public class ParentLastURLClassLoader extends URLClassLoader {

    /**
     * Create a child-first/parent-last URLClassLoader
     * @param urls
     *  the URLS to be included in the classpath of the new classloader
     * @param parent
     *  parent classloader
     */
    public ParentLastURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    /**
     * {@inheritDoc}
     * 
     * For the parent-last classloader, first, if the class is already loaded, just recovers
     * it, and if it has to laod it, it tries first to load the class from the JARs,
     * and if it is not possible, it falls back to the parent classloader
     */
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // First, check if the class is already loaded
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }

        try {
            // Try to load the class from the URLs of this classloader
            Class<?> localClass = findClass(name);
            if (resolve) {
                resolveClass(localClass);
            }
            return localClass;
        } catch (ClassNotFoundException e) {
            // Class not found in this classloader, delegate to parent classloader
            return super.loadClass(name, resolve);
        }
    }
}

ОБНОВЛЕНИЕ Код от @reda-alaoui работает отлично и лучше моего:https://gist.github.com/reda-alaoui/a3030964293268eca48ddc66d8a07d74

Другие вопросы по тегам