Comment créer un parent-last / child-first ClassLoader en Java, ou comment modifier une ancienne version de Xerces qui était déjà chargée dans le CL parent?

je voudrais créer un parent-last / child-first class loader, par exemple un class loader qui va chercher des classes dans le child class loder d'abord, et puis seulement puis déléguer à son parent ClassLoader pour rechercher des classes.

Clarification:

je sais maintenant que pour obtenir la séparation complète de ClassLoading j'ai besoin d'utiliser quelque chose comme un URLClassLoader passant null comme il est parent, grâce à cette réponse à ma question précédente

Néanmoins, la question qui me vient à m'aider à résoudre ce problème:

  1. mon code + les bocaux dépendants sont chargés dans un système existant, en utilisant un ClassLoader qui définit ClassLoader de ce système comme parent (URLClassLoader)

  2. ce système utilise certaines bibliothèques d'une version qui n'est pas 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 si fonctionne seul, mais il échoue si Fonctionne à partir de ce ClassLoader

  4. Mais j'ai besoin d'accéder à de nombreuses autres classes à l'intérieur du chargeur de classe parent

  5. donc je veux me permettre de surcharger, le classloader 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 pots, au lieu de l'un des utilisateurs par le chargeur de classe qui a chargé mon code et des pots.

voici le code du système qui charge mon code + bocaux (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 (tiré entièrement de la démo du code de 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();
    }
}

cela fonctionne de manière autonome (en tournant main) mais ne fonctionne pas avec cette erreur lors du chargement par le CL parent:

org.w3c.dom.DOMException: NAMESPACE_ERR: une tentative est faite pour créer ou modifier un objet de façon à ce qui est incorrect à l'égard de les espaces de noms.

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

31
demandé sur Community 2011-03-27 00:40:52

5 réponses

Aujourd'hui est votre jour de chance, comme j'ai dû résoudre ce problème exact. Je te préviens, les entrailles de la classe sont un endroit effrayant. Faire cela me fait penser que les concepteurs de Java jamais imaginé que vous pourriez vouloir avoir un parent-last classloader.

pour utiliser il suffit de fournir une liste d'URLs contenant des classes ou des bocaux à être disponibles dans le child classloader.

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

EDIT : Sergio fait remarquer que si vous appelez .loadClass avec le même nom de classe, vous obtiendrez une LinkageError. Bien que cela soit vrai, l'utilisation normale de ce classloader est de le Définir comme classloader 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 ChildURLClassLoader findClass au sommet.

                Class<?> loaded = super.findLoadedClass(name);
                if( loaded != null )
                    return loaded;
26
répondu karoberts 2013-08-27 22:29:11

le code suivant est celui que j'utilise. Il a l'avantage par rapport à l'autre réponse qu'il ne casse pas la chaîne mère (vous pouvez suivre getClassLoader().getParent() ).

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

(il n'honore pas encore le chargeur de classe système, mais quand j'obtiens que corrigé je vais mettre à jour le réponse)

il honore le chargeur de classe système (pour java.* classes, dir approuvé, etc.). Cela fonctionne aussi lorsque la sécurité est activée et que le classloader n'a pas accès à son parent (oui, cette situation est étrange, 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;
    }

}
15
répondu Yoni 2011-06-30 05:41:56

en lisant le code source de Jetty ou Tomcat, qui fournissent tous deux des chargeurs de dernière classe pour implémenter la sémantique webapp.

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

, C'est-à-dire en supplantant la méthode findClass dans votre classe ClassLoader . Mais pourquoi réinventer la roue quand on peut voler?

en lisant vos différentes mises à jour, je vois 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é, alors 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 de 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 webapps, les Class loaders exemptent certains paquets du traitement "local-first" en partant de l'hypothèse que le conteneur et la webapp doivent s'entendre sur L'API entre eux.

10
répondu bmargulies 2011-03-27 12:56:06

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

il semble Qu'AntClassLoader ait un support pour parent first / last, (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:

Found ce ainsi, lorsqu'en dernier recours, j'ai commencé à deviner les noms de classe dans google (c'est ce que ChildFirstURLClassLoader produit) - mais il 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[] à it

la 2e Option (d'un projet OSGI en code google ) n'était pas tout à fait ce dont j'avais besoin car il a cherché classloader parent avant le système classloader (Ant class loader le fait correctement d'ailleurs). Le problème comme je le vois, je pense que votre classloader 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é en 1.5, cela n'a aucun mal puisque le dernier class loader parent (modèle de délégation régulière, par exemple URLClassLoader) chargera toujours en premier les classes de JDK, mais ici l'enfant d'abord naïf l'implémentation semble dévoiler l'ancien bocal redondant dans le chargeur de classe parent, et suit la propre implémentation de JDK / JRE.

je n'ai pas encore trouver un certifié, entièrement testé, mature Parent Dernier / l'Enfant d'Abord la mise en œuvre correcte qui n'est pas couplé à une solution spécifique (Ant, Catalina/Tomcat)

mise à Jour 3 - je l'ai trouvé! Je regardais au mauvais endroit,

Je n'ai fait qu'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 est que je ne sais pas si l'approche META-INF/services a la même délégation de classe que les classes normales (par exemple, est-ce parent-premier / enfant-dernier ou parent-dernier / enfant-Premier?)

2
répondu Eran Medan 2011-03-27 07:49:38

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
répondu Jesse 2017-07-26 09:07:02