Yesterday I faced an interesting issue after deploying my Java 8 webapp on Tomcat 8. Rather than how to solve this issue I'm more interested in understanding why that happens. But let's start from the beginning.
I have two classes defined as follows:
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";
}
}
As you can see, they are in the same package and, ultimately, end up in the same jar, let's call it commons.jar. This jar is a dependency of my webapp (i.e. as been defined as dependency in my webapp's pom.xml).
In my webapp, there is a piece of code which does:
package package2;
public class Something {
...
Bar[] sortedBars = bars.stream()
.sorted(Comparator.comparing(Bar::getBar)
.thenComparing(Bar::getFoo))
.toArray(Bar[]::new);
...
}
and when it is executed I get:
java.lang.IllegalAccessError: tried to access class package1.Foo from class package2.Something
Playing around and experimenting I was able to avoid the error in three two ways:
changing the Foo class to be public instead of package-private;
changing the package of the Something class to be "package1" (i.e. literally the same as the Foo and Bar classes but physically different being the Something class defined in the webapp);
forcing the class-loading of Foo before executing the offending code:try { Class<?> fooClass = Class.forName("package1.Foo"); } catch (ClassNotFoundException e) { }
Can someone give me a clear, technical explanation that justifies the issue and the above results?
Update 1
When I tried the third solution I was actually using the commons.jar of the first one (the one where the Foo class is public instead of package private). My bad sorry.
Moreover, as pointed out in one of my comments, I tried to log the classloader of the Bar class and Something class, right before the offending code and the result for both was:
WebappClassLoader
context: my-web-app
delegate: false
----------> Parent Classloader:
java.net.URLClassLoader@681a9515
Update 2
Ok, I finally solved one of the mysteries!
In one of my comments I said that I wasn't able to replicate the problem by executing the offending code from a simple main created in a different package than Foo and Bar of the commons.jar. Well...Eclipse (4.5.2) and Maven (3.3.3) fooled me here!
With this simple 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>
if I execute "mvn clean package" (as Eclipse Run Configuration) and run the main from within Eclipse I get the wonderful IllegalAccessError (cool!);
if I execute Maven -> Update project... and run the main from within Eclipse I don't get any error (not cool!).
So I switched to the command-line and I confirmed the first option: the error consistently appears regardless by whether the offending code is in the webapp or in the jar. Nice!
Then, I was able to further simplify the Something class and discovered something interesting:
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
}
}
I'm about to be blasphemous here so bear with me: could it be that the Bar::getFoo method reference simply get "resolved" to the Foo::getFoo method reference and, since the Foo class is not visible in Something (being Foo package private), the IllegalAccessError is thrown?
I was able to reproduce the same issue compiling in Eclipse (Mars, 4.5.1) and from command line using Maven (Maven Compiler Plugin version 3.5.1, the latest at the moment).
exec:java
from console > Errorexec:java
from console > No ErrorCompiling from command line directly with
javac
(no Eclipse, no Maven, jdk-8u73) and running from command line directly withjava
> ErrorNote the stacktrace above, the first (pre-java-8) invocation works fine while the second (java-8 based) throws an exception.
After some investigation, I found relevant the following links:
JDK-8068152 bug report, describing a similar issue and, above all, mentioning the following concerning the Maven Compiler Plugin and Java:
(NOTE: bold is mine)
Maven Compiler Plugin - Using Non-Javac Compilers, describing how you can plug a different compiler to the Maven Compiler Plugin and use it.
So, simply switching from the configuration below:
To the following:
Fixed the issue, no
IllegalAccessError
any more, for the same code. But doing so, we actually removed the diff between Maven and Eclipse in this context (making Maven using the Eclipse compiler), so it was kind of normal result.So indeed, this leads to the following conclusions:
For reference, I tried also the following without much success before switching to the eclipse compiler for Maven:
executable
optionTo summarize, the JDK is coherent with Maven, and it is most probably a bug. Below some related bug reports I found:
If packages commons.jar and jar with package2 are loaded by another class-loader, then it is different runtime packages and this fact preventing methods of Something class from access to package members of Foo. See chapter 5.4.4 of JVM spec and this awesome topic.
I think there is one more solution in addition to what you already tried: override method getFoo in Bar class