110 votes

Comment résoudre l'exception InaccessibleObjectException ("Impossible de rendre {member} accessible : le module {A} n' ouvre pas {package} à {B}") sur Java 9?

Cette exception se produit dans de nombreux scénarios lors de l'exécution d'une application sur Java 9. Certaines bibliothèques et frameworks (Spring, Hibernate, JAXB) y sont particulièrement sujets. Voici un exemple de Javassist :

java.lang.reflect.InaccessibleObjectException: Impossible de rendre accessible protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError : le module java.base n'ouvre pas "java.lang" au module non nommé @1941a8ff
    à java.base/jdk.internal.reflect.Reflection.throwInaccessibleObjectException(Reflection.java:427)
    à java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:201)
    à java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:192)
    à java.base/java.lang.reflect.Method.setAccessible(Method.java:186)
    à javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:102)
    à javassist.util.proxy.FactoryHelper.toClass2(FactoryHelper.java:180)
    à javassist.util.proxy.FactoryHelper.toClass(FactoryHelper.java:163)
    à javassist.util.proxy.ProxyFactory.createClass3(ProxyFactory.java:501)
    à javassist.util.proxy.ProxyFactory.createClass2(ProxyFactory.java:486)
    à javassist.util.proxy.ProxyFactory.createClass1(ProxyFactory.java:422)
    à javassist.util.proxy.ProxyFactory.createClass(ProxyFactory.java:394)

Le message dit :

Impossible de rendre accessible protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError : le module java.base n'ouvre pas "java.lang" au module non nommé @1941a8ff

Que faire pour éviter l'exception et permettre au programme de s'exécuter avec succès ?

155voto

Nicolai Points 17516

L'exception est causée par le système de module de plate-forme Java qui a été introduit dans Java 9, en particulier son implémentation de l'encapsulation forte. Il permet uniquement un accès dans certaines conditions, les plus importantes étant :

  • le type doit être public
  • le package propriétaire doit être exporté

Les mêmes limitations s'appliquent à la réflexion, que le code causant l'exception a tenté d'utiliser. Plus précisément, l'exception est causée par un appel à setAccessible. Cela peut être vu dans la trace de pile ci-dessus, où les lignes correspondantes dans javassist.util.proxy.SecurityActions ressemblent à ceci :

static void setAccessible(final AccessibleObject ao,
                          final boolean accessible) {
    if (System.getSecurityManager() == null)
        ao.setAccessible(accessible); // <~ Dragons
    else {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                ao.setAccessible(accessible);  // <~ moar Dragons
                return null;
            }
        });
    }
}

Pour s'assurer que le programme s'exécute avec succès, le système de module doit être convaincu d'autoriser l'accès à l'élément sur lequel setAccessible a été appelé. Toutes les informations nécessaires à cela sont contenues dans le message d'exception, mais il existe un certain nombre de mécanismes pour y parvenir. Lequel est le meilleur dépend du scénario exact qui l'a causé.

Impossible de rendre {membre} accessible : le module {A} ne 'ouvre pas {package}' à {B}

De loin, les scénarios les plus courants sont les deux suivants :

  1. Une bibliothèque ou un framework utilise la réflexion pour appeler un module JDK. Dans ce scénario :

    • {A} est un module Java (préfixé par java. ou jdk.)
    • {membre} et {package} font partie de l'API Java
    • {B} est un module de bibliothèque, de framework ou d'application ; souvent unnamed module @...
  2. Une bibliothèque/framework basé(e) sur la réflexion comme Spring, Hibernate, JAXB,... fait de la réflexion sur le code d'application pour accéder aux beans, entités,... Dans ce scénario :

    • {A} est un module d'application
    • {membre} et {package} font partie du code d'application
    • {B} est soit un module de framework soit unnamed module @...

Remarquez que certaines bibliothèques (comme JAXB, par exemple) peuvent échouer sur les deux points, donc examinez attentivement dans quel scénario vous vous trouvez ! Celui de la question est le cas 1.

1. Appel Réflexif dans le JDK

Les modules JDK sont immuables pour les développeurs d'applications, donc nous ne pouvons pas changer leurs propriétés. Cela laisse une seule solution possible : des indicateurs en ligne de commande. Avec eux, il est possible d'ouvrir des packages spécifiques à la réflexion.

Ainsi, dans un cas comme ci-dessus (abrégé)...

Impossible de rendre java.lang.ClassLoader.defineClass accessible : le module java.base ne "ouvre pas java.lang" à unnamed module @1941a8ff

... la correction correcte est de lancer le JVM comme suit :

# --add-opens a la syntaxe suivante : {A}/{package}={B}
java --add-opens java.base/java.lang=ALL-UNNAMED

Si le code réfléchi se trouve dans un module nommé, ALL-UNNAMED peut être remplacé par son nom.

Notez qu'il peut parfois être difficile de trouver un moyen d'appliquer cet indicateur au JVM qui exécutera effectivement le code réfléchi. Cela peut être particulièrement difficile si le code en question fait partie du processus de construction du projet et est exécuté dans un JVM que l'outil de construction a démarré.

S'il y a trop d'indicateurs à ajouter, vous pouvez envisager d'utiliser le basculement d'accès à l'encapsulation --permit-illegal-access à la place. Il permettra à tout le code sur le chemin de classe de réfléchir globalement à l'ensemble des modules nommés. Notez que cet indicateur fonctionnera uniquement avec Java 9!

2. Réflexion sur le Code d'Application

