plugin compiler artifactid java maven java-8 maven-3 maven-compiler-plugin

java - artifactid - maven-compiler-plugin version



IllegalAccessError cuando se usa una referencia de método público de una clase de paquete privado a través de una subclase pública de otro paquete (2)

Ayer enfrenté un problema interesante después de implementar mi aplicación web Java 8 en Tomcat 8. En lugar de cómo resolver este problema, me interesa más entender por qué sucede eso. Pero empecemos por el principio.

Tengo dos clases definidas de la siguiente manera:

Foo.java

package package1; abstract class Foo { public String getFoo() { return "foo"; } }

Bar.java

package package1; public class Bar extends Foo { public String getBar() { return "bar"; } }

Como puede ver, están en el mismo paquete y, en última instancia, terminan en el mismo contenedor , llamémoslo commons.jar . Este contenedor es una dependencia de mi aplicación web (es decir, como se ha definido como dependencia en el archivo pom.xml de mi aplicación).

En mi aplicación web, hay un trozo de código que hace:

package package2; public class Something { ... Bar[] sortedBars = bars.stream() .sorted(Comparator.comparing(Bar::getBar) .thenComparing(Bar::getFoo)) .toArray(Bar[]::new); ... }

y cuando se ejecuta obtengo:

java.lang.IllegalAccessError: tried to access class package1.Foo from class package2.Something

Jugando y experimentando, pude evitar el error de tres maneras:

  1. cambiar la clase Foo para que sea pública en lugar de paquete-privada;

  2. cambiar el paquete de la clase Something para que sea "package1" (es decir, literalmente lo mismo que las clases Foo y Bar, pero físicamente diferente es la clase Something definida en la aplicación web);

  3. forzando la carga de clase de Foo antes de ejecutar el código ofensivo:

    try { Class<?> fooClass = Class.forName("package1.Foo"); } catch (ClassNotFoundException e) { }

¿Alguien me puede dar una explicación técnica clara que justifique el problema y los resultados anteriores?

Actualización 1

Cuando probé la tercera solución, en realidad estaba usando el commons.jar de la primera (la clase en la que la clase Foo es pública en lugar del paquete privado). Mi mal lo siento

Además, como señalé en uno de mis comentarios, traté de registrar el cargador de clases de la clase Bar y la clase Something, justo antes del código ofensivo y el resultado para ambos fue:

WebappClassLoader context: my-web-app delegate: false ----------> Parent Classloader: java.net.URLClassLoader@681a9515

Actualización 2

Ok, finalmente resolví uno de los misterios!

En uno de mis comentarios dije que no podía replicar el problema ejecutando el código ofensivo desde un simple main creado en un paquete diferente al de Foo y Bar del commons.jar . Bueno ... ¡Eclipse (4.5.2) y Maven (3.3.3) me engañaron aquí!

Con este sencillo pom:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>my.test</groupId> <artifactId>commons</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> </project>

  1. si ejecuto "mvn clean package" (como Eclipse Run Configuration) y ejecuto main desde Eclipse obtengo el maravilloso IllegalAccessError (¡genial!);

  2. si ejecuto Maven -> Actualizar proyecto ... y ejecuto el main desde Eclipse, no obtengo ningún error (¡no está bien!).

Así que me cambié a la línea de comandos y confirmé la primera opción: el error aparece constantemente independientemente de si el código ofensivo está en la aplicación web o en el contenedor. ¡Bonito!

Luego, pude simplificar aún más la clase Something y descubrí algo interesante:

package package2; import java.util.stream.Stream; import package1.Bar; public class Something { public static void main(String[] args) { System.out.println(new Bar().getFoo()); // "foo" Stream.of(new Bar()).map(Bar::getFoo).forEach(System.out::println); // IllegalAccessError } }

Estoy a punto de ser blasfemo aquí, así que tengan paciencia conmigo: ¿podría ser que la referencia del método Bar :: getFoo simplemente se "resuelva" a la referencia del método Foo :: getFoo y, dado que la clase Foo no es visible en Something? Foo paquete privado), se lanza el IllegalAccessError?


Pude reproducir el mismo problema compilando en Eclipse (Mars, 4.5.1 ) y desde la línea de comandos usando Maven (Maven Compiler Plugin versión 3.5.1 , la última en este momento).

  • Compilando y ejecutando el main desde Eclipse> No Error
  • Compilando desde la consola / Maven y ejecutando el main desde Eclipse> Error
  • Compilando desde la consola / Maven y ejecutando el principal a través de exec:java desde la consola> Error
  • Compilando desde Eclipse y ejecutando main a través de exec:java desde la consola> Sin error
  • Compilando desde la línea de comandos directamente con javac (sin Eclipse, sin Maven, jdk-8u73 ) y ejecutando desde la línea de comandos directamente con java > Error

    foo Exception in thread "main" java.lang.IllegalAccessError: tried to access class com.sample.package1.Foo from class com.sample.package2.Main at com.sample.package2.Main.lambda$MR$main$getFoo$e8593739$1(Main.java:14) at com.sample.package2.Main$$Lambda$1/2055281021.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source) at java.util.stream.Streams$StreamBuilderImpl.forEachRemaining(Unknown Source) at java.util.stream.AbstractPipeline.copyInto(Unknown Source) at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source) at java.util.stream.AbstractPipeline.evaluate(Unknown Source) at java.util.stream.ReferencePipeline.forEach(Unknown Source) at com.sample.package2.Main.main(Main.java:14)

