web-dev-qa-db-fra.com

Comment puis-je créer un ClassLoader parent-dernier / premier enfant en Java, ou comment remplacer une ancienne version de Xerces qui était déjà chargée dans le CL parent?

Je voudrais créer un chargeur de classe parent-dernier/enfant-premier, par exemple un chargeur de classe qui recherchera d'abord les classes dans le loder de classe enfant, puis déléguera à son ClassLoader parent pour rechercher des classes.

Clarification:

Je sais maintenant que pour obtenir une séparation complète de ClassLoading, je dois utiliser quelque chose comme un URLClassLoader passant null comme parent, grâce à cette réponse à ma question précédente

Cependant la question actuelle vient m'aider à résoudre ce problème:

  1. Mon code + les fichiers dépendants sont chargés dans un système existant, à l'aide d'un ClassLoader qui définit le ClassLoader de ce système comme parent (URLClassLoader)

  2. Ce système utilise certaines bibliothèques d'une version non compatible avec celle dont j'ai besoin (par exemple, une ancienne version de Xerces, qui ne me permet pas d'exécuter mon code)

  3. Mon code fonctionne parfaitement bien s'il s'exécute de façon autonome, mais il échoue s'il s'exécute à partir de ce ClassLoader

  4. Cependant, j'ai besoin d'accéder à de nombreuses autres classes dans le ClassLoader parent

  5. Par conséquent, je veux me permettre de remplacer, le chargeur de classe parent "jars" avec le mien: si une classe que j'appelle se trouve dans le chargeur de classe enfant (par exemple, j'ai fourni une version plus récente de Xerces avec mes propres jars, au lieu de celui des utilisateurs par le ClassLoader qui a chargé mon code et mes pots.

Voici le code du système qui charge mon code + Jars (je ne peux pas changer celui-ci)

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

Voici "mon" code (entièrement extrait de la démonstration du code "Hello World" de 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();
    }
}

