Historique. Avant Java 9, la réflexion pouvait contourner arbitrairement les modificateurs d'accès via setAccessible(true), brisant l'encapsulation à volonté. L'introduction du Java Platform Module System (JPMS) a établi une forte encapsulation par défaut, où les modules doivent explicitement accorder la permission d'accès réfléchi profond à leurs packages internes.
Problème. Lorsque du code dans un module tente d'utiliser des MethodHandles ou une réflexion de base pour accéder à un champ non public dans le package d'un autre module, la JVM effectue une vérification d'accessibilité rigoureuse. Cette vérification s'assure que le package cible a été explicitement ouvert au module appelant. Sans cette permission, la JVM lance une InaccessibleObjectException (ou IllegalAccessException pour la réflexion héritée), peu importe si un SecurityManager est installé ou si le champ est accessible via VarHandle.
Solution. Le module doit déclarer opens package.name [to specific.module]; dans son module-info.java, ou l'application doit être lancée avec le drapeau --add-opens source.module/package.name=target.module. Cette directive modifie dynamiquement le graphe d'accessibilité interne du module, ajoutant le module cible à l'ensemble des modules autorisés à effectuer une réflexion profonde sur les membres privés de ce package.
// Module : app.core (module-info.java) module app.core { // Le package com.app.internal n'est pas ouvert exports com.app.api; } // Module : framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // Lance InaccessibleObjectException sans --add-opens VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
Une équipe de développement a migré son application monolithique basée sur Spring vers le Java Module System, partitionnant le code en un module de logique métier principale (app.core) et un module de framework d'injection de dépendances distinct (framework.inject). Immédiatement après le déploiement, l'application a échoué lors de l'initialisation des beans avec une InaccessibleObjectException lorsque le framework a tenté d'injecter des valeurs de configuration dans des champs privés situés dans le package interne com.app.internal de app.core.
Trois solutions architecturales potentielles ont été évaluées. La première approche consistait à déplacer toutes les classes injectables dans des packages exportés au sein de app.core. Bien que cela résolve la violation d'accès immédiate, cela violerait fondamentalement les principes d'encapsulation en exposant les détails de mise en œuvre internes à tous les autres modules, augmentant la charge de maintenance et élargissant la surface d'attaque pour les futurs audits de sécurité. La deuxième solution proposait d'utiliser le paramètre JVM --add-exports pour exposer les packages internes au module de framework. Cependant, bien que --add-exports accorde une visibilité à la compilation et à l'exécution sur les types publics, il n'autorise pas explicitement la réflexion profonde sur les membres privés, le rendant insuffisant pour les mécanismes d'injection de champs de Spring qui requièrent la modification de l'état privé. La troisième option utilisait l'argument de ligne de commande ciblé --add-opens app.core/com.app.internal=framework.inject. Cette approche maintenait une encapsulation stricte au niveau source pour tous les autres modules tout en accordant explicitement uniquement au cadre d'injection les privilèges nécessaires pour effectuer une réflexion profonde sur le package interne spécifique.
L'équipe a finalement sélectionné la troisième option, documentant les directives --add-opens requises dans leurs scripts de déploiement et configurations Docker. Cette solution a préservé l'intégrité du système de modules pendant le développement tout en permettant au framework de fonctionner correctement, aboutissant à une migration réussie avec des frontières d'accès explicitement contrôlées.
Pourquoi setAccessible(true) échoue-t-il sur un champ privé dans un package exporté lorsqu'il est accédé depuis un autre module, malgré l'absence de SecurityManager ?
Les candidats confondent souvent l'exportation de packages avec l'ouverture. La directive exports ne rend accessible que les types et membres publics pour la compilation et l'invocation standard ; elle n'accorde pas le ReflectPermission nécessaire pour supprimer les contrôles d'accès au langage Java. La forte encapsulation de JPMS fonctionne indépendamment du SecurityManager, appliquée directement par les mécanismes de contrôle d'accès de la JVM. Pour activer setAccessible(true) sur des membres non publics, le package doit être explicitement déclaré comme open, ou l'ensemble du module doit être déclaré comme un open module.
Comment le mécanisme de capture de MethodHandles.Lookup influence-t-il l'accessibilité entre modules, et pourquoi l'invocation de MethodHandles.lookup().in(targetClass) pourrait-elle dégrader les capacités de recherche ?
Un objet Lookup encapsule les privilèges d'accès du module et du contexte de package de son créateur. Lorsque Lookup.in(targetClass) est invoqué, la JVM réévalue les privilèges de recherche en fonction du module de la classe cible. Si la classe cible se trouve dans un module différent qui n'a pas ouvert son package au module de recherche, la recherche est "dégradée" en mode PUBLIC, lui enlevant les capacités d'accès PRIVATE et MODULE. Pour maintenir des droits d'accès complets entre les modules, le module cible doit explicitement ouvrir le package au module de recherche, ou le code doit utiliser privateLookupIn, ce qui nécessite que la classe cible soit dans le même module ou accessible via le graphe de modules.
Quelle distinction fondamentale existe entre --add-exports et --add-opens au niveau de la JVM, et pourquoi ce dernier causes-t-il une IllegalAccessException lors de l'injection de dépendances même lorsque la compilation réussit ?
Le paramètre --add-exports ajoute un package à la liste des packages exportés du module, permettant au module cible d'accéder aux types publics tant à la compilation qu'à l'exécution. Cependant, cette directive ne modifie pas l'ensemble "ouvert" du module, qui contrôle la réflexion profonde. La Java Language Specification sépare strictement la lisibilité (exportations) de la réfléchabilité (ouvertures). Les frameworks d'injection de dépendances nécessitent ce dernier pour manipuler des champs privés via Reflection ou VarHandle. Par conséquent, bien que --add-exports satisfasse le compilateur et permette l'invocation de méthode, les tentatives d'exécution pour modifier l'état privé échoueront toujours. Seul --add-opens ajoute le package à l'ensemble des packages accessibles pour une réflexion profonde, permettant ainsi au framework de modifier les valeurs des champs privés.