Skip to content

Commit 9d265a2

Browse files
authored
Add JPMS ServiceLoader Support with Multi-Release JAR (#500)
* #426 Add JPMS module support with ServiceLoader **Note:** This is not a complete solution as handling of JDK 8 compatibility is outstanding. Currently, the fix would break usage of JDK 8. Enable `exec:java`-goal to execute Java applications using the Java Platform Module System (JPMS) with proper ServiceLoader support. Changes: - Split execution logic into classpath and module-path modes - Detect module syntax (module/class) in mainClass parameter - Create ModuleLayer with resolveAndBind() to include service providers - Use ModuleLayer.Controller to open packages for reflective access - Set thread context classloader to module's classloader The plugin now properly handles: - Module-path execution when `mainClass` uses "module/class" syntax - ServiceLoader provider discovery and binding in modular applications - Reflective access to main methods in unexported packages - Mixed module and classpath dependencies Integration test mexec-gh-426 validates the ServiceLoader functionality with a multi-module JPMS application (contract, provider, consumer). Technical implementation: - Use Configuration.resolveAndBind() instead of resolve() to include service providers declared with "uses" in module-info - Obtain ModuleLayer.Controller to programmatically open packages via addOpens() for reflective main method invocation - Load classes through the module layer's ClassLoader to maintain proper module isolation and visibility The fix was created in the course of support-and-care/maven-support-and-care#138. * #426 Fix JSR-512 main method detection This commit fixes two issues that caused build failures in GitHub: 1. JSR-512 Main Method Detection Problem: Tests failed with error: "The specified mainClass doesn't contain a main method with appropriate signature" JSR-512 (Java 21+) allows flexible main methods that can be non-public and instance methods. The original code used getMethod() which only finds PUBLIC methods, causing it to fail when searching for package-private main methods. Solution: - Changed from getMethod() to getDeclaredMethod() for JSR-512 - Added setAccessible(true) for non-public method invocation - Created findMethod() helper that searches class hierarchy - Validates return type is void (JSR-512 requirement) - Maintains correct priority order: 1. static void main(String[]) - traditional 2. static void main() - JSR-512 static no-args 3. void main(String[]) - JSR-512 instance with args 4. void main() - JSR-512 instance no-args 2. SystemExitException Logging Problem: Integration tests expected [ERROR] log prefix for SystemExitException but it was missing. When SystemExitException was thrown during method invocation, it got wrapped in InvocationTargetException and bypassed the logging code. Solution: - Added special handling in InvocationTargetException catch - Detects SystemExitException in the cause chain - Logs with appropriate level (ERROR for non-zero exit codes) - Re-throws the exception to maintain expected behavior * #426 Create Multi-Release Jar * #426 Move common ExecJava code to base class * #494 Find classic main method even if private
1 parent 1c26293 commit 9d265a2

File tree

16 files changed

+1324
-810
lines changed

16 files changed

+1324
-810
lines changed

pom.xml

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@
139139
<invoker.parallelThreads>1C</invoker.parallelThreads>
140140
<project.build.outputTimestamp>2025-10-05T19:10:54Z</project.build.outputTimestamp>
141141
<maven-toolchains-plugin.version>3.2.0</maven-toolchains-plugin.version>
142+
143+
<mojo.java.target>8</mojo.java.target>
142144
</properties>
143145

144146
<dependencies>
@@ -287,24 +289,13 @@
287289
<configuration>
288290
<proc>none</proc>
289291
</configuration>
290-
<executions>
291-
<execution>
292-
<id>default-testCompile</id>
293-
<goals>
294-
<goal>testCompile</goal>
295-
</goals>
296-
<phase>test-compile</phase>
297-
<configuration>
298-
<parameters>true</parameters>
299-
</configuration>
300-
</execution>
301-
</executions>
302292
</plugin>
303293

304294
<plugin>
305295
<groupId>org.codehaus.mojo</groupId>
306296
<artifactId>animal-sniffer-maven-plugin</artifactId>
307297
<configuration>
298+
<skip>true</skip>
308299
<signature>
309300
<groupId>org.codehaus.mojo.signature</groupId>
310301
<artifactId>java18</artifactId>
@@ -374,6 +365,80 @@
374365
</build>
375366

376367
<profiles>
368+
<profile>
369+
<!-- Multi-Release JAR: Compile Java 9+ specific code when JDK 9+ is available -->
370+
<id>java9-mrjar</id>
371+
<activation>
372+
<jdk>[9,)</jdk>
373+
</activation>
374+
<build>
375+
<plugins>
376+
<plugin>
377+
<groupId>org.apache.maven.plugins</groupId>
378+
<artifactId>maven-compiler-plugin</artifactId>
379+
<executions>
380+
<!-- Compile Java 9+ specific code -->
381+
<execution>
382+
<id>java9-compile</id>
383+
<goals>
384+
<goal>compile</goal>
385+
</goals>
386+
<phase>compile</phase>
387+
<configuration>
388+
<release>9</release>
389+
<compileSourceRoots>
390+
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
391+
</compileSourceRoots>
392+
<outputDirectory>${project.build.outputDirectory}/META-INF/versions/9</outputDirectory>
393+
</configuration>
394+
</execution>
395+
<!-- Test compilation with parameters enabled -->
396+
<execution>
397+
<id>default-testCompile</id>
398+
<goals>
399+
<goal>testCompile</goal>
400+
</goals>
401+
<phase>test-compile</phase>
402+
<configuration>
403+
<source>9</source>
404+
<target>9</target>
405+
<parameters>true</parameters>
406+
</configuration>
407+
</execution>
408+
</executions>
409+
</plugin>
410+
</plugins>
411+
</build>
412+
</profile>
413+
<profile>
414+
<!-- JDK 8: Use simplified test compilation -->
415+
<id>java8-tests</id>
416+
<activation>
417+
<jdk>1.8</jdk>
418+
</activation>
419+
<build>
420+
<plugins>
421+
<plugin>
422+
<groupId>org.apache.maven.plugins</groupId>
423+
<artifactId>maven-compiler-plugin</artifactId>
424+
<executions>
425+
<execution>
426+
<id>default-testCompile</id>
427+
<goals>
428+
<goal>testCompile</goal>
429+
</goals>
430+
<phase>test-compile</phase>
431+
<configuration>
432+
<source>8</source>
433+
<target>8</target>
434+
<parameters>true</parameters>
435+
</configuration>
436+
</execution>
437+
</executions>
438+
</plugin>
439+
</plugins>
440+
</build>
441+
</profile>
377442
<profile>
378443
<!-- workaround for concurrent access to local repository on Windows -->
379444
<!-- https://issues.apache.org/jira/browse/MRESOLVER-372 -->
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.codehaus.mojo.exec.it</groupId>
8+
<artifactId>java_module-serviceloader</artifactId>
9+
<version>0.0.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>consumer</artifactId>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.codehaus.mojo.exec.it</groupId>
17+
<artifactId>contract</artifactId>
18+
<version>${project.version}</version>
19+
</dependency>
20+
<dependency>
21+
<groupId>org.codehaus.mojo.exec.it</groupId>
22+
<artifactId>provider</artifactId>
23+
<version>${project.version}</version>
24+
<scope>runtime</scope>
25+
</dependency>
26+
</dependencies>
27+
28+
<build>
29+
<plugins>
30+
<plugin>
31+
<groupId>org.codehaus.mojo</groupId>
32+
<artifactId>exec-maven-plugin</artifactId>
33+
<version>@project.version@</version>
34+
<configuration>
35+
<mainClass>consumer/org.example.consumer.App</mainClass>
36+
</configuration>
37+
<executions>
38+
<execution>
39+
<id>test-jpms-serviceloader</id>
40+
<phase>integration-test</phase>
41+
<goals>
42+
<goal>java</goal>
43+
</goals>
44+
</execution>
45+
</executions>
46+
</plugin>
47+
</plugins>
48+
</build>
49+
</project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module consumer {
2+
requires contract;
3+
uses org.example.api.Greeter;
4+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.example.consumer;
2+
3+
import java.util.ServiceLoader;
4+
import org.example.api.Greeter;
5+
6+
public class App {
7+
public static void main(String[] args) {
8+
ServiceLoader<Greeter> loader = ServiceLoader.load(Greeter.class);
9+
10+
Greeter greeter = loader.findFirst()
11+
.orElseThrow(() -> new RuntimeException("No Greeter service provider found"));
12+
13+
String message = greeter.greet("World");
14+
System.out.println(message);
15+
}
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.codehaus.mojo.exec.it</groupId>
8+
<artifactId>java_module-serviceloader</artifactId>
9+
<version>0.0.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>contract</artifactId>
13+
</project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module contract {
2+
exports org.example.api;
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.example.api;
2+
3+
public interface Greeter {
4+
String greet(String name);
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
invoker.goals = clean install
2+
invoker.java.version = 11+
3+
invoker.buildResult = success
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<groupId>org.codehaus.mojo.exec.it</groupId>
7+
<artifactId>java_module-serviceloader</artifactId>
8+
<version>0.0.1-SNAPSHOT</version>
9+
<packaging>pom</packaging>
10+
<name>JPMS ServiceLoader Test</name>
11+
<description>Test exec:java with Java Module System and ServiceLoader</description>
12+
13+
<modules>
14+
<module>contract</module>
15+
<module>provider</module>
16+
<module>consumer</module>
17+
</modules>
18+
19+
<properties>
20+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
21+
</properties>
22+
23+
<build>
24+
<pluginManagement>
25+
<plugins>
26+
<plugin>
27+
<groupId>org.apache.maven.plugins</groupId>
28+
<artifactId>maven-compiler-plugin</artifactId>
29+
<version>3.13.0</version>
30+
<configuration>
31+
<release>11</release>
32+
</configuration>
33+
</plugin>
34+
</plugins>
35+
</pluginManagement>
36+
</build>
37+
</project>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.codehaus.mojo.exec.it</groupId>
8+
<artifactId>java_module-serviceloader</artifactId>
9+
<version>0.0.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>provider</artifactId>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.codehaus.mojo.exec.it</groupId>
17+
<artifactId>contract</artifactId>
18+
<version>${project.version}</version>
19+
</dependency>
20+
</dependencies>
21+
</project>

0 commit comments

Comments
 (0)