web-dev-qa-db-fra.com

Java 9, problème de compatibilité avec ClassLoader.getSystemClassLoader

Le code suivant ajoute un fichier jar au chemin de génération, il fonctionne très bien avec Java 8. Cependant, il lève une exception avec Java 9, l'exception est liée à le cast vers URLClassLoader. Toutes les idées comment cela peut être résolu? une solution optimale le modifiera pour fonctionner avec les deux Java 8 & 9.

private static int AddtoBuildPath(File f) {
    try {
        URI u = f.toURI();
        URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Class<URLClassLoader> urlClass = URLClassLoader.class;
        Method method = urlClass.getDeclaredMethod("addURL", URL.class);
        method.setAccessible(true);
        method.invoke(urlClassLoader, u.toURL());
    } catch (NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException | MalformedURLException | IllegalAccessException ex) {
        return 1;
    }

    return 0;
}
11
Mostafa abdo

Vous avez rencontré le fait que le chargeur de classe système n'est plus un URLClassLoader . Comme indiqué par le type de retour de ClassLoader::getSystemClassLoader, Il s'agissait d'un détail d'implémentation, même si une quantité non négligeable de code était utilisée.

À en juger par les commentaires, vous cherchez un moyen de charger dynamiquement des classes au moment de l'exécution. Comme souligne Alan Bateman , cela ne peut pas être fait dans Java 9 en ajoutant au chemin de classe.

Vous devriez plutôt envisager de créer un nouveau chargeur de classe pour cela. Cela a l'avantage supplémentaire de vous permettre de vous débarrasser des nouvelles classes car elles ne sont pas chargées dans le chargeur de classe d'application. Si vous compilez avec Java 9, vous devriez lire sur couches - ils vous donnent une abstraction propre pour charger un graphique de module entièrement nouveau.

6
Nicolai

J'ai trébuché sur cette question il y a quelque temps. Comme beaucoup, j'avais utilisé une méthode similaire à celle de la question

private static int AddtoBuildPath(File f)

pour ajouter dynamiquement des chemins au chemin de classe lors de l'exécution. Le code dans la question est probablement un mauvais style à plusieurs égards: 1) en supposant que ClassLoader.getSystemClassLoader() renvoie un URLClassLoader est un détail d'implémentation non documenté et 2) en utilisant la réflexion pour faire addURL le public en est peut-être un autre.

façon plus propre d'ajouter dynamiquement des chemins de classe

Dans le cas où vous devez utiliser les URL de chemin de classe supplémentaires pour le chargement de classe via "Class.forName", Une solution propre, élégante et compatible (Java 8 à 10) est la suivante:

1) Écrivez votre propre chargeur de classe en étendant le chargeur de classe URL, en ayant une méthode publique addURL

public class MyClassloader extends URLClassLoader {

    public MyClassloader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public void addURL(URL url) {
        super.addURL(url);
    }
}

2) Déclarez un objet (à l'échelle de singleton/application) de votre chargeur de classe

private final MyClassloader classLoader;

et l'instancier via

classLoader = new MyClassloader(new URL[0], this.getClass().getClassLoader());

Remarque: Le chargeur de classe système est le parent. Les classes chargées par classLoader connaissent celles qui peuvent être chargées par this.getClass().getClassLoader() mais pas l'inverse.

3) Ajoutez des chemins de classe supplémentaires chaque fois que nécessaire (dynamiquement):

File file = new File(path);
if(file.exists()) {
    URL url = file.toURI().toURL();
    classLoader.addURL(url);
}

4) Instanciez des objets ou votre application via votre chargeur de classe singleton via

cls = Class.forName(name, true, classLoader);

Remarque: Étant donné que les chargeurs de classe essaient une délégation vers le chargeur de classe parent avant de charger une classe (et le parent vers son parent), vous devez vous assurer que la classe à charger n'est pas visible par le chargeur de classe parent pour vous assurer qu'elle est chargé via le chargeur de classe donné. Pour rendre cela plus clair: si vous avez ClassPathB sur votre chemin d'accès aux classes système et ajoutez plus tard ClassPathB et un peu ClassPathA à votre classLoader personnalisé, alors les classes sous ClassPathB sera chargé via le chargeur de classe système et les classes sous ClassPathA ne leur sont pas connues. Cependant, si vous supprimez ClassPathB de votre chemin d'accès aux classes système, ces classes seront chargées via votre classLoader personnalisé, puis les classes sous ClassPathA sont connues de celles sous ClassPathB.