Cela fonctionne de manière autonome (en cours d'exécution principal) mais échoue avec cette erreur lors du chargement via le CL parent:

org.w3c.dom.DOMException: NAMESPACE_ERR: Une tentative est faite pour créer ou modifier un objet d'une manière incorrecte en ce qui concerne les espaces de noms.

probablement parce que le système parent utilise Xerces d'une ancienne version, et même si je fournis le bon pot Xerces dans le dossier/addOns, puisque ses classes ont déjà été chargées et utilisées par le système parent, il ne permet pas à mon propre code d'utiliser mon propre pot en raison de la direction de la délégation. J'espère que cela clarifiera ma question et je suis sûr qu'elle a déjà été posée. (Peut-être que je ne pose pas la bonne question)

35
Eran Medan

Aujourd'hui est votre jour de chance, car j'ai dû résoudre ce problème exact. Je vous préviens cependant, les entrailles du chargement des classes sont un endroit effrayant. Faire cela me fait penser que les concepteurs de Java n'ont jamais imaginé que vous souhaitiez avoir un chargeur de classe parent-dernier.

Pour l'utiliser, il suffit de fournir une liste d'URL contenant des classes ou des fichiers JAR qui seront disponibles dans le chargeur de classe enfant.

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

MODIFIER : Sergio et ɹoƃı ont souligné que si vous appelez .loadClass avec le même nom de classe, vous obtiendrez une LinkageError. Bien que cela soit vrai, le cas d'utilisation normal de ce chargeur de classe est de le définir comme chargeur de classe du thread Thread.currentThread().setContextClassLoader() ou via Class.forName(), et cela fonctionne tel quel.

Cependant, si .loadClass() était nécessaire directement, ce code pourrait être ajouté dans la méthode findURPass de ChildURLClassLoader en haut.

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

Le code suivant est ce que j'utilise. Il a l'avantage par rapport à l'autre réponse qu'il ne rompt pas la chaîne parente (vous pouvez suivre getClassLoader().getParent()).

Il a également un avantage sur WebappClassLoader de Tomcat en ne réinventant pas la roue et en ne dépendant pas d'autres objets. Il réutilise autant que possible le code d'URLClassLoader.

(il n'honore pas encore le chargeur de classe système, mais quand je le corrigerai, je mettrai à jour la réponse)

Il honore le chargeur de classe système (pour les classes Java. *, Dir approuvé, etc.). Cela fonctionne également lorsque la sécurité est activée et que le chargeur de classe n'a pas accès à son parent (oui, cette situation est bizarre, mais possible).

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

}
18
Yoni

En lisant le code source de Jetty ou de Tomcat, qui fournissent tous deux des chargeurs de classe parent-dernier pour implémenter la sémantique webapp.

https://github.com/Apache/Tomcat/blob/7.0.93/Java/org/Apache/catalina/loader/WebappClassLoaderBase.Java

Autrement dit, en redéfinissant la méthode findClass dans votre classe ClassLoader. Mais pourquoi réinventer la roue quand on peut la voler?

En lisant vos différentes mises à jour, je constate que vous avez rencontré des problèmes classiques avec le système XML SPI.

Le problème général est le suivant: si vous créez un chargeur de classe complètement isolé, il est difficile d'utiliser les objets qu'il renvoie. Si vous autorisez le partage, vous pouvez avoir des problèmes lorsque le parent contient les mauvaises versions des choses.

C'est pour faire face à toute cette folie qu'OSGi a été inventé, mais c'est une grosse pilule à avaler.

Même dans les applications Web, les chargeurs de classe exemptent certains packages du traitement "local en premier" en supposant que le conteneur et l'application Web doivent se mettre d'accord sur l'API entre eux.

13
bmargulies

(voir en bas pour une mise à jour sur une solution que j'ai trouvée)

Il semble que AntClassLoader ait un support pour le parent premier/dernier, (ne l'a pas encore testé)

http://svn.Apache.org/repos/asf/ant/core/trunk/src/main/org/Apache/tools/ant/AntClassLoader.Java

Voici un extrait

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

Mise à jour:

Trouvé ceci également, quand en dernier recours j'ai commencé à deviner les noms de classe dans google (c'est ce que ChildFirstURLClassLoader a produit) - mais cela semble être incorrect

Mise à jour 2:

La 1ère option (AntClassLoader) est très couplée à Ant (nécessite un contexte de projet et pas facile de passer un URL[] à elle

La 2e option (d'un projet OSGI dans le code Google ) n'était pas tout à fait ce dont j'avais besoin car elle recherchait le chargeur de classe parent avant le chargeur de classe système (le chargeur de classe Ant le fait correctement d'ailleurs). Le problème tel que je le vois, pense que votre chargeur de classe parent inclut un pot (qu'il ne devrait pas avoir) d'une fonctionnalité qui n'était pas sur JDK 1.4 mais a été ajoutée en 1.5, cela n'a aucun mal en tant que chargeur de dernière classe parent ( modèle de délégation régulière, par exemple URLClassLoader) chargera toujours d'abord les classes du JDK, mais ici la première implémentation naïve enfant semble dévoiler l'ancien pot redondant dans le chargeur de classe parent, en suivant la propre implémentation du JDK/JRE.

Je n'ai pas encore trouvé d'implémentation correcte de Parent Last/Child First, certifiée, entièrement testée et mature, qui n'est pas couplée à une solution spécifique (Ant, Catalina/Tomcat)

Mise à jour 3 - Je l'ai trouvé! Je cherchais au mauvais endroit,

Tout ce que j'ai fait, c'est ajouter META-INF/services/javax.xml.transform.TransformerFactory et restauré le JDK com.Sun.org.Apache.xalan.internal.xsltc.trax.TransformerFactoryImpl au lieu de l'ancien Xalan org.Apache.xalan.processor.TransformerFactoryImpl

La seule raison pour laquelle je n'accepte pas encore ma propre réponse, c'est que je ne sais pas si le META-INF/services l'approche a la même délégation de chargeur de classe que les classes normales (par exemple, est-ce parent-premier/enfant-dernier ou parent-dernier/enfant-premier?)

2
Eran Medan

Vous pouvez remplacer findClass() et loadClass() pour implémenter un chargeur enfant de première classe:


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