Tenga en cuenta el stacktrace anterior, la primera invocación (pre-java-8) funciona bien, mientras que la segunda (basada en java-8) lanza una excepción.

Después de una investigación, encontré relevantes los siguientes enlaces:

  • Informe de error JDK-8068152 , que describe un problema similar y, sobre todo, menciona lo siguiente sobre el Complemento del compilador de Maven y Java:

    Esto parece un problema inducido por el complemento de maven proporcionado. El complemento de maven proporcionado (en el directorio "plugin") agrega "tools.jar" al ClassLoader.getSystemClassLoader() , y esto está provocando el problema. Realmente no veo mucho que podría (o debería) hacerse en el lado de javac, lo siento.

    En más detalles, ToolProvider.getSystemJavaCompiler() buscará en ClassLoader.getSystemClassLoader() para encontrar clases javac. Si no encuentra javac allí, intenta encontrar tools.jar automáticamente y crea un URLClassLoader para tools.jar, cargando el javac usando este cargador de clases. Cuando la compilación se ejecuta con este cargador de clases, carga las clases con este cargador de clases. Pero luego, cuando los complementos agreguen tools.jar al ClassLoader.getSystemClassLoader() , las clases comenzarán a ser cargadas por el cargador de clases del sistema. Y el acceso privado al paquete se deniega cuando se accede a una clase desde el mismo paquete pero se carga por un cargador de clases diferente, lo que lleva al error anterior . Esto se agrava debido a que Maven almacena en caché los resultados de ToolProvider.getSystemJavaCompiler() , gracias a los cuales la ejecución del complemento entre dos compilaciones aún conduce al error.

    (NOTA: negrita es mía)

  • Complemento del compilador de Maven: utiliza compiladores que no son Javac y describe cómo puedes conectar un compilador diferente al complemento del compilador de Maven y usarlo.

Entonces, simplemente cambiando de la configuración a continuación:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin>

A lo siguiente:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <compilerId>eclipse</compilerId> </configuration> <dependencies> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-compiler-eclipse</artifactId> <version>2.7</version> </dependency> </dependencies> </plugin>

Se IllegalAccessError el problema , no más IllegalAccessError , para el mismo código. Pero al hacerlo, eliminamos la diferencia entre Maven y Eclipse en este contexto (haciendo que Maven use el compilador de Eclipse), por lo que fue una especie de resultado normal.

Así que, de hecho, esto lleva a las siguientes conclusiones:

  • El compilador de Java Eclipse es diferente al compilador de Java Maven, nada nuevo en este caso, pero esa es otra confirmación.
  • El compilador Java Maven en este caso tiene un problema, mientras que el compilador Java Eclipse no lo tiene. Sin embargo, el compilador de Maven es coherente con el compilador de JDK. Así que en realidad podría ser un error en el JDK que tiene efecto en el compilador de Maven.
  • Hacer que Maven use el mismo compilador de Eclipse soluciona el problema o lo oculta .

Como referencia, probé también lo siguiente sin mucho éxito antes de cambiar al compilador de eclipse para Maven:

  • Cambiando la versión del complemento de compilador de Maven, cada versión desde 2.5 hasta 3.5.1
  • Probando con JDK-8u25, JDK-8u60, JDK-8u73
  • Asegurarse de que Eclipse y Maven Compiler usen exactamente el mismo javac, explícitamente usando la opción executable

Para resumir, el JDK es coherente con Maven, y probablemente sea un error. A continuación algunos informes de errores relacionados que encontré:

  • JDK-8029707 : IllegalAccessError mediante un método heredado de llamada funcional al consumidor . Se corrigió como No se solucionó (era exactamente el mismo problema)
  • JDK-8141122 : IllegalAccessException usando la referencia del método a la clase de paquete privado a través de pub . Abrir (de nuevo, exactamente el mismo problema)
  • JDK-8143647 : Javac compila referencia de método que permite resultados en un IllegalAccessError . Se corrigió en Java 9 (problema similar, el código pre-java-8 funcionaría bien, el código de estilo java-8 se rompería)
  • JDK-8138667 : java.lang.IllegalAccessError: intentó acceder al método (para un método protegido) . Abierto (problema similar, multa de compilación pero error de tiempo de ejecución para acceso ilegal en el código lambda).

Si los paquetes commons.jar y jar con package2 son cargados por otro cargador de clases, entonces se trata de paquetes de tiempo de ejecución diferentes y esto impide que los métodos de la clase Something accedan a los miembros del paquete de Foo . Consulte el capítulo 5.4.4 de la especificación JVM y este increíble tema .

Creo que hay una solución más además de lo que ya probaste : reemplazar el método getFoo en la clase Bar