102 votes

Comment résoudre l'exception InaccessibleObjectException ("Unable to make {member} accessible : module {A} does not 'opens {package}' to {B}") en Java 9 ?

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

java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @1941a8ff
    at java.base/jdk.internal.reflect.Reflection.throwInaccessibleObjectException(Reflection.java:427)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:201)
    at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:192)
    at java.base/java.lang.reflect.Method.setAccessible(Method.java:186)
    at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:102)
    at javassist.util.proxy.FactoryHelper.toClass2(FactoryHelper.java:180)
    at javassist.util.proxy.FactoryHelper.toClass(FactoryHelper.java:163)
    at javassist.util.proxy.ProxyFactory.createClass3(ProxyFactory.java:501)
    at javassist.util.proxy.ProxyFactory.createClass2(ProxyFactory.java:486)
    at javassist.util.proxy.ProxyFactory.createClass1(ProxyFactory.java:422)
    at javassist.util.proxy.ProxyFactory.createClass(ProxyFactory.java:394)

Le message dit :

Impossible de rendre accessible la classe finale protégée java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError : le module java.base ne "ouvre pas java.lang" au module non nommé @1941a8ff

Que peut-on faire pour éviter l'exception et faire en sorte que le programme s'exécute correctement ?

141voto

Nicolai Points 17516

L'exception est causée par le Système de modules de la plate-forme Java qui a été introduit dans Java 9, en particulier sa mise en œuvre de l'encapsulation forte. Elle permet seulement accès sous certaines conditions, les plus importantes sont :

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

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

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, il faut convaincre le système de modules d'autoriser l'accès à l'élément sur lequel l setAccessible a été appelé. Toutes les informations requises pour cela sont contenues dans le message d'exception mais il y a un certain nombre de mécanismes pour y parvenir. La meilleure dépend du scénario exact qui en est à l'origine.

Impossible de rendre {member} accessible : le module {A} n'ouvre pas {package} à {B}.

Les deux scénarios suivants sont de loin les plus marquants :

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

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

    • {A} est un module d'application
    • {member} et {package} font partie du code de l'application
    • {B} est soit un module de cadre, soit unnamed module @...

Notez que certaines bibliothèques (JAXB, par exemple) peuvent échouer sur les deux comptes, alors examinez bien le scénario dans lequel vous vous trouvez ! Celui de la question est le cas 1.

1. Appel réfléchi dans le JDK

Les modules du JDK étant immuables pour les développeurs d'applications, nous ne pouvons pas modifier leurs propriétés. Il ne reste donc qu'une seule solution possible : drapeaux de ligne de commande . Grâce à eux, il est possible d'ouvrir des paquets spécifiques à la réflexion.

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

Impossible de rendre accessible java.lang.ClassLoader.defineClass : le module java.base ne "ouvre pas java.lang" au module non nommé @1941a8ff

... la solution correcte consiste à lancer la JVM comme suit :

# --add-opens has the following syntax: {A}/{package}={B}
java --add-opens java.base/java.lang=ALL-UNNAMED

Si le code de réflexion 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 à la 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 une JVM que l'outil de construction a généré.

S'il y a trop de drapeaux à ajouter, vous pouvez envisager d'utiliser la fonction interrupteur d'encapsulation --permit-illegal-access à la place. Cela permettra à tout le code sur le chemin de la classe de refléter les modules nommés globalement. Notez que ce drapeau ne fonctionnera qu'en Java 9 !

2. Réflexion sur le code d'application

Dans ce scénario, il est probable que vous puissiez modifier le module dans lequel la réflexion est utilisée pour s'introduire. (Si ce n'est pas le cas, vous êtes effectivement dans le cas 1.) Cela signifie que les drapeaux de ligne de commande ne sont pas nécessaires et qu'à la place le module {A} Il est possible d'utiliser le descripteur de l'application pour ouvrir ses données internes. Il existe une variété de choix :

  • exporter le paquet avec exports {package} ce qui le rend disponible à la compilation et à l'exécution pour tout le code.
  • exporter le paquet vers le module d'accès avec exports {package} to {B} ce qui le rend disponible au moment de la compilation et de l'exécution, mais seulement pour les personnes suivantes {B}
  • ouvrir le paquet avec opens {package} ce qui le rend disponible au moment de l'exécution (avec ou sans réflexion) pour tout le code.
  • ouvrir le paquet au module d'accès avec opens {package} to {B} ce qui le rend disponible au moment de l'exécution (avec ou sans réflexion) mais seulement pour {B}
  • ouvrir le module entier avec open module {A} { ... } qui rend tous ses paquets disponibles au moment de l'exécution (avec ou sans réflexion) pour tout le code.

Voir ce poste pour une discussion plus détaillée et une comparaison de ces approches.

0 votes

Est Lombok dans le cas 1 et difficile de trouver un moyen d'appliquer ce drapeau à la JVM qui va effectivement exécuter le code réfléchi car il fait partie du processus de construction du projet ?

0 votes

Oui, Lombok est le cas 1. La question est de savoir s'il est difficile d'appliquer les drapeaux. ici .

1 votes

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

5voto

Alan Bateman Points 3303

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

1 votes

Il serait utile de savoir comment vous suggérez de "régler leurs problèmes". Est-ce au moyen de méthodes ou de var handles ? À 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 d'explicitement prévu par la spécification JPA.

4 votes

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

5voto

David T Points 16

Il s'agit d'un problème très difficile à résoudre et, comme l'ont fait remarquer d'autres personnes, l'option --add-opens n'est qu'une solution de rechange. L'urgence de résoudre les problèmes sous-jacents ne fera que croître lorsque Java 9 sera disponible publiquement.

Je me suis retrouvé sur cette page après avoir reçu cette erreur Javassist exacte alors que je testais mon application basée sur Hibernate sur Java 9. Et comme mon objectif est de prendre en charge Java 7, 8 et 9 sur plusieurs plates-formes, je me suis battu pour trouver la meilleure solution. (Notez que les JVM de Java 7 et 8 s'interrompent immédiatement lorsqu'elles voient un argument "--add-opens" non reconnu sur la ligne de commande ; ce problème ne peut donc pas être résolu par des modifications statiques des fichiers batch, des scripts ou des raccourcis).

Il serait agréable de recevoir des conseils officiels de la part des auteurs des bibliothèques les plus courantes (telles que Spring et Hibernate), mais à 100 jours de la sortie prévue 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 plus (les versions antérieures ne fonctionnent pas), et
  2. Demande amélioration du bytecode au moment de la construction (en utilisant les plugins Gradle, Maven ou Ant).

Ainsi, Hibernate n'a pas besoin d'effectuer des modifications de classe basées sur Javassist au moment de l'exécution, ce qui élimine le suivi de pile présenté dans le message original.

CEPENDANT, vous devez ensuite tester votre application de manière approfondie. Les modifications du bytecode appliquées par Hibernate au moment de la construction semblent différer de celles appliquées au moment de l'exécution, ce qui entraîne un comportement légèrement différent de l'application. Les tests unitaires de 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û rechercher de nouvelles LazyInitializationExceptions et d'autres problèmes.) Et le comportement semble varier d'une version d'Hibernate à l'autre. Procédez avec prudence.

5voto

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

3voto

Karol Golec Points 11

J'ai eu des avertissements avec Hibernate 5.

Illegal reflective access by javassist.util.proxy.SecurityActions

J'ai ajouté la dernière bibliothèque javassist aux dépendances gradle :

compile group: 'org.javassist', name: 'javassist', version: '3.22.0-GA'

Cela a résolu mon problème.

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