Dans ce scénario, il est probable que vous puissiez modifier le module que la réflexion utilise pour s'infiltrer. (Sinon, vous êtes effectivement dans le cas 1.) Cela signifie que les indicateurs en ligne de commande ne sont pas nécessaires et que instead le descripteur du module {A} peut être utilisé pour ouvrir ses éléments internes. Il y a plusieurs choix :

  • exporter le package avec exports {package}, ce qui le rend disponible en compilation et en exécution pour tout le code
  • exporter le package au module accédant avec exports {package} to {B}, ce qui le rend disponible en compilation et en exécution mais uniquement pour {B}
  • ouvrir le package avec opens {package}, ce qui le rend disponible en exécution (avec ou sans réflexion) pour tout le code
  • ouvrir le package au module accédant avec opens {package} to {B}, ce qui le rend disponible en exécution (avec ou sans réflexion) mais uniquement pour {B}
  • ouvrir l'ensemble du module avec open module {A} { ... }, ce qui rend tous ses packages disponibles en exécution (avec ou sans réflexion) pour tout le code

Voir cet article pour une discussion plus détaillée et une comparaison de ces approches.

0 votes

Est-ce que Lombok est dans le cas 1 et difficile à trouver un moyen d'appliquer ce drapeau à la JVM qui exécutera effectivement le code de réflexion car il fait partie du processus de construction du projet?

0 votes

Oui, Lombok est le cas 1. La question de savoir à quel point il est difficile d'appliquer les drapeaux se trouve ici.

1 votes

Merci pour l'explication. Pour mon cas très similaire cela échoue toujours même avec cette option --add-opens. Étrange.

8voto

Alan Bateman Points 3303

L'utilisation de --add-opens devrait être considérée comme une solution de contournement. La bonne chose à faire est que Spring, Hibernate et autres bibliothèques effectuant un accès illégal corrigent leurs problèmes.

1 votes

Il serait utile de savoir comment vous suggérez de "réparer leurs problèmes". Est-ce par le biais de méthodes ou de gestionnaires de variables ? À mon avis, accéder à l'état en lisant/écrivant des champs privés n'est pas intrinsèquement mauvais, par exemple, c'est quelque chose explicitement prévu par la spécification JPA.

5 votes

Pour l'exemple spécifique, il semble que Hibernate ou Spring utilisent Javassist pour pirater une méthode defineClass non publique. La méthode Lookup.defineClass a spécifiquement été ajoutée pour aider les bibliothèques à injecter des classes afin que ce soit la voie à suivre pour ce cas d'utilisation. Dans le cas où JPA et d'autres bibliothèques ont besoin d'accéder à des membres privés de leurs consommateurs, ils devront documenter que le consommateur ouvre le package à la bibliothèque (pour les frameworks basés sur des annotations (comme JPA), cela peut éventuellement être vérifié lors de la construction).

6voto

David T Points 16

Ceci est un problème très difficile à résoudre; et comme l'ont souligné d'autres, l'option --add-opens n'est qu'un contournement. L'urgence de résoudre les problèmes sous-jacents ne fera qu'augmenter une fois que Java 9 sera disponible au public.

Je me suis retrouvé sur cette page après avoir reçu cette erreur exacte de Javassist lors du test de mon application basée sur Hibernate sur Java 9. Et comme je vise à prendre en charge Java 7, 8 et 9 sur plusieurs plateformes, j'ai eu du mal à trouver la meilleure solution. (Notez que les JVM Java 7 et 8 s'arrêteront immédiatement lorsqu'ils verront un argument "--add-opens" non reconnu sur la ligne de commande; donc cela ne peut pas être résolu avec des modifications statiques aux fichiers de commandes, scripts ou raccourcis.)

Il serait agréable de recevoir des conseils officiels des auteurs des bibliothèques principales (telles que Spring et Hibernate), mais à 100 jours du lancement actuellement prévu de Java 9, ces conseils semblent encore difficiles à trouver.

Après de nombreuses expérimentations et tests, j'ai été soulagé de trouver une solution pour Hibernate:

  1. Utilisez Hibernate 5.0.0 ou ultérieur (les versions antérieures ne fonctionneront pas), et
  2. Demandez une amélioration du bytecode au moment de la construction (en utilisant les plugins Gradle, Maven, ou Ant).

Cela évite le besoin pour Hibernate d'effectuer des modifications de classe basées sur Javassist à l'exécution, éliminant la trace de pile affichée dans le message d'origine.

CEPENDANT, vous devez tester soigneusement votre application par la suite. Les modifications de bytecode appliquées par Hibernate au moment de la construction semblent différer de celles appliquées à l'exécution, entraînant un comportement d'application légèrement différent. Les tests unitaires dans mon application qui ont réussi pendant des années ont soudainement échoué lorsque j'ai activé l'amélioration du bytecode au moment de la construction. (J'ai dû résoudre de nouvelles LazyInitializationExceptions et d'autres problèmes.) De plus, le comportement semble varier d'une version de Hibernate à une autre. Procédez avec prudence.

6voto

Ajoutez --illegal-access=warn et --add-opens java.base/java.lang=ALL-UNNAMED à votre eclipse.ini

4voto

Dhaval Patel Points 33

J'ai rencontré le même problème en 2021 en utilisant openJDK 1.8 et STS 4.

Fenêtre => Préférences => Java => JRE installés.

J'ai ajouté un nouveau JRE (mentionné ci-dessous) en utilisant l'option d'ajout, j'ai parcouru le dossier openJDK, sélectionné OK. Rendu le nouveau JDK par défaut. Cliqué sur appliquer et fermer.

/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre

Ça a fonctionné à merveille :)

0 votes

Cela a fonctionné. Merci.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X