Andy Wilkinson opened SPR-16977 and commented
It appears the classpath scanning doesn't work when Surefire launches the JVM configured to use the module path. target/classes is placed on the module path and target/test-classes is patched into this module but classpath scanning only finds classes in target/test-classes.
I have attached a minimal sample that should reproduce the problem when built (mvn test) with Java 10. The sysout from the test should show that only the test class has been found:
[INFO] Running com.example.ScanningTest
[file [/Users/awilkinson/dev/temp/module-path-scanning/target/test-classes/com/example/ScanningTest.class]]
To my rather untrained eye, building with -X and examining the arguments that Surefire uses to launch the forked JVM (in target/surefire) suggests that Surefire's configuration of the JVM is correct.
When the sample is modified to work with Java 8 (remove module-info.java and change the compiler plugin configuration to remove <release>10</release>) the class in target/classes is also found:
[INFO] Running com.example.ScanningTest
[file [/Users/awilkinson/dev/temp/module-path-scanning/target/test-classes/com/example/ScanningTest.class], file [/Users/awilkinson/dev/temp/module-path-scanning/target/classes/com/example/One.class]]
Affects: 5.0.7
Reference URL: https://github.com/spring-projects/spring-boot/issues/13581
Attachments:
Issue Links:
2 votes, 7 watchers
Juergen Hoeller commented
This all depends on ClassLoader.getResources results for the given base package. The module system possibly only exposes the root URL for the patched part there? In any case, debugging ClassLoader.getResources results in both scenarios would help here...
Andy Wilkinson commented
On closer inspection, this appears to be a bug in the JDK. With the test changed to the following:
@Test
public void scanningTest() throws Exception {
Enumeration<URL> resourceUrls = getClass().getClassLoader().getResources("com/example");
while (resourceUrls.hasMoreElements()) {
System.out.println(resourceUrls.nextElement());
}
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
System.out.println(Arrays.toString(resolver.getResources("classpath*:com/example/**/*.class")));
}
It produces output similar to this:
[INFO] Running com.example.ScanningTest
file:/Users/awilkinson/dev/temp/module-path-scanning/target/test-classes/com/example/
file:/Users/awilkinson/dev/temp/module-path-scanning/target/test-classes/com/example
[file [/Users/awilkinson/dev/temp/module-path-scanning/target/test-classes/com/example/ScanningTest.class]]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.094 s - in com.example.ScanningTest
Note the two (slightly different) URLs that are both pointing to target/test-classes. This explains why no classes in target/classes are found.
Juergen Hoeller commented
Looks like a bug in the ClassLoader implementation indeed. They might not have fully tested this in module patch scenarios...
Probably worth reporting to OpenJDK. Aside from a proper fix, maybe there's a workaround to discover in time for our GA still.
Andy Wilkinson commented
I've reported a bug. It's in internal review at the moment (ID 9055803). I'll comment again here if it makes it out the other end and becomes public. For reference, this is the test case that I provided with the bug report. Unlike the sample attached to this issue, it does not use Spring Framework.
Andy Wilkinson commented
The bug聽is now public but has been closed as not an issue.聽The situation's now being discussed in this thread on the core libs dev OpenJDK mailing list.
Andy Wilkinson commented
The thread on the mailing list seems to have reached a conclusion. The following recommendation came from Alan Bateman:
With modules then it should be looking at the modules in the boot layer and using ModuleReader to get the contents. It can use the value of java.class.path to scan the class path.
Thomas Kratz commented
Are there any plans to resolve this like recommended?聽
Would doFindAllClassPathResources(String path)聽be the point to put that? I don't clearly understand that suggestion, but maybe I could invest some time over the holidays.
I would still think this is a JDK issue.
聽
Is there a workaround for this? I have a JavaFx project that I added Spring Boot to which works really great, but I am unable to run my @JsonTest.
As a workaround, I need to manually specify the application class + the json deserializers I want to test:
@JsonTest
class MyJsonDeserializerTest {
...
}
needs to become:
@JsonTest
@ContextConfiguration(classes = {MyApplication.class, MyJsonDeserializerTest.TestConfig.class})
class MyJsonDeserializerTest {
...
@TestConfiguration
static class TestConfig {
@Bean
public MyJsonDeserializer deserializer() {
return new MyJsonDeserializer();
}
}
}
(Note: It seems I only need to do this for Maven, IntelliJ seems to run the test fine, but I have to assume that IntelliJ is somehow "cheating" given this issue and the related https://github.com/spring-projects/spring-boot/issues/13581 )
Another year has passed.
To me this still is a showstopper.
I have the opposite problem currently. Failsafe works just fine, apparently because they point to the packaged JAR instead of the exploded target/classes directory. However, IntelliJ wants to use the exploded directory when running integration tests in the IDE, so this fails unless I uncheck the module-path option (which I have to do for every run configuration separately).
Additionally, I can make Failsafe break the same way as the IDE by forcing it to use the exploded directory (via <classesDirectory>${project.build.outputDirectory}</classesDirectory>), so there's something magic about being in a JAR versus hanging around on the filesystem -- but I'm not clear on what.
Is this truly a JDK issue or a Spring issue? It doesn't seem that the core JDK team plans on changing behavior, thus it seems incumbent on the Spring team to work around this somehow (or at the very least, document that component scanning will break unexpectedly when using exploded classes versus packaged in JAR classes....)
@jhoeller @wilkinsona could this be fixed inside spring by making the component scanning code be more module path aware? Especially for running spring tests on module path using the surefire plugin or in intellij this is a problem.
@tgolden-andplus I'm having the same issue.
For future reference; the only work around I found was to uncheck "use module path" in Templates > JUnit in your Run/Debug Generations to make sure it is unchecked for every new junit run config you create.
For me the workaround was using a org.springframework.core.io.ClassPathResource instead of ClassLoader.getResource
@manosbatsis and how dit you make sure that spring uses this ClassPathResource for its component scanning?
@tgolden-andplus problem with that workaround is that you then no longer have the module checking which is important. Also stops working when using e.g. serviceloader to load services from other modules in your code under test
Probably the only fix is a fix in the code of spring itself, solving the way it does component scanning to handle modules better.
@tomdw to be clear, i had trouble when trying to "manually" load a classpath resource from within tests using ClassLoader.getResource. After a search landed me here, i figured out a workaround was needed to bypass the bug and ClassPathResource did the trick for me, so i thought it might be useful.
I am facing similar issue when writing junit5 cases with spring boot and java 11. As the jdk bug raised is in resolved status, what is further action on this? How can this be resolved?
It seems module-info cannot be used together with junit and spring due to this issue. As it is blocking for me in my project I have no other way than to fall back to java 8 version.
I had the same (or very similar) issue running tests with maven-failsafe-plugin 3.0.0-M5 on AdoptOpen JDK 11. The work around is to disable using module path in plugin's configuration
<useModulePath>false</useModulePath>
I should mention that I am also suffering from this issue and it would really be appreciated if this could be solved by Spring if it is not going to be solved by Java itself.
We upgraded to a new version of spring boot 2.3.4 and all our integration tests started failing. All our integration test cases were earlier working on AdoptOpen JDK 11 with the older version. I guess this should be an issue at Spring end. As mentioned above the classpath scanning is only finding only "target/test-classes".
I was able to use the workaround as suggested by @kopper
Most helpful comment
I had the same (or very similar) issue running tests with maven-failsafe-plugin 3.0.0-M5 on AdoptOpen JDK 11. The work around is to disable using module path in plugin's configuration
<useModulePath>false</useModulePath>