Historia. Antes de Java 9, la reflexión podía eludir arbitrariamente los modificadores de acceso a través de setAccessible(true), rompiendo la encapsulación a voluntad. La introducción del Java Platform Module System (JPMS) estableció una fuerte encapsulación por defecto, donde los módulos deben otorgar explícitamente permiso para el acceso reflexivo profundo a sus paquetes internos.
Problema. Cuando el código en un módulo intenta usar MethodHandles o reflexión central para acceder a un campo no público en el paquete de otro módulo, la JVM realiza una verificación rigurosa de accesibilidad. Esta verificación asegura que el paquete objetivo haya sido explícitamente abierto al módulo del llamador. Sin este permiso, la JVM lanza una InaccessibleObjectException (o IllegalAccessException para la reflexión heredada), independientemente de si se ha instalado un SecurityManager o si se accede al campo a través de VarHandle.
Solución. El módulo debe declarar opens package.name [to specific.module]; en su module-info.java, o la aplicación debe iniciarse con el argumento --add-opens source.module/package.name=target.module. Esta directiva modifica dinámicamente el gráfico de accesibilidad interno del módulo, añadiendo el módulo objetivo al conjunto de módulos autorizados para realizar reflexión profunda sobre los miembros privados de ese paquete.
// Módulo: app.core (module-info.java) module app.core { // El paquete com.app.internal no está abierto exports com.app.api; } // Módulo: framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // Lanza InaccessibleObjectException sin --add-opens VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
Un equipo de desarrollo migró su aplicación monolítica basada en Spring al Java Module System, dividiendo la base de código en el módulo de lógica empresarial central (app.core) y un módulo separado para el marco de inyección de dependencias (framework.inject). Inmediatamente después del despliegue, la aplicación se bloqueó durante la inicialización de beans con una InaccessibleObjectException cuando el marco intentó inyectar valores de configuración en campos privados que residen dentro del paquete interno com.app.internal de app.core.
Se evaluaron tres posibles soluciones arquitectónicas. El primer enfoque consistió en reubicar todas las clases inyectables en paquetes exportados dentro de app.core. Si bien esto resolvería la violación de acceso inmediata, violaría fundamentalmente los principios de encapsulación al exponer detalles de implementación interna a todos los demás módulos, incrementando así la carga de mantenimiento y ampliando la superficie de ataque para futuras auditorías de seguridad. La segunda solución propuso utilizar el argumento de JVM --add-exports para exponer los paquetes internos al módulo de marco. Sin embargo, mientras que --add-exports otorga visibilidad en tiempo de compilación y tiempo de ejecución a tipos públicos, explícitamente no permite reflexión profunda sobre miembros privados, lo que lo hace insuficiente para los mecanismos de inyección de campo de Spring que requieren modificar el estado privado. La tercera opción utilizó el argumento de línea de comandos dirigido --add-opens app.core/com.app.internal=framework.inject. Este enfoque mantuvo una estricta encapsulación a nivel de código fuente para todos los demás módulos mientras otorgaba explícitamente solo al marco de inyección los privilegios necesarios para realizar reflexión profunda sobre el paquete interno específico.
El equipo finalmente seleccionó la tercera opción, documentando las directivas --add-opens requeridas en sus scripts de despliegue y configuraciones de Docker. Esta solución preservó la integridad del sistema de módulos durante el desarrollo mientras permitía que el marco funcionara correctamente, resultando en una migración exitosa con límites de acceso controlados explícitamente.
¿Por qué setAccessible(true) falla en un campo privado dentro de un paquete exportado cuando se accede desde un módulo diferente, a pesar de la ausencia de un SecurityManager?
Los candidatos a menudo confunden la exportación de paquetes con la apertura. La directiva exports solo hace accesibles los tipos y miembros públicos para la compilación y la invocación estándar; no otorga el ReflectPermission requerido para suprimir las verificaciones de acceso del lenguaje Java. La fuerte encapsulación de JPMS funciona independientemente del SecurityManager, aplicándose directamente a los mecanismos de control de acceso de la JVM. Para habilitar setAccessible(true) en miembros no públicos, el paquete debe ser declarado explícitamente como open, o el módulo completo debe ser declarado como un open module.
¿Cómo influye el mecanismo de captura de MethodHandles.Lookup en la accesibilidad entre módulos y por qué invocar MethodHandles.lookup().in(targetClass) podría degradar las capacidades de búsqueda?
Un objeto Lookup encapsula los privilegios de acceso del contexto de módulo y paquete de su creador. Cuando se invoca Lookup.in(targetClass), la JVM reevalúa los privilegios de búsqueda en función del módulo de la clase objetivo. Si la clase objetivo se encuentra en un módulo diferente que no ha abierto su paquete al módulo de búsqueda, la búsqueda se "degrada" a modo PUBLIC, despojándola del acceso PRIVATE y MODULE. Para mantener plenos derechos de acceso entre módulos, el módulo objetivo debe abrir explícitamente el paquete al módulo de búsqueda, o el código debe utilizar privateLookupIn, que requiere que la clase objetivo esté dentro del mismo módulo o accesible a través del gráfico de módulos.
¿Qué distinción fundamental existe entre --add-exports y --add-opens a nivel de JVM y por qué el primero causa IllegalAccessException durante la inyección de dependencias incluso cuando la compilación tiene éxito?
La bandera --add-exports añade un paquete a la lista exportada del módulo, permitiendo que el módulo objetivo acceda a tipos públicos tanto en tiempo de compilación como en tiempo de ejecución. Sin embargo, esta directiva no modifica el conjunto "abierto" del módulo, que controla la reflexión profunda. La Especificación del Lenguaje Java separa estrictamente la legibilidad (exports) de la reflejabilidad (opens). Los marcos de inyección de dependencias requieren lo último para manipular campos privados a través de Reflexión o VarHandle. En consecuencia, aunque --add-exports satisface al compilador y permite la invocación de métodos, los intentos en tiempo de ejecución para modificar el estado privado aún fallarán. Solo --add-opens añade el paquete al conjunto de paquetes accesibles para la reflexión profunda, permitiendo que el marco altere los valores de los campos privados.