This repository helps me reproduce and correctly understand classloader problem with generation of AssertJ POJO assertion classes.
Github issue has #57.
Current version of assertj-assertions-generator-maven-plugin has an unpleasant bug, when user runs mvn clean install
and later just mvn test
generator fails on following error:
[ERROR] Failed to execute goal org.assertj:assertj-assertions-generator-maven-plugin:2.2.0:generate-assertions (default) on project maven-test-classpath: Execution default of goal org.assertj:assertj-assertions-generator-maven-plugin:2.2.0:generate-assertions failed: A required class was missing while executing org.assertj:assertj-assertions-generator-maven-plugin:2.2.0:generate-assertions: org/junit/rules/TestRule
[ERROR] -----------------------------------------------------
[ERROR] realm = plugin>org.assertj:assertj-assertions-generator-maven-plugin:2.2.0
[ERROR] strategy = org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy
[ERROR] urls[0] = file:/C:/Users/ivo.smid/.m2/repository/org/assertj/assertj-assertions-generator-maven-plugin/2.2.0/assertj-assertions-generator-maven-plugin-2.2.0.jar
[ERROR] urls[1] = file:/C:/Users/ivo.smid/.m2/repository/org/assertj/assertj-assertions-generator/2.2.0/assertj-assertions-generator-2.2.0.jar
[ERROR] urls[2] = file:/C:/Users/ivo.smid/.m2/repository/org/apache/commons/commons-lang3/3.5/commons-lang3-3.5.jar
[ERROR] urls[3] = file:/C:/Users/ivo.smid/.m2/repository/commons-cli/commons-cli/1.2/commons-cli-1.2.jar
[ERROR] urls[4] = file:/C:/Users/ivo.smid/.m2/repository/com/google/guava/guava/20.0/guava-20.0.jar
[ERROR] urls[5] = file:/C:/Users/ivo.smid/.m2/repository/ch/qos/logback/logback-classic/1.0.13/logback-classic-1.0.13.jar
[ERROR] urls[6] = file:/C:/Users/ivo.smid/.m2/repository/ch/qos/logback/logback-core/1.0.13/logback-core-1.0.13.jar
[ERROR] urls[7] = file:/C:/Users/ivo.smid/.m2/repository/org/assertj/assertj-core/2.9.1/assertj-core-2.9.1.jar
[ERROR] urls[8] = file:/C:/Users/ivo.smid/.m2/repository/commons-collections/commons-collections/3.2.1/commons-collections-3.2.1.jar
[ERROR] urls[9] = file:/C:/Users/ivo.smid/.m2/repository/commons-io/commons-io/2.5/commons-io-2.5.jar
[ERROR] urls[10] = file:/C:/Users/ivo.smid/.m2/repository/backport-util-concurrent/backport-util-concurrent/3.1/backport-util-concurrent-3.1.jar
[ERROR] urls[11] = file:/C:/Users/ivo.smid/.m2/repository/org/codehaus/plexus/plexus-interpolation/1.11/plexus-interpolation-1.11.jar
[ERROR] urls[12] = file:/C:/Users/ivo.smid/.m2/repository/org/codehaus/plexus/plexus-utils/1.5.15/plexus-utils-1.5.15.jar
[ERROR] Number of foreign imports: 1
[ERROR] import: Entry[import from realm ClassRealm[maven.api, parent: null]]
[ERROR]
[ERROR] -----------------------------------------------------
[ERROR] : org.junit.rules.TestRule
It basically says that class org.junit.rules.TestRule
cannot be found, but we have this class on classpath!
Running mvn test -X
to see stack trace and debug output of Maven execution. Output reveals that class real cannot
be found. Again, why?
...
Caused by: java.lang.ClassNotFoundException: org.junit.rules.TestRule
at org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy.loadClass (SelfFirstStrategy.java:50)
at org.codehaus.plexus.classworlds.realm.ClassRealm.unsynchronizedLoadClass (ClassRealm.java:271)
at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass (ClassRealm.java:247)
at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass (ClassRealm.java:239)
at java.lang.ClassLoader.defineClass1 (Native Method)
at java.lang.ClassLoader.defineClass (ClassLoader.java:1016)
...
AssertJ generator mojo works as follows.
- First it looks at project classloader and detects configured classes. In our case
cz.bedla.dto
package. - Second it generates assertion classes for those detected classes
When we have default plugin configuration as follows:
<plugin>
<groupId>org.assertj</groupId>
<artifactId>assertj-assertions-generator-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<goals>
<goal>generate-assertions</goal>
</goals>
</execution>
</executions>
<configuration>
<packages>
<param>cz.bedla.dto</param>
</packages>
</configuration>
</plugin>
After run, we expect few generated classes in directory target/generated-test-sources/assertj-assertions
cz/bedla/dto/AbstractFooAssert.java
cz/bedla/dto/Assertions.java
cz/bedla/dto/BddAssertions.java
cz/bedla/dto/FooAssert.java
cz/bedla/dto/JUnitSoftAssertions.java
Those classes are compiled without error with mvn clean install
. With mvn test
command we got error as
described at beginning.
First run of mvn clean install
means that package cz.bedla.dto
contains only one class cz.bedla.dto.Foo
as input for the generator.
Second run of mvn test
means that package cz.bedla.dto
contains that DTO class cz.bedla.dto.Foo
and
generated classes from first run:
cz.bedla.dto.AbstractFooAssert
cz.bedla.dto.Assertions
cz.bedla.dto.BddAssertions
cz.bedla.dto.FooAssert
cz.bedla.dto.JUnitSoftAssertions
From plugin point of view those classes have to be analysed and that's the problem. For details see "Technical description of problem" chapter.
When you need JUnit4 soft assertions to be generated you have to repeat JUnit4 dependency on plugin's classpath.
<plugin>
<groupId>org.assertj</groupId>
<artifactId>assertj-assertions-generator-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<goals>
<goal>generate-assertions</goal>
</goals>
</execution>
</executions>
<configuration>
<generateJUnitSoftAssertions>true</generateJUnitSoftAssertions>
<packages>
<param>cz.bedla.dto</param>
</packages>
</configuration>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</plugin>
Build dependency of JUnit4 is only used to detect if it is possible to generate JUnit4 soft-assertions.
See org.assertj.maven.AssertJAssertionsGeneratorMojo#junitFoundBy
method, and it's usage.
When you don't need JUnit4 soft assertion (for example when you use JUnit5, etc.) it is possible disable their generation.
<plugin>
<groupId>org.assertj</groupId>
<artifactId>assertj-assertions-generator-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<goals>
<goal>generate-assertions</goal>
</goals>
</execution>
</executions>
<configuration>
<generateJUnitSoftAssertions>false</generateJUnitSoftAssertions>
<packages>
<param>cz.bedla.dto</param>
</packages>
</configuration>
</plugin>
Note: in this case you don't need JUnit4 plugin's dependency.
When you need JUnit4 soft assertion, but don't want to add artificial plugin dependency of JUnit4 on the plugin's class path.
Flag cleanTargetDir
seems reasonable but, it clears only generates .java
files, not compiled .class
files from
previous run. This means that error occurs even in this case.
<plugin>
<groupId>org.assertj</groupId>
<artifactId>assertj-assertions-generator-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<goals>
<goal>generate-assertions</goal>
</goals>
</execution>
</executions>
<configuration>
<generateJUnitSoftAssertions>true</generateJUnitSoftAssertions>
<cleanTargetDir>true</cleanTargetDir>
<packages>
<param>cz.bedla.dto</param>
</packages>
</configuration>
</plugin>
Note: in this case you don't need JUnit4 plugin's dependency.
As described above second run of generator with already generated assertion classes fails badly with classloader error.
Problem is with plugin flag generateJUnitSoftAssertions=true
and generated class cz.bedla.dto.JUnitSoftAssertions
from target/generated-test-sources/assertj-assertions
directory.
First let's look at class in question.
Generated class extends class from assertj-core-2.9.1.jar
.
package cz.bedla.dto;
@javax.annotation.Generated(value="assertj-assertions-generator")
public class JUnitSoftAssertions extends org.assertj.core.api.JUnitSoftAssertions {
// class body here
}
AssertJ Core class implements interface from junit-4.12.jar
.
package org.assertj.core.api;
public class JUnitSoftAssertions extends AbstractStandardSoftAssertions implements org.junit.rules.TestRule {
// class body here
}
AssertJ plugin generator loads all the classes from package cz.bedla.dto
and process its metadata to decide if it
is possible to generate assertions for them.
Steps are following:
org.assertj.maven.AssertJAssertionsGeneratorMojo#execute(...)
compile and test classpath.
Method org.assertj.maven.AssertJAssertionsGeneratorMojo#getProjectClassLoader(...)
project.getCompileClasspathElements()
|
+- 0 = C:\Users\ivo.smid\IdeaProjects\maven-assertj-generator-test-error\target\classes
+- ...
project.getTestClasspathElements()
|
+- 0 = "C:\Users\ivo.smid\IdeaProjects\maven-assertj-generator-test-error\target\test-classes"
+- 1 = "C:\Users\ivo.smid\IdeaProjects\maven-assertj-generator-test-error\target\classes"
+- 9 = "C:\Users\ivo.smid\.m2\repository\org\assertj\assertj-core\2.9.1\assertj-core-2.9.1.jar"
+- 30 = "C:\Users\ivo.smid\.m2\repository\junit\junit\4.12\junit-4.12.jar"
+- ...
As you can see test classpath correctly contains assertj-core-2.9.1.jar
and junit-4.12.jar
.
This projectClassLoader
has a parent classloader from current Thread.currentThread().getContextClassLoader()
.
projectClassLoader = new URLClassLoader(..., Thread.currentThread().getContextClassLoader());
.URLClassPath
|
+- 37 = {URL@4444} "file:/C:/Users/ivo.smid/.m2/repository/org/assertj/assertj-core/2.9.1/assertj-core-2.9.1.jar"
+- 58 = {URL@4465} "file:/C:/Users/ivo.smid/.m2/repository/junit/junit/4.12/junit-4.12.jar"
+- ...
The contextClassLoader
is ClassWorlds Realm of current AssertJ generator plugin.
contextClassLoader = Thread.currentThread().getContextClassLoader() =
ClassRealm[plugin>org.assertj:assertj-assertions-generator-maven-plugin:2.2.0, parent: jdk.internal.loader.ClassLoaders$AppClassLoader@6ed3ef1]
.URLClassPath
|
+- 7 = {URL@4348} "file:/C:/Users/ivo.smid/.m2/repository/org/assertj/assertj-core/2.9.1/assertj-core-2.9.1.jar"
+- ...
.importRealms
|
+- ClassRealm[maven.api, parent: null] ]
+- ...
Note 1: there is not junit-4.12.jar
on contextClassLoader
's classpath.
Note 2: importRealm
is only one and points to main Maven's realm.
org.assertj.assertions.generator.util.ClassUtil#getPackageClassesFromClasspathFiles
0 = {URL@3728} "file:/C:/Users/ivo.smid/IdeaProjects/maven-assertj-generator-test-error/target/classes/cz%5cbedla%5cdto"
1 = {URL@3729} "file:/C:/Users/ivo.smid/IdeaProjects/maven-assertj-generator-test-error/target/test-classes/cz%5cbedla%5cdto"
This will also find cz/bedla/dto/JUnitSoftAssertions.class
class from compiled from generated test sources. And
will try to load metadata from it.
private static TypeToken<?> loadClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
return TypeToken.of(Class.forName(className, false, classLoader));
}
This will invoke Class#forName(...)
with projectClassLoader
as parameter. As we know from previous information
projectClassLoader
contains all the classes/libraries necessary to load JUnitSoftAssertions
classes.
projectClassLoader.loadClass("cz.bedla.dto.JUnitSoftAssertions")
The default strategy to load class is to ask parent classloader, if it already contains desired class.
This means that when JVM sees that cz.bedla.dto.JUnitSoftAssertions
extends
org.assertj.core.api.JUnitSoftAssertions
, it will try to load class by asking parent classloader for that class.
projectClassLoader.loadClass("org.assertj.core.api.JUnitSoftAssertions");
parent = contextClassLoader;
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// ...
}
Context class loader is ClassWorlds realm
ClassRealm[plugin>org.assertj:assertj-assertions-generator-maven-plugin:2.2.0, parent: ...]
with
assertj-core-2.9.1.jar
library, but without junit-4.12.jar
.
contextClassLoader.findClass("org.assertj.core.api.JUnitSoftAssertions")
Resource res = urlClassPath.getResource("org/assertj/core/api/JUnitSoftAssertions.class", false);
if (res != null) {
return defineClass(name, res);
} else {
return null;
}
contextClassLoader
asks it's urlClassPath
if it contains org.assertj.core.api.JUnitSoftAssertions
and it
answers yes by returning non-null res
variable.
Because realm of contextClassLoader
returns positive answer about having org.assertj.core.api.JUnitSoftAssertions
,
class loader is going to try to define this class.
Now it sees that org.assertj.core.api.JUnitSoftAssertions
class implements org.junit.rules.TestRule
interface.
Correctly contextClassLoader
tries to load that interface.
But it does not have one, it will ask it's importRealm
to load it, but without success and
with ClassNotFoundException
thrown.
contextClassLoader.loadClass("org.junit.rules.TestRule") == null
contextClassLoader.loadClassFromImport("org.junit.rules.TestRule")
importRealm.loadClass(""org.junit.rules.TestRule"") = throw new ClassNotFoundException(...)
Interface org.junit.rules.TestRule
currently lives in projectClassLoader
and contextClassLoader
tries to load it.
This two class loaders are in different hierarchy and that's why it is not possible to load that interface.
When you look at Workaround 1 - you need JUnit4 soft assertions generated
, it adds artificial dependency to
junit-4.12.jar
library to satisfy this load. It does not matter that JUnit4 (as AssertJ Core) loaded two times
inside projectClassLoader
and contextClassLoader
because it is only used to load metadata and generate assertions.