5) Vous pouvez envisager de passer votre chargeur de classe à un thread via

setContextClassLoader(classLoader)

dans le cas où ce thread utilise getContextClassLoader.

3
Christian Fries

Shadov a pointé un fil sur communauté Oracle . Il y a la bonne réponse:

Class.forName("nameofclass", true, new URLClassLoader(urlarrayofextrajarsordirs));

Les mises en garde qui y sont mentionnées sont également importantes:

Avertissements:

Java.util.ServiceLoader utilise le contexte ClassLoader du thread Thread.currentThread (). SetContextClassLoader (specialloader);

Java.sql.DriverManager honore la classe appelante 'ClassLoader, -pas- le ClassLoader du thread. Créez directement le pilote à l'aide de Class.forName ("nom_direction", true, new URLClassLoader (urlarrayofextrajarsordirs) .newInstance ();

javax.activation utilise le contexte ClassLoader du thread (important pour javax.mail).

2
PaL

Si vous cherchez simplement à lire le chemin d'accès aux classes actuel, par exemple parce que vous souhaitez faire tourner une autre machine virtuelle Java avec le même chemin d'accès aux classes que l'actuel, vous pouvez effectuer les opérations suivantes:

object ClassloaderHelper {
  def getURLs(classloader: ClassLoader) = {
    // jdk9+ need to use reflection
    val clazz = classloader.getClass

    val field = clazz.getDeclaredField("ucp")
    field.setAccessible(true)
    val value = field.get(classloader)

    value.asInstanceOf[URLClassPath].getURLs
  }
}

val classpath =
  (
    // jdk8
    // ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs ++
    // getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs

    // jdk9+
    ClassloaderHelper.getURLs(ClassLoader.getSystemClassLoader) ++
    ClassloaderHelper.getURLs(getClass.getClassLoader)
  )

Par défaut, les champs finaux de la classe $ AppClassLoader ne peuvent pas être accédés via la réflexion, un indicateur supplémentaire doit être transmis à la JVM:

--add-opens Java.base/jdk.internal.loader=ALL-UNNAMED
1
Edi

j'ai trouvé cela et j'ai travaillé pour moi.

String pathSeparator = Syste .getProperty ("path.separator"); String [] classPathEntries = System.getProperty ("Java.class.path") .split (pathSeparator);

à partir du site Web https://blog.codefx.org/Java/java-11-migration-guide/#Casting-To-URL-Class-Loader

0
Eduardo Iglesias

En ce qui concerne la solution d'Edi, cela a fonctionné pour moi:

public final class IndependentClassLoader extends URLClassLoader {

    private static final ClassLoader INSTANCE = new IndependentClassLoader();

    /**
     * @return instance
     */
    public static ClassLoader getInstance() {

        return INSTANCE;
    }

    private IndependentClassLoader() {

        super(getAppClassLoaderUrls(), null);
    }

    private static URL[] getAppClassLoaderUrls() {

        return getURLs(IndependentClassLoader.class.getClassLoader());
    }

    private static URL[] getURLs(ClassLoader classLoader) {

        Class<?> clazz = classLoader.getClass();

        try {
            Field field = null;
            field = clazz.getDeclaredField("ucp");
            field.setAccessible(true);

            Object urlClassPath = field.get(classLoader);

            Method method = urlClassPath.getClass().getDeclaredMethod("getURLs", new Class[] {});
            method.setAccessible(true);
            URL[] urls = (URL[]) method.invoke(urlClassPath, new Object[] {});

            return urls;

        } catch (Exception e) {
            throw new NestableRuntimeException(e);
        }

    }
}

En cours d'exécution dans Eclipse, vous devez définir VM Arguments pour la configuration de lancement/débogage JUnit. En exécutant avec maven via la ligne de commande, vous avez deux options:

Option 1
Ajoutez les lignes suivantes à pom.xml:

            <plugin>
                <groupId>org.Apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>
                <configuration>
                    <argLine>--add-opens Java.base/jdk.internal.loader=ALL-UNNAMED</argLine>
                </configuration>
            </plugin>

Option 2

courir mvn test -DargLine="-Dsystem.test.property=--add-opens Java.base/jdk.internal.loader=ALL-UNNAMED"

0
Splioo