From 652516902a43bb0ac1686c78092b5508064a35d4 Mon Sep 17 00:00:00 2001 From: Slawomir Jaranowski Date: Sun, 5 Oct 2025 21:10:54 +0200 Subject: [PATCH 1/5] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 313c26b9..4dadcd9b 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ exec-maven-plugin - 3.6.1 + 3.6.2-SNAPSHOT maven-plugin Exec Maven Plugin @@ -127,7 +127,7 @@ scm:git:https://github.com/mojohaus/exec-maven-plugin.git scm:git:ssh://git@github.com/mojohaus/exec-maven-plugin.git - 3.6.1 + HEAD https://github.com/mojohaus/exec-maven-plugin/tree/master @@ -137,7 +137,7 @@ 1.7.36 9.8 1C - 2025-10-05T19:10:01Z + 2025-10-05T19:10:54Z 3.2.0 From 1c262939f6c5f9caef59b2d80efa583d8626c095 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:02:07 +0000 Subject: [PATCH 2/5] Bump asm.version from 9.8 to 9.9 Bumps `asm.version` from 9.8 to 9.9. Updates `org.ow2.asm:asm` from 9.8 to 9.9 Updates `org.ow2.asm:asm-commons` from 9.8 to 9.9 --- updated-dependencies: - dependency-name: org.ow2.asm:asm dependency-version: '9.9' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.ow2.asm:asm-commons dependency-version: '9.9' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4dadcd9b..11ab0d35 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ 3.9.11 3.6.3 1.7.36 - 9.8 + 9.9 1C 2025-10-05T19:10:54Z 3.2.0 From 9d265a200b147bbf7f7a509c0ea6eca064a83c0e Mon Sep 17 00:00:00 2001 From: Gerd Aschemann Date: Sun, 19 Oct 2025 08:41:04 +0200 Subject: [PATCH 3/5] 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 --- pom.xml | 89 +- src/it/projects/mexec-gh-426/consumer/pom.xml | 49 + .../consumer/src/main/java/module-info.java | 4 + .../main/java/org/example/consumer/App.java | 16 + src/it/projects/mexec-gh-426/contract/pom.xml | 13 + .../contract/src/main/java/module-info.java | 3 + .../main/java/org/example/api/Greeter.java | 5 + .../projects/mexec-gh-426/invoker.properties | 3 + src/it/projects/mexec-gh-426/pom.xml | 37 + src/it/projects/mexec-gh-426/provider/pom.xml | 21 + .../provider/src/main/java/module-info.java | 5 + .../org/example/provider/GreeterImpl.java | 9 + src/it/projects/mexec-gh-426/verify.groovy | 36 + .../mojo/exec/AbstractExecJavaBase.java | 887 ++++++++++++++++++ .../org/codehaus/mojo/exec/ExecJavaMojo.java | 813 +--------------- .../org/codehaus/mojo/exec/ExecJavaMojo.java | 144 +++ 16 files changed, 1324 insertions(+), 810 deletions(-) create mode 100644 src/it/projects/mexec-gh-426/consumer/pom.xml create mode 100644 src/it/projects/mexec-gh-426/consumer/src/main/java/module-info.java create mode 100644 src/it/projects/mexec-gh-426/consumer/src/main/java/org/example/consumer/App.java create mode 100644 src/it/projects/mexec-gh-426/contract/pom.xml create mode 100644 src/it/projects/mexec-gh-426/contract/src/main/java/module-info.java create mode 100644 src/it/projects/mexec-gh-426/contract/src/main/java/org/example/api/Greeter.java create mode 100644 src/it/projects/mexec-gh-426/invoker.properties create mode 100644 src/it/projects/mexec-gh-426/pom.xml create mode 100644 src/it/projects/mexec-gh-426/provider/pom.xml create mode 100644 src/it/projects/mexec-gh-426/provider/src/main/java/module-info.java create mode 100644 src/it/projects/mexec-gh-426/provider/src/main/java/org/example/provider/GreeterImpl.java create mode 100644 src/it/projects/mexec-gh-426/verify.groovy create mode 100644 src/main/java/org/codehaus/mojo/exec/AbstractExecJavaBase.java create mode 100644 src/main/java9/org/codehaus/mojo/exec/ExecJavaMojo.java diff --git a/pom.xml b/pom.xml index 11ab0d35..20569b2e 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,8 @@ 1C 2025-10-05T19:10:54Z 3.2.0 + + 8 @@ -287,24 +289,13 @@ none - - - default-testCompile - - testCompile - - test-compile - - true - - - org.codehaus.mojo animal-sniffer-maven-plugin + true org.codehaus.mojo.signature java18 @@ -374,6 +365,80 @@ + + + java9-mrjar + + [9,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + java9-compile + + compile + + compile + + 9 + + ${project.basedir}/src/main/java9 + + ${project.build.outputDirectory}/META-INF/versions/9 + + + + + default-testCompile + + testCompile + + test-compile + + 9 + 9 + true + + + + + + + + + + java8-tests + + 1.8 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-testCompile + + testCompile + + test-compile + + 8 + 8 + true + + + + + + + diff --git a/src/it/projects/mexec-gh-426/consumer/pom.xml b/src/it/projects/mexec-gh-426/consumer/pom.xml new file mode 100644 index 00000000..7a24f250 --- /dev/null +++ b/src/it/projects/mexec-gh-426/consumer/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + java_module-serviceloader + 0.0.1-SNAPSHOT + + + consumer + + + + org.codehaus.mojo.exec.it + contract + ${project.version} + + + org.codehaus.mojo.exec.it + provider + ${project.version} + runtime + + + + + + + org.codehaus.mojo + exec-maven-plugin + @project.version@ + + consumer/org.example.consumer.App + + + + test-jpms-serviceloader + integration-test + + java + + + + + + + diff --git a/src/it/projects/mexec-gh-426/consumer/src/main/java/module-info.java b/src/it/projects/mexec-gh-426/consumer/src/main/java/module-info.java new file mode 100644 index 00000000..754c2150 --- /dev/null +++ b/src/it/projects/mexec-gh-426/consumer/src/main/java/module-info.java @@ -0,0 +1,4 @@ +module consumer { + requires contract; + uses org.example.api.Greeter; +} diff --git a/src/it/projects/mexec-gh-426/consumer/src/main/java/org/example/consumer/App.java b/src/it/projects/mexec-gh-426/consumer/src/main/java/org/example/consumer/App.java new file mode 100644 index 00000000..2fce234a --- /dev/null +++ b/src/it/projects/mexec-gh-426/consumer/src/main/java/org/example/consumer/App.java @@ -0,0 +1,16 @@ +package org.example.consumer; + +import java.util.ServiceLoader; +import org.example.api.Greeter; + +public class App { + public static void main(String[] args) { + ServiceLoader loader = ServiceLoader.load(Greeter.class); + + Greeter greeter = loader.findFirst() + .orElseThrow(() -> new RuntimeException("No Greeter service provider found")); + + String message = greeter.greet("World"); + System.out.println(message); + } +} diff --git a/src/it/projects/mexec-gh-426/contract/pom.xml b/src/it/projects/mexec-gh-426/contract/pom.xml new file mode 100644 index 00000000..48076ca1 --- /dev/null +++ b/src/it/projects/mexec-gh-426/contract/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + java_module-serviceloader + 0.0.1-SNAPSHOT + + + contract + diff --git a/src/it/projects/mexec-gh-426/contract/src/main/java/module-info.java b/src/it/projects/mexec-gh-426/contract/src/main/java/module-info.java new file mode 100644 index 00000000..b0f7d473 --- /dev/null +++ b/src/it/projects/mexec-gh-426/contract/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module contract { + exports org.example.api; +} diff --git a/src/it/projects/mexec-gh-426/contract/src/main/java/org/example/api/Greeter.java b/src/it/projects/mexec-gh-426/contract/src/main/java/org/example/api/Greeter.java new file mode 100644 index 00000000..4691e8e8 --- /dev/null +++ b/src/it/projects/mexec-gh-426/contract/src/main/java/org/example/api/Greeter.java @@ -0,0 +1,5 @@ +package org.example.api; + +public interface Greeter { + String greet(String name); +} diff --git a/src/it/projects/mexec-gh-426/invoker.properties b/src/it/projects/mexec-gh-426/invoker.properties new file mode 100644 index 00000000..54e94ff6 --- /dev/null +++ b/src/it/projects/mexec-gh-426/invoker.properties @@ -0,0 +1,3 @@ +invoker.goals = clean install +invoker.java.version = 11+ +invoker.buildResult = success diff --git a/src/it/projects/mexec-gh-426/pom.xml b/src/it/projects/mexec-gh-426/pom.xml new file mode 100644 index 00000000..ce8f1b62 --- /dev/null +++ b/src/it/projects/mexec-gh-426/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.codehaus.mojo.exec.it + java_module-serviceloader + 0.0.1-SNAPSHOT + pom + JPMS ServiceLoader Test + Test exec:java with Java Module System and ServiceLoader + + + contract + provider + consumer + + + + UTF-8 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 11 + + + + + + diff --git a/src/it/projects/mexec-gh-426/provider/pom.xml b/src/it/projects/mexec-gh-426/provider/pom.xml new file mode 100644 index 00000000..3fb0daa1 --- /dev/null +++ b/src/it/projects/mexec-gh-426/provider/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + + org.codehaus.mojo.exec.it + java_module-serviceloader + 0.0.1-SNAPSHOT + + + provider + + + + org.codehaus.mojo.exec.it + contract + ${project.version} + + + diff --git a/src/it/projects/mexec-gh-426/provider/src/main/java/module-info.java b/src/it/projects/mexec-gh-426/provider/src/main/java/module-info.java new file mode 100644 index 00000000..ff645382 --- /dev/null +++ b/src/it/projects/mexec-gh-426/provider/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module provider { + requires contract; + exports org.example.provider; + provides org.example.api.Greeter with org.example.provider.GreeterImpl; +} diff --git a/src/it/projects/mexec-gh-426/provider/src/main/java/org/example/provider/GreeterImpl.java b/src/it/projects/mexec-gh-426/provider/src/main/java/org/example/provider/GreeterImpl.java new file mode 100644 index 00000000..bb97eb1b --- /dev/null +++ b/src/it/projects/mexec-gh-426/provider/src/main/java/org/example/provider/GreeterImpl.java @@ -0,0 +1,9 @@ +package org.example.provider; + +import org.example.api.Greeter; + +public class GreeterImpl implements Greeter { + public String greet(String name) { + return "Hello from ServiceLoader, " + name + "!"; + } +} diff --git a/src/it/projects/mexec-gh-426/verify.groovy b/src/it/projects/mexec-gh-426/verify.groovy new file mode 100644 index 00000000..d2105eee --- /dev/null +++ b/src/it/projects/mexec-gh-426/verify.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Verify that the build log contains the expected output from ServiceLoader +File buildLog = new File(basedir, 'build.log') +assert buildLog.exists() + +String log = buildLog.getText('UTF-8') + +// Check that exec:java ran successfully +assert log.contains('[INFO] --- exec') +assert log.contains(':java') + +// Check that ServiceLoader found the provider and printed the greeting +assert log.contains('Hello from ServiceLoader, World!') + +// Ensure no errors about missing service providers +assert !log.contains('No Greeter service provider found') + +println "SUCCESS: JPMS ServiceLoader test passed - provider was loaded and executed correctly" diff --git a/src/main/java/org/codehaus/mojo/exec/AbstractExecJavaBase.java b/src/main/java/org/codehaus/mojo/exec/AbstractExecJavaBase.java new file mode 100644 index 00000000..e32ac197 --- /dev/null +++ b/src/main/java/org/codehaus/mojo/exec/AbstractExecJavaBase.java @@ -0,0 +1,887 @@ +package org.codehaus.mojo.exec; + +import javax.inject.Inject; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Parameter; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; + +import static java.util.stream.Collectors.toList; + +/** + * Abstract base class for ExecJavaMojo implementations (Java 8 and Java 9+). + * Contains all common code shared between the Java 8 base and Java 9+ version. + * + * @since 3.6.2 + */ +public abstract class AbstractExecJavaBase extends AbstractExecMojo { + // Implementation note: Constants can be included in javadocs by {@value #MY_CONST} + protected static final String THREAD_STOP_UNAVAILABLE = + "Thread.stop() is unavailable in this JRE version, cannot force-stop any threads"; + + /** + * The main class to execute.
+ * With Java 9 and above you can prefix it with the modulename, e.g. com.greetings/com.greetings.Main + * Without modulename the classpath will be used, with modulename a new modulelayer will be created. + *

+ * Note that you can also provide a {@link Runnable} fully qualified name. + * The runnable can get constructor injections either by type if you have maven in your classpath (can be provided) + * or by name (ensure to enable {@code -parameters} Java compiler option) for loose coupling. + * Current support loose injections are: + *

    + *
  • systemProperties: Properties, session system properties
  • + *
  • systemPropertiesUpdater: BiConsumer<String, String>, session system properties update callback (pass the key/value to update, null value means removal of the key)
  • + *
  • userProperties: Properties, session user properties
  • + *
  • userPropertiesUpdater: BiConsumer<String, String>, session user properties update callback (pass the key/value to update, null value means removal of the key)
  • + *
  • projectProperties: Properties, project properties
  • + *
  • projectPropertiesUpdater: BiConsumer<String, String>, project properties update callback (pass the key/value to update, null value means removal of the key)
  • + *
  • highestVersionResolver: Function<String, String>, passing a groupId:artifactId you get the latest resolved version from the project repositories
  • + *
+ * + * @since 1.0 + */ + @Parameter(required = true, property = "exec.mainClass") + protected String mainClass; + + /** + * Forces the creation of fork join common pool to avoids the threads to be owned by the isolated thread group + * and prevent a proper shutdown. + * If set to zero the default parallelism is used to precreate all threads, + * if negative it is ignored else the value is the one used to create the fork join threads. + * + * @since 3.0.1 + */ + @Parameter(property = "exec.preloadCommonPool", defaultValue = "0") + protected int preloadCommonPool; + + /** + * The class arguments. + * + * @since 1.0 + */ + @Parameter(property = "exec.arguments") + protected String[] arguments; + + /** + * A list of system properties to be passed. Note: as the execution is not forked, some system properties required + * by the JVM cannot be passed here. Use MAVEN_OPTS or the exec:exec instead. See the user guide for more + * information. + * + * @since 1.0 + */ + @Parameter + protected AbstractProperty[] systemProperties; + + /** + * Indicates if mojo should be kept running after the mainclass terminates. Use full for server like apps with + * daemon threads. + * + * @deprecated since 1.1-alpha-1 + * @since 1.0 + */ + @Parameter(property = "exec.keepAlive", defaultValue = "false") + @Deprecated + protected boolean keepAlive; + + /** + * Indicates if the project dependencies should be used when executing the main class. + * + * @since 1.1-beta-1 + */ + @Parameter(property = "exec.includeProjectDependencies", defaultValue = "true") + protected boolean includeProjectDependencies; + + /** + * Whether to interrupt/join and possibly stop the daemon threads upon quitting.
+ * If this is false, maven does nothing about the daemon threads. When maven has no more work to do, + * the VM will normally terminate any remaining daemon threads. + *

+ * In certain cases (in particular if maven is embedded), you might need to keep this enabled to make sure threads + * are properly cleaned up to ensure they don't interfere with subsequent activity. In that case, see + * {@link #daemonThreadJoinTimeout} and {@link #stopUnresponsiveDaemonThreads} for further tuning. + *

+ * + * @since 1.1-beta-1 + */ + @Parameter(property = "exec.cleanupDaemonThreads", defaultValue = "true") + protected boolean cleanupDaemonThreads; + + /** + * This defines the number of milliseconds to wait for daemon threads to quit following their interruption.
+ * This is only taken into account if {@link #cleanupDaemonThreads} is true. A value <=0 means to + * not timeout (i.e. wait indefinitely for threads to finish). Following a timeout, a warning will be logged. + *

+ * Note: properly coded threads should terminate upon interruption but some threads may prove problematic: as + * the VM does interrupt daemon threads, some code may not have been written to handle interruption properly. For + * example java.util.Timer is known to not handle interruptions in JDK <= 1.6. So it is not possible for us to + * infinitely wait by default otherwise maven could hang. A sensible default value has been chosen, but this default + * value may change in the future based on user feedback. + *

+ * + * @since 1.1-beta-1 + */ + @Parameter(property = "exec.daemonThreadJoinTimeout", defaultValue = "15000") + protected long daemonThreadJoinTimeout; + + /** + * Wether to call {@link Thread#stop()} following a timing out of waiting for an interrupted thread to finish. This + * is only taken into account if {@link #cleanupDaemonThreads} is true and the + * {@link #daemonThreadJoinTimeout} threshold has been reached for an uncooperative thread. If this is + * false, or if {@link Thread#stop()} fails to get the thread to stop, then a warning is logged and + * Maven will continue on while the affected threads (and related objects in memory) linger on. Consider setting + * this to true if you are invoking problematic code that you can't fix. An example is + * {@link java.util.Timer} which doesn't respond to interruption. To have Timer fixed, vote for + * this bug. + *

+ * Note: In JDK 20+, the long deprecated {@link Thread#stop()} (since JDK 1.2) has been removed and will + * throw an {@link UnsupportedOperationException}. This will be handled gracefully, yielding a log warning + * {@value #THREAD_STOP_UNAVAILABLE} once and not trying to stop any further threads during the same execution. + * + * @since 1.1-beta-1 + */ + @Parameter(property = "exec.stopUnresponsiveDaemonThreads", defaultValue = "false") + protected boolean stopUnresponsiveDaemonThreads; + + protected Properties originalSystemProperties; + + /** + * Additional elements to be appended to the classpath. + * + * @since 1.3 + */ + @Parameter + protected List additionalClasspathElements; + + /** + * List of file to exclude from the classpath. + * It matches the jar name, for example {@code slf4j-simple-1.7.30.jar}. + * + * @since 3.0.1 + */ + @Parameter + protected List classpathFilenameExclusions; + + /** + * Additional packages to load from the jvm even if a classpath dependency matches. + * + * @since 3.5.0 + */ + @Parameter + protected List forcedJvmPackages; + + /** + * Additional packages to NOT load from the jvm even if it is in a flat classpath. + * Can enable to reproduce a webapp behavior for example where library is loaded over the JVM. + * + * @since 3.5.0 + */ + @Parameter + protected List excludedJvmPackages; + + /** + * Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build) + * by calling {@link System#exit(int)}. When active, loaded classes will replace this call by a custom callback. + * In case of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. + * Otherwise, it will throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself + * had exited with an exception. + * This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users + * had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no + * longer necessary. + * + * @since 3.2.0 + */ + @Parameter(property = "exec.blockSystemExit", defaultValue = "false") + protected boolean blockSystemExit; + + // todo: for maven4 move to Lookup instead + protected final PlexusContainer container; + + @Inject + protected AbstractExecJavaBase(RepositorySystem repositorySystem, PlexusContainer container) { + super(repositorySystem); + this.container = Objects.requireNonNull(container); + } + + /** + * Execute goal. + * + * @throws MojoExecutionException execution of the main class or one of the threads it generated failed. + * @throws MojoFailureException something bad happened... + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (isSkip()) { + getLog().info("skipping execute as per configuration"); + return; + } + + if (null == arguments) { + arguments = new String[0]; + } + + if (getLog().isDebugEnabled()) { + StringBuffer msg = new StringBuffer("Invoking : "); + msg.append(mainClass); + msg.append(".main("); + for (int i = 0; i < arguments.length; i++) { + if (i > 0) { + msg.append(", "); + } + msg.append(arguments[i]); + } + msg.append(")"); + getLog().debug(msg); + } + + if (preloadCommonPool >= 0) { + preloadCommonPool(); + } + + IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(mainClass /* name */); + Thread bootstrapThread = new Thread( // TODO: drop this useless thread 99% of the time + threadGroup, + () -> { + int sepIndex = mainClass.indexOf('/'); + + final String bootClassName; + if (sepIndex >= 0) { + bootClassName = mainClass.substring(sepIndex + 1); + } else { + bootClassName = mainClass; + } + + try { + if (sepIndex >= 0) { + final String moduleName = mainClass.substring(0, sepIndex); + doExecModulePath(moduleName, bootClassName); + } else { + doExecClassLoader(bootClassName); + } + } catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on + Thread.currentThread() + .getThreadGroup() + .uncaughtException( + Thread.currentThread(), + new Exception( + "The specified mainClass doesn't contain a main method with appropriate signature.", + e)); + } catch (InvocationTargetException e) { + // use the cause if available to improve the plugin execution output + Throwable exceptionToReport = e.getCause() != null ? e.getCause() : e; + // Special handling for SystemExitException + if (exceptionToReport instanceof SystemExitException) { + SystemExitException systemExitException = (SystemExitException) exceptionToReport; + if (systemExitException.getExitCode() != 0) { + getLog().error(systemExitException.getMessage()); + throw systemExitException; + } else { + getLog().info(systemExitException.getMessage()); + } + } else { + Thread.currentThread() + .getThreadGroup() + .uncaughtException(Thread.currentThread(), exceptionToReport); + } + } catch (SystemExitException systemExitException) { + if (systemExitException.getExitCode() != 0) { + getLog().error(systemExitException.getMessage()); + throw systemExitException; + } else { + getLog().info(systemExitException.getMessage()); + } + } catch (Throwable e) { // just pass it on + Thread.currentThread().getThreadGroup().uncaughtException(Thread.currentThread(), e); + } + }, + mainClass + ".main()"); + URLClassLoader classLoader = getClassLoader(); // TODO: enable to cache accross executions + bootstrapThread.setContextClassLoader(classLoader); + setSystemProperties(); + + bootstrapThread.start(); + joinNonDaemonThreads(threadGroup); + // It's plausible that spontaneously a non-daemon thread might be created as we try and shut down, + // but it's too late since the termination condition (only daemon threads) has been triggered. + if (keepAlive) { + getLog().warn( + "Warning: keepAlive is now deprecated and obsolete. Do you need it? Please comment on MEXEC-6."); + waitFor(0); + } + + if (cleanupDaemonThreads) { + + terminateThreads(threadGroup); + + try { + threadGroup.destroy(); + } catch (RuntimeException | Error /* missing method in future java version */ e) { + getLog().warn("Couldn't destroy threadgroup " + threadGroup, e); + } + } + + if (classLoader != null) { + try { + classLoader.close(); + } catch (IOException e) { + getLog().error(e.getMessage(), e); + } + } + + if (originalSystemProperties != null) { + System.setProperties(originalSystemProperties); + } + + synchronized (threadGroup) { + if (threadGroup.uncaughtException != null) { + throw new MojoExecutionException( + "An exception occurred while executing the Java class. " + + threadGroup.uncaughtException.getMessage(), + threadGroup.uncaughtException); + } + } + + registerSourceRoots(); + } + + protected void doExecClassLoader(final String bootClassName) throws Throwable { + Class bootClass = Thread.currentThread().getContextClassLoader().loadClass(bootClassName); + executeMainMethod(bootClass); + } + + /** + * Execute using module path (Java 9+ JPMS). + * Subclasses must implement this method to provide version-specific module path execution. + * + * @param moduleName the module name + * @param bootClassName the fully qualified class name + * @throws Throwable if execution fails + */ + protected abstract void doExecModulePath(final String moduleName, final String bootClassName) throws Throwable; + + /** + * Execute the main method. Subclasses may override to provide version-specific behavior + * (e.g., Module opening logic for Java 9+). + * + * @param bootClass the class containing the main method + * @throws Throwable if execution fails + */ + protected void executeMainMethod(Class bootClass) throws Throwable { + // Try static main(String[] args) - highest priority (JEP 445) + java.lang.reflect.Method staticMainArgs = findMethod(bootClass, "main", true, new Class[] {String[].class}); + if (staticMainArgs != null) { + staticMainArgs.setAccessible(true); + staticMainArgs.invoke(null, (Object) arguments); + return; + } + + // Try static main() - JEP 445 + java.lang.reflect.Method staticMainNoArgs = findMethod(bootClass, "main", true, new Class[0]); + if (staticMainNoArgs != null) { + staticMainNoArgs.setAccessible(true); + staticMainNoArgs.invoke(null); + return; + } + + // Try instance main(String[] args) - JEP 445 + java.lang.reflect.Method instanceMainArgs = + findMethod(bootClass, "main", false, new Class[] {String[].class}); + if (instanceMainArgs != null) { + instanceMainArgs.setAccessible(true); + instanceMainArgs.invoke(newInstance(bootClass), (Object) arguments); + return; + } + + // Try instance main() - JEP 445 + java.lang.reflect.Method instanceMainNoArgs = findMethod(bootClass, "main", false, new Class[0]); + if (instanceMainNoArgs != null) { + instanceMainNoArgs.setAccessible(true); + instanceMainNoArgs.invoke(newInstance(bootClass)); + return; + } + + if (Runnable.class.isAssignableFrom(bootClass)) { + doRun(bootClass); + return; + } + + throw new NoSuchMethodException("No suitable main method found for " + bootClass + ", and not Runnable"); + } + + /** + * Finds a method with the given name, parameter types, and static modifier requirement. + * Searches the class hierarchy including inherited methods. + * Only matches methods with void return type (for JSR-512 compliance). + * + * @param clazz the class to search + * @param methodName the method name + * @param isStatic whether the method should be static + * @param parameterTypes the parameter types + * @return the matching method, or null if not found + */ + protected java.lang.reflect.Method findMethod( + Class clazz, String methodName, boolean isStatic, Class[] parameterTypes) { + Class currentClass = clazz; + while (currentClass != null) { + try { + java.lang.reflect.Method method = currentClass.getDeclaredMethod(methodName, parameterTypes); + boolean methodIsStatic = Modifier.isStatic(method.getModifiers()); + // Check if static modifier matches requirement and return type is void + if (methodIsStatic == isStatic && method.getReturnType() == void.class) { + return method; + } + } catch (NoSuchMethodException e) { + // Method not found in this class, continue to superclass + } + currentClass = currentClass.getSuperclass(); + } + return null; + } + + protected Object newInstance(final Class bootClass) + throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + final Constructor constructor = bootClass.getDeclaredConstructor(); + if ((constructor.getModifiers() & Modifier.PRIVATE) != 0) { + throw new NoSuchMethodException("No public constructor found for " + bootClass); + } + return constructor.newInstance(); + } + + protected void doRun(final Class bootClass) + throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + final Class runnableClass = bootClass.asSubclass(Runnable.class); + final Constructor constructor = Stream.of(runnableClass.getDeclaredConstructors()) + .map(i -> (Constructor) i) + .filter(i -> Modifier.isPublic(i.getModifiers())) + .max(Comparator., Integer>comparing(Constructor::getParameterCount)) + .orElseThrow(() -> new IllegalArgumentException("No public constructor found for " + bootClass)); + if (getLog().isDebugEnabled()) { + getLog().debug("Using constructor " + constructor); + } + + Runnable runnable; + try { // todo: enhance that but since injection API is being defined at mvn4 level it is + // good enough + final Object[] args = Stream.of(constructor.getParameters()) + .map(param -> { + try { + return lookupParam(param); + } catch (final ComponentLookupException e) { + getLog().error(e.getMessage(), e); + throw new IllegalStateException(e); + } + }) + .toArray(Object[]::new); + constructor.setAccessible(true); + runnable = constructor.newInstance(args); + } catch (final RuntimeException re) { + if (getLog().isDebugEnabled()) { + getLog().debug( + "Can't inject " + runnableClass + "': " + re.getMessage() + ", will ignore injections", + re); + } + final Constructor declaredConstructor = runnableClass.getDeclaredConstructor(); + declaredConstructor.setAccessible(true); + runnable = declaredConstructor.newInstance(); + } + runnable.run(); + } + + protected Object lookupParam(final java.lang.reflect.Parameter param) throws ComponentLookupException { + final String name = param.getName(); + switch (name) { + case "systemProperties": // Properties + return getSession().getSystemProperties(); + case "systemPropertiesUpdater": // BiConsumer + return propertiesUpdater(getSession().getSystemProperties()); + case "userProperties": // Properties + return getSession().getUserProperties(); + case "userPropertiesUpdater": // BiConsumer + return propertiesUpdater(getSession().getUserProperties()); + case "projectProperties": // Properties + return project.getProperties(); + case "projectPropertiesUpdater": // BiConsumer + return propertiesUpdater(project.getProperties()); + case "highestVersionResolver": // Function + return resolveVersion(VersionRangeResult::getHighestVersion); + case "session": // MavenSession + return getSession(); + case "container": // PlexusContainer + return container; + default: // Any + return lookup(param, name); + } + } + + protected Object lookup(final java.lang.reflect.Parameter param, final String name) + throws ComponentLookupException { + // try injecting a real instance but loose coupled - will use reflection + if (param.getType() == Object.class && name.contains("_")) { + final ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + try { + final int hintIdx = name.indexOf("__hint_"); + if (hintIdx > 0) { + final String hint = name.substring(hintIdx + "__hint_".length()); + final String typeName = name.substring(0, hintIdx).replace('_', '.'); + return container.lookup(loader.loadClass(typeName), hint); + } + + final String typeName = name.replace('_', '.'); + return container.lookup(loader.loadClass(typeName)); + } catch (final ClassNotFoundException cnfe) { + if (getLog().isDebugEnabled()) { + getLog().debug("Can't load param (" + name + "): " + cnfe.getMessage(), cnfe); + } + // let's try to lookup object, unlikely but not impossible + } + } + + // just lookup by type + return container.lookup(param.getType()); + } + + protected Function resolveVersion(final Function fn) { + return ga -> { + final int sep = ga.indexOf(':'); + if (sep < 0) { + throw new IllegalArgumentException("Invalid groupId:artifactId argument: '" + ga + "'"); + } + + final org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(ga + ":[0,)"); + final VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setArtifact(artifact); + try { + if (includePluginDependencies && includeProjectDependencies) { + rangeRequest.setRepositories(Stream.concat( + project.getRemoteProjectRepositories().stream(), + project.getRemotePluginRepositories().stream()) + .distinct() + .collect(toList())); + } else if (includePluginDependencies) { + rangeRequest.setRepositories(project.getRemotePluginRepositories()); + } else if (includeProjectDependencies) { + rangeRequest.setRepositories(project.getRemoteProjectRepositories()); + } + final VersionRangeResult rangeResult = + repositorySystem.resolveVersionRange(getSession().getRepositorySession(), rangeRequest); + return String.valueOf(fn.apply(rangeResult)); + } catch (final VersionRangeResolutionException e) { + throw new IllegalStateException(e); + } + }; + } + + protected BiConsumer propertiesUpdater(final Properties props) { + return (k, v) -> { + if (v == null) { + props.remove(k); + } else { + props.setProperty(k, v); + } + }; + } + + /** + * To avoid the exec:java to consider common pool threads leaked, let's pre-create them. + */ + protected void preloadCommonPool() { + try { + // ensure common pool exists in the jvm + final ExecutorService es = ForkJoinPool.commonPool(); + final int max = preloadCommonPool > 0 ? preloadCommonPool : ForkJoinPool.getCommonPoolParallelism(); + final CountDownLatch preLoad = new CountDownLatch(1); + for (int i = 0; i < max; i++) { + es.submit(() -> { + try { + preLoad.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + preLoad.countDown(); + } catch (final Exception e) { + getLog().debug(e.getMessage() + ", skipping commonpool earger init"); + } + } + + /** + * a ThreadGroup to isolate execution and collect exceptions. + */ + class IsolatedThreadGroup extends ThreadGroup { + private Throwable uncaughtException; // synchronize access to this + + public IsolatedThreadGroup(String name) { + super(name); + } + + public void uncaughtException(Thread thread, Throwable throwable) { + if (throwable instanceof ThreadDeath) { + return; // harmless + } + synchronized (this) { + if (uncaughtException == null) // only remember the first one + { + uncaughtException = throwable; // will be reported eventually + } + } + getLog().warn(throwable); + } + } + + protected void joinNonDaemonThreads(ThreadGroup threadGroup) { + boolean foundNonDaemon; + do { + foundNonDaemon = false; + Collection threads = getActiveThreads(threadGroup); + for (Thread thread : threads) { + if (thread.isDaemon()) { + continue; + } + foundNonDaemon = true; // try again; maybe more threads were created while we were busy + joinThread(thread, 0); + } + } while (foundNonDaemon); + } + + protected void joinThread(Thread thread, long timeoutMsecs) { + try { + getLog().debug("joining on thread " + thread); + thread.join(timeoutMsecs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // good practice if don't throw + getLog().warn("interrupted while joining against thread " + thread, e); // not expected! + } + if (thread.isAlive()) // generally abnormal + { + getLog().warn("thread " + thread + " was interrupted but is still alive after waiting at least " + + timeoutMsecs + "msecs"); + } + } + + protected void terminateThreads(ThreadGroup threadGroup) { + long startTime = System.currentTimeMillis(); + Set uncooperativeThreads = new HashSet<>(); // these were not responsive to interruption + for (Collection threads = getActiveThreads(threadGroup); + !threads.isEmpty(); + threads = getActiveThreads(threadGroup), threads.removeAll(uncooperativeThreads)) { + // Interrupt all threads we know about as of this instant (harmless if spuriously went dead (! isAlive()) + // or if something else interrupted it ( isInterrupted() ). + for (Thread thread : threads) { + getLog().debug("interrupting thread " + thread); + thread.interrupt(); + } + // Now join with a timeout and call stop() (assuming flags are set right) + boolean threadStopIsAvailable = true; + for (Thread thread : threads) { + if (!thread.isAlive()) { + continue; // and, presumably it won't show up in getActiveThreads() next iteration + } + if (daemonThreadJoinTimeout <= 0) { + joinThread(thread, 0); // waits until not alive; no timeout + continue; + } + long timeout = daemonThreadJoinTimeout - (System.currentTimeMillis() - startTime); + if (timeout > 0) { + joinThread(thread, timeout); + } + if (!thread.isAlive()) { + continue; + } + uncooperativeThreads.add(thread); // ensure we don't process again + if (stopUnresponsiveDaemonThreads && threadStopIsAvailable) { + getLog().warn("thread " + thread + " will be Thread.stop()'ed"); + try { + thread.stop(); + } catch (UnsupportedOperationException unsupportedOperationException) { + threadStopIsAvailable = false; + getLog().warn(THREAD_STOP_UNAVAILABLE); + } + } else { + getLog().warn("thread " + thread + " will linger despite being asked to die via interruption"); + } + } + } + if (!uncooperativeThreads.isEmpty()) { + getLog().warn("NOTE: " + uncooperativeThreads.size() + " thread(s) did not finish despite being asked to" + + " via interruption. This is not a problem with exec:java, it is a problem with the running code." + + " Although not serious, it should be remedied."); + } else { + int activeCount = threadGroup.activeCount(); + if (activeCount != 0) { + // TODO this may be nothing; continue on anyway; perhaps don't even log in future + Thread[] threadsArray = new Thread[1]; + threadGroup.enumerate(threadsArray); + getLog().debug("strange; " + activeCount + " thread(s) still active in the group " + threadGroup + + " such as " + threadsArray[0]); + } + } + } + + protected Collection getActiveThreads(ThreadGroup threadGroup) { + Thread[] threads = new Thread[threadGroup.activeCount()]; + int numThreads = threadGroup.enumerate(threads); + Collection result = new ArrayList<>(numThreads); + for (int i = 0; i < threads.length && threads[i] != null; i++) { + result.add(threads[i]); + } + return result; // note: result should be modifiable + } + + /** + * Pass any given system properties to the java system properties. + */ + protected void setSystemProperties() { + if (systemProperties == null) { + return; + } + // copy otherwise the restore phase does nothing + originalSystemProperties = new Properties(); + originalSystemProperties.putAll(System.getProperties()); + + if (Stream.of(systemProperties).anyMatch(it -> it instanceof ProjectProperties)) { + System.getProperties().putAll(project.getProperties()); + } + + for (AbstractProperty systemProperty : systemProperties) { + if (!(systemProperty instanceof Property)) { + continue; + } + + Property prop = (Property) systemProperty; + String value = prop.getValue(); + System.setProperty(prop.getKey(), value == null ? "" : value); + } + } + + /** + * Set up a classloader for the execution of the main class. + * + * @return the classloader + * @throws MojoExecutionException if a problem happens + */ + protected URLClassLoader getClassLoader() throws MojoExecutionException { + List classpathURLs = new ArrayList<>(); + this.addRelevantPluginDependenciesToClasspath(classpathURLs); + this.addRelevantProjectDependenciesToClasspath(classpathURLs); + this.addAdditionalClasspathElements(classpathURLs); + try { + return URLClassLoaderBuilder.builder() + .setLogger(getLog()) + .setPaths(classpathURLs) + .setExclusions(classpathFilenameExclusions) + .setForcedJvmPackages(forcedJvmPackages) + .setExcludedJvmPackages(excludedJvmPackages) + .withTransformers(blockSystemExit) + .build(); + } catch (NullPointerException | IOException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + protected void addAdditionalClasspathElements(List path) { + if (additionalClasspathElements != null) { + for (String classPathElement : additionalClasspathElements) { + Path file = Paths.get(classPathElement); + if (!file.isAbsolute()) { + file = project.getBasedir().toPath().resolve(file); + } + getLog().debug("Adding additional classpath element: " + file + " to classpath"); + path.add(file); + } + } + } + + /** + * Add any relevant project dependencies to the classpath. Indirectly takes includePluginDependencies and + * ExecutableDependency into consideration. + * + * @param path classpath of {@link java.net.URL} objects + * @throws MojoExecutionException if a problem happens + */ + protected void addRelevantPluginDependenciesToClasspath(List path) throws MojoExecutionException { + if (hasCommandlineArgs()) { + arguments = parseCommandlineArgs(); + } + + for (Artifact classPathElement : this.determineRelevantPluginDependencies()) { + getLog().debug("Adding plugin dependency artifact: " + classPathElement.getArtifactId() + " to classpath"); + path.add(classPathElement.getFile().toPath()); + } + } + + /** + * Add any relevant project dependencies to the classpath. Takes includeProjectDependencies into consideration. + * + * @param path classpath of {@link java.net.URL} objects + */ + protected void addRelevantProjectDependenciesToClasspath(List path) { + if (this.includeProjectDependencies) { + getLog().debug("Project Dependencies will be included."); + + List artifacts = new ArrayList<>(); + List theClasspathFiles = new ArrayList<>(); + + collectProjectArtifactsAndClasspath(artifacts, theClasspathFiles); + + for (Path classpathFile : theClasspathFiles) { + getLog().debug("Adding to classpath : " + classpathFile); + path.add(classpathFile); + } + + for (Artifact classPathElement : artifacts) { + getLog().debug("Adding project dependency artifact: " + classPathElement.getArtifactId() + + " to classpath"); + path.add(classPathElement.getFile().toPath()); + } + } else { + getLog().debug("Project Dependencies will be excluded."); + } + } + + /** + * Stop program execution for nn millis. + * + * @param millis the number of millis-seconds to wait for, 0 stops program forever. + */ + protected void waitFor(long millis) { + Object lock = new Object(); + synchronized (lock) { + try { + lock.wait(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // good practice if don't throw + getLog().warn("Spuriously interrupted while waiting for " + millis + "ms", e); + } + } + } +} diff --git a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java index 3a47d417..54835157 100644 --- a/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java +++ b/src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -2,823 +2,40 @@ import javax.inject.Inject; -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.net.URLClassLoader; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.stream.Stream; - -import org.apache.maven.artifact.Artifact; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.codehaus.plexus.PlexusContainer; -import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.eclipse.aether.RepositorySystem; -import org.eclipse.aether.artifact.DefaultArtifact; -import org.eclipse.aether.resolution.VersionRangeRequest; -import org.eclipse.aether.resolution.VersionRangeResolutionException; -import org.eclipse.aether.resolution.VersionRangeResult; - -import static java.util.stream.Collectors.toList; /** * Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath. + * This is the Java 8 base implementation. * * @author Kaare Nilsen (kaare.nilsen@gmail.com), David Smiley (dsmiley@mitre.org) * @since 1.0 */ @Mojo(name = "java", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST) -public class ExecJavaMojo extends AbstractExecMojo { - // Implementation note: Constants can be included in javadocs by {@value #MY_CONST} - private static final String THREAD_STOP_UNAVAILABLE = - "Thread.stop() is unavailable in this JRE version, cannot force-stop any threads"; - - /** - * The main class to execute.
- * With Java 9 and above you can prefix it with the modulename, e.g. com.greetings/com.greetings.Main - * Without modulename the classpath will be used, with modulename a new modulelayer will be created. - *

- * Note that you can also provide a {@link Runnable} fully qualified name. - * The runnable can get constructor injections either by type if you have maven in your classpath (can be provided) - * or by name (ensure to enable {@code -parameters} Java compiler option) for loose coupling. - * Current support loose injections are: - *

    - *
  • systemProperties: Properties, session system properties
  • - *
  • systemPropertiesUpdater: BiConsumer<String, String>, session system properties update callback (pass the key/value to update, null value means removal of the key)
  • - *
  • userProperties: Properties, session user properties
  • - *
  • userPropertiesUpdater: BiConsumer<String, String>, session user properties update callback (pass the key/value to update, null value means removal of the key)
  • - *
  • projectProperties: Properties, project properties
  • - *
  • projectPropertiesUpdater: BiConsumer<String, String>, project properties update callback (pass the key/value to update, null value means removal of the key)
  • - *
  • highestVersionResolver: Function<String, String>, passing a groupId:artifactId you get the latest resolved version from the project repositories
  • - *
- * - * @since 1.0 - */ - @Parameter(required = true, property = "exec.mainClass") - private String mainClass; - - /** - * Forces the creation of fork join common pool to avoids the threads to be owned by the isolated thread group - * and prevent a proper shutdown. - * If set to zero the default parallelism is used to precreate all threads, - * if negative it is ignored else the value is the one used to create the fork join threads. - * - * @since 3.0.1 - */ - @Parameter(property = "exec.preloadCommonPool", defaultValue = "0") - private int preloadCommonPool; - - /** - * The class arguments. - * - * @since 1.0 - */ - @Parameter(property = "exec.arguments") - private String[] arguments; - - /** - * A list of system properties to be passed. Note: as the execution is not forked, some system properties required - * by the JVM cannot be passed here. Use MAVEN_OPTS or the exec:exec instead. See the user guide for more - * information. - * - * @since 1.0 - */ - @Parameter - private AbstractProperty[] systemProperties; - - /** - * Indicates if mojo should be kept running after the mainclass terminates. Use full for server like apps with - * daemon threads. - * - * @deprecated since 1.1-alpha-1 - * @since 1.0 - */ - @Parameter(property = "exec.keepAlive", defaultValue = "false") - @Deprecated - private boolean keepAlive; - - /** - * Indicates if the project dependencies should be used when executing the main class. - * - * @since 1.1-beta-1 - */ - @Parameter(property = "exec.includeProjectDependencies", defaultValue = "true") - private boolean includeProjectDependencies; - - /** - * Whether to interrupt/join and possibly stop the daemon threads upon quitting.
- * If this is false, maven does nothing about the daemon threads. When maven has no more work to do, - * the VM will normally terminate any remaining daemon threads. - *

- * In certain cases (in particular if maven is embedded), you might need to keep this enabled to make sure threads - * are properly cleaned up to ensure they don't interfere with subsequent activity. In that case, see - * {@link #daemonThreadJoinTimeout} and {@link #stopUnresponsiveDaemonThreads} for further tuning. - *

- * - * @since 1.1-beta-1 - */ - @Parameter(property = "exec.cleanupDaemonThreads", defaultValue = "true") - private boolean cleanupDaemonThreads; - - /** - * This defines the number of milliseconds to wait for daemon threads to quit following their interruption.
- * This is only taken into account if {@link #cleanupDaemonThreads} is true. A value <=0 means to - * not timeout (i.e. wait indefinitely for threads to finish). Following a timeout, a warning will be logged. - *

- * Note: properly coded threads should terminate upon interruption but some threads may prove problematic: as - * the VM does interrupt daemon threads, some code may not have been written to handle interruption properly. For - * example java.util.Timer is known to not handle interruptions in JDK <= 1.6. So it is not possible for us to - * infinitely wait by default otherwise maven could hang. A sensible default value has been chosen, but this default - * value may change in the future based on user feedback. - *

- * - * @since 1.1-beta-1 - */ - @Parameter(property = "exec.daemonThreadJoinTimeout", defaultValue = "15000") - private long daemonThreadJoinTimeout; - - /** - * Wether to call {@link Thread#stop()} following a timing out of waiting for an interrupted thread to finish. This - * is only taken into account if {@link #cleanupDaemonThreads} is true and the - * {@link #daemonThreadJoinTimeout} threshold has been reached for an uncooperative thread. If this is - * false, or if {@link Thread#stop()} fails to get the thread to stop, then a warning is logged and - * Maven will continue on while the affected threads (and related objects in memory) linger on. Consider setting - * this to true if you are invoking problematic code that you can't fix. An example is - * {@link java.util.Timer} which doesn't respond to interruption. To have Timer fixed, vote for - * this bug. - *

- * Note: In JDK 20+, the long deprecated {@link Thread#stop()} (since JDK 1.2) has been removed and will - * throw an {@link UnsupportedOperationException}. This will be handled gracefully, yielding a log warning - * {@value #THREAD_STOP_UNAVAILABLE} once and not trying to stop any further threads during the same execution. - * - * @since 1.1-beta-1 - */ - @Parameter(property = "exec.stopUnresponsiveDaemonThreads", defaultValue = "false") - private boolean stopUnresponsiveDaemonThreads; - - private Properties originalSystemProperties; - - /** - * Additional elements to be appended to the classpath. - * - * @since 1.3 - */ - @Parameter - private List additionalClasspathElements; - - /** - * List of file to exclude from the classpath. - * It matches the jar name, for example {@code slf4j-simple-1.7.30.jar}. - * - * @since 3.0.1 - */ - @Parameter - private List classpathFilenameExclusions; - - /** - * Additional packages to load from the jvm even if a classpath dependency matches. - * - * @since 3.5.0 - */ - @Parameter - private List forcedJvmPackages; - - /** - * Additional packages to NOT load from the jvm even if it is in a flat classpath. - * Can enable to reproduce a webapp behavior for example where library is loaded over the JVM. - * - * @since 3.5.0 - */ - @Parameter - private List excludedJvmPackages; - - /** - * Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build) - * by calling {@link System#exit(int)}. When active, loaded classes will replace this call by a custom callback. - * In case of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. - * Otherwise, it will throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself - * had exited with an exception. - * This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users - * had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no - * longer necessary. - * - * @since 3.2.0 - */ - @Parameter(property = "exec.blockSystemExit", defaultValue = "false") - private boolean blockSystemExit; - - // todo: for maven4 move to Lookup instead - private final PlexusContainer container; +public class ExecJavaMojo extends AbstractExecJavaBase { @Inject protected ExecJavaMojo(RepositorySystem repositorySystem, PlexusContainer container) { - super(repositorySystem); - this.container = Objects.requireNonNull(container); - } - - /** - * Execute goal. - * - * @throws MojoExecutionException execution of the main class or one of the threads it generated failed. - * @throws MojoFailureException something bad happened... - */ - public void execute() throws MojoExecutionException, MojoFailureException { - if (isSkip()) { - getLog().info("skipping execute as per configuration"); - return; - } - - if (null == arguments) { - arguments = new String[0]; - } - - if (getLog().isDebugEnabled()) { - StringBuffer msg = new StringBuffer("Invoking : "); - msg.append(mainClass); - msg.append(".main("); - for (int i = 0; i < arguments.length; i++) { - if (i > 0) { - msg.append(", "); - } - msg.append(arguments[i]); - } - msg.append(")"); - getLog().debug(msg); - } - - if (preloadCommonPool >= 0) { - preloadCommonPool(); - } - - IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(mainClass /* name */); - Thread bootstrapThread = new Thread( // TODO: drop this useless thread 99% of the time - threadGroup, - () -> { - int sepIndex = mainClass.indexOf('/'); - - final String bootClassName; - if (sepIndex >= 0) { - bootClassName = mainClass.substring(sepIndex + 1); - } else { - bootClassName = mainClass; - } - - try { - doExec(bootClassName); - } catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on - Thread.currentThread() - .getThreadGroup() - .uncaughtException( - Thread.currentThread(), - new Exception( - "The specified mainClass doesn't contain a main method with appropriate signature.", - e)); - } catch (InvocationTargetException e) { - // use the cause if available to improve the plugin execution output - Throwable exceptionToReport = e.getCause() != null ? e.getCause() : e; - Thread.currentThread() - .getThreadGroup() - .uncaughtException(Thread.currentThread(), exceptionToReport); - } catch (SystemExitException systemExitException) { - if (systemExitException.getExitCode() != 0) { - getLog().error(systemExitException.getMessage()); - throw systemExitException; - } else { - getLog().info(systemExitException.getMessage()); - } - } catch (Throwable e) { // just pass it on - Thread.currentThread().getThreadGroup().uncaughtException(Thread.currentThread(), e); - } - }, - mainClass + ".main()"); - URLClassLoader classLoader = getClassLoader(); // TODO: enable to cache accross executions - bootstrapThread.setContextClassLoader(classLoader); - setSystemProperties(); - - bootstrapThread.start(); - joinNonDaemonThreads(threadGroup); - // It's plausible that spontaneously a non-daemon thread might be created as we try and shut down, - // but it's too late since the termination condition (only daemon threads) has been triggered. - if (keepAlive) { - getLog().warn( - "Warning: keepAlive is now deprecated and obsolete. Do you need it? Please comment on MEXEC-6."); - waitFor(0); - } - - if (cleanupDaemonThreads) { - - terminateThreads(threadGroup); - - try { - threadGroup.destroy(); - } catch (RuntimeException | Error /* missing method in future java version */ e) { - getLog().warn("Couldn't destroy threadgroup " + threadGroup, e); - } - } - - if (classLoader != null) { - try { - classLoader.close(); - } catch (IOException e) { - getLog().error(e.getMessage(), e); - } - } - - if (originalSystemProperties != null) { - System.setProperties(originalSystemProperties); - } - - synchronized (threadGroup) { - if (threadGroup.uncaughtException != null) { - throw new MojoExecutionException( - "An exception occurred while executing the Java class. " - + threadGroup.uncaughtException.getMessage(), - threadGroup.uncaughtException); - } - } - - registerSourceRoots(); - } - - private void doExec(final String bootClassName) throws Throwable { - Class bootClass = Thread.currentThread().getContextClassLoader().loadClass(bootClassName); - MethodHandles.Lookup lookup = MethodHandles.lookup(); - try { - MethodHandle mainHandle = - lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class)); - mainHandle.invoke(arguments); - return; - } catch (final NoSuchMethodException | IllegalAccessException e) { - // No static main(String[]) - } - try { - MethodHandle mainHandle = - lookup.findVirtual(bootClass, "main", MethodType.methodType(void.class, String[].class)); - mainHandle.invoke(newInstance(bootClass), arguments); - return; - } catch (final NoSuchMethodException | IllegalAccessException e) { - // No instance main(String[]) - } - try { - MethodHandle mainHandle = lookup.findStatic(bootClass, "main", MethodType.methodType(void.class)); - mainHandle.invoke(); - return; - } catch (final NoSuchMethodException | IllegalAccessException e) { - // No static main() - } - try { - MethodHandle mainHandle = lookup.findVirtual(bootClass, "main", MethodType.methodType(void.class)); - mainHandle.invoke(newInstance(bootClass)); - return; - } catch (final NoSuchMethodException | IllegalAccessException e) { - // No instance main() - } - - if (Runnable.class.isAssignableFrom(bootClass)) { - doRun(bootClass); - return; - } - - throw new NoSuchMethodException("No suitable main method found for " + bootClass + ", and not Runnable"); - } - - private Object newInstance(final Class bootClass) - throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { - final Constructor constructor = bootClass.getDeclaredConstructor(); - if ((constructor.getModifiers() & Modifier.PRIVATE) != 0) { - throw new NoSuchMethodException("No public constructor found for " + bootClass); - } - return constructor.newInstance(); - } - - private void doRun(final Class bootClass) - throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { - final Class runnableClass = bootClass.asSubclass(Runnable.class); - final Constructor constructor = Stream.of(runnableClass.getDeclaredConstructors()) - .map(i -> (Constructor) i) - .filter(i -> Modifier.isPublic(i.getModifiers())) - .max(Comparator., Integer>comparing(Constructor::getParameterCount)) - .orElseThrow(() -> new IllegalArgumentException("No public constructor found for " + bootClass)); - if (getLog().isDebugEnabled()) { - getLog().debug("Using constructor " + constructor); - } - - Runnable runnable; - try { // todo: enhance that but since injection API is being defined at mvn4 level it is - // good enough - final Object[] args = Stream.of(constructor.getParameters()) - .map(param -> { - try { - return lookupParam(param); - } catch (final ComponentLookupException e) { - getLog().error(e.getMessage(), e); - throw new IllegalStateException(e); - } - }) - .toArray(Object[]::new); - constructor.setAccessible(true); - runnable = constructor.newInstance(args); - } catch (final RuntimeException re) { - if (getLog().isDebugEnabled()) { - getLog().debug( - "Can't inject " + runnableClass + "': " + re.getMessage() + ", will ignore injections", - re); - } - final Constructor declaredConstructor = runnableClass.getDeclaredConstructor(); - declaredConstructor.setAccessible(true); - runnable = declaredConstructor.newInstance(); - } - runnable.run(); - } - - private Object lookupParam(final java.lang.reflect.Parameter param) throws ComponentLookupException { - final String name = param.getName(); - switch (name) { - case "systemProperties": // Properties - return getSession().getSystemProperties(); - case "systemPropertiesUpdater": // BiConsumer - return propertiesUpdater(getSession().getSystemProperties()); - case "userProperties": // Properties - return getSession().getUserProperties(); - case "userPropertiesUpdater": // BiConsumer - return propertiesUpdater(getSession().getUserProperties()); - case "projectProperties": // Properties - return project.getProperties(); - case "projectPropertiesUpdater": // BiConsumer - return propertiesUpdater(project.getProperties()); - case "highestVersionResolver": // Function - return resolveVersion(VersionRangeResult::getHighestVersion); - case "session": // MavenSession - return getSession(); - case "container": // PlexusContainer - return container; - default: // Any - return lookup(param, name); - } - } - - private Object lookup(final java.lang.reflect.Parameter param, final String name) throws ComponentLookupException { - // try injecting a real instance but loose coupled - will use reflection - if (param.getType() == Object.class && name.contains("_")) { - final ClassLoader loader = Thread.currentThread().getContextClassLoader(); - - try { - final int hintIdx = name.indexOf("__hint_"); - if (hintIdx > 0) { - final String hint = name.substring(hintIdx + "__hint_".length()); - final String typeName = name.substring(0, hintIdx).replace('_', '.'); - return container.lookup(loader.loadClass(typeName), hint); - } - - final String typeName = name.replace('_', '.'); - return container.lookup(loader.loadClass(typeName)); - } catch (final ClassNotFoundException cnfe) { - if (getLog().isDebugEnabled()) { - getLog().debug("Can't load param (" + name + "): " + cnfe.getMessage(), cnfe); - } - // let's try to lookup object, unlikely but not impossible - } - } - - // just lookup by type - return container.lookup(param.getType()); - } - - private Function resolveVersion(final Function fn) { - return ga -> { - final int sep = ga.indexOf(':'); - if (sep < 0) { - throw new IllegalArgumentException("Invalid groupId:artifactId argument: '" + ga + "'"); - } - - final org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(ga + ":[0,)"); - final VersionRangeRequest rangeRequest = new VersionRangeRequest(); - rangeRequest.setArtifact(artifact); - try { - if (includePluginDependencies && includeProjectDependencies) { - rangeRequest.setRepositories(Stream.concat( - project.getRemoteProjectRepositories().stream(), - project.getRemotePluginRepositories().stream()) - .distinct() - .collect(toList())); - } else if (includePluginDependencies) { - rangeRequest.setRepositories(project.getRemotePluginRepositories()); - } else if (includeProjectDependencies) { - rangeRequest.setRepositories(project.getRemoteProjectRepositories()); - } - final VersionRangeResult rangeResult = - repositorySystem.resolveVersionRange(getSession().getRepositorySession(), rangeRequest); - return String.valueOf(fn.apply(rangeResult)); - } catch (final VersionRangeResolutionException e) { - throw new IllegalStateException(e); - } - }; - } - - private BiConsumer propertiesUpdater(final Properties props) { - return (k, v) -> { - if (v == null) { - props.remove(k); - } else { - props.setProperty(k, v); - } - }; - } - - /** - * To avoid the exec:java to consider common pool threads leaked, let's pre-create them. - */ - private void preloadCommonPool() { - try { - // ensure common pool exists in the jvm - final ExecutorService es = ForkJoinPool.commonPool(); - final int max = preloadCommonPool > 0 ? preloadCommonPool : ForkJoinPool.getCommonPoolParallelism(); - final CountDownLatch preLoad = new CountDownLatch(1); - for (int i = 0; i < max; i++) { - es.submit(() -> { - try { - preLoad.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } - preLoad.countDown(); - } catch (final Exception e) { - getLog().debug(e.getMessage() + ", skipping commonpool earger init"); - } - } - - /** - * a ThreadGroup to isolate execution and collect exceptions. - */ - class IsolatedThreadGroup extends ThreadGroup { - private Throwable uncaughtException; // synchronize access to this - - public IsolatedThreadGroup(String name) { - super(name); - } - - public void uncaughtException(Thread thread, Throwable throwable) { - if (throwable instanceof ThreadDeath) { - return; // harmless - } - synchronized (this) { - if (uncaughtException == null) // only remember the first one - { - uncaughtException = throwable; // will be reported eventually - } - } - getLog().warn(throwable); - } - } - - private void joinNonDaemonThreads(ThreadGroup threadGroup) { - boolean foundNonDaemon; - do { - foundNonDaemon = false; - Collection threads = getActiveThreads(threadGroup); - for (Thread thread : threads) { - if (thread.isDaemon()) { - continue; - } - foundNonDaemon = true; // try again; maybe more threads were created while we were busy - joinThread(thread, 0); - } - } while (foundNonDaemon); - } - - private void joinThread(Thread thread, long timeoutMsecs) { - try { - getLog().debug("joining on thread " + thread); - thread.join(timeoutMsecs); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); // good practice if don't throw - getLog().warn("interrupted while joining against thread " + thread, e); // not expected! - } - if (thread.isAlive()) // generally abnormal - { - getLog().warn("thread " + thread + " was interrupted but is still alive after waiting at least " - + timeoutMsecs + "msecs"); - } - } - - private void terminateThreads(ThreadGroup threadGroup) { - long startTime = System.currentTimeMillis(); - Set uncooperativeThreads = new HashSet<>(); // these were not responsive to interruption - for (Collection threads = getActiveThreads(threadGroup); - !threads.isEmpty(); - threads = getActiveThreads(threadGroup), threads.removeAll(uncooperativeThreads)) { - // Interrupt all threads we know about as of this instant (harmless if spuriously went dead (! isAlive()) - // or if something else interrupted it ( isInterrupted() ). - for (Thread thread : threads) { - getLog().debug("interrupting thread " + thread); - thread.interrupt(); - } - // Now join with a timeout and call stop() (assuming flags are set right) - boolean threadStopIsAvailable = true; - for (Thread thread : threads) { - if (!thread.isAlive()) { - continue; // and, presumably it won't show up in getActiveThreads() next iteration - } - if (daemonThreadJoinTimeout <= 0) { - joinThread(thread, 0); // waits until not alive; no timeout - continue; - } - long timeout = daemonThreadJoinTimeout - (System.currentTimeMillis() - startTime); - if (timeout > 0) { - joinThread(thread, timeout); - } - if (!thread.isAlive()) { - continue; - } - uncooperativeThreads.add(thread); // ensure we don't process again - if (stopUnresponsiveDaemonThreads && threadStopIsAvailable) { - getLog().warn("thread " + thread + " will be Thread.stop()'ed"); - try { - thread.stop(); - } catch (UnsupportedOperationException unsupportedOperationException) { - threadStopIsAvailable = false; - getLog().warn(THREAD_STOP_UNAVAILABLE); - } - } else { - getLog().warn("thread " + thread + " will linger despite being asked to die via interruption"); - } - } - } - if (!uncooperativeThreads.isEmpty()) { - getLog().warn("NOTE: " + uncooperativeThreads.size() + " thread(s) did not finish despite being asked to" - + " via interruption. This is not a problem with exec:java, it is a problem with the running code." - + " Although not serious, it should be remedied."); - } else { - int activeCount = threadGroup.activeCount(); - if (activeCount != 0) { - // TODO this may be nothing; continue on anyway; perhaps don't even log in future - Thread[] threadsArray = new Thread[1]; - threadGroup.enumerate(threadsArray); - getLog().debug("strange; " + activeCount + " thread(s) still active in the group " + threadGroup - + " such as " + threadsArray[0]); - } - } - } - - private Collection getActiveThreads(ThreadGroup threadGroup) { - Thread[] threads = new Thread[threadGroup.activeCount()]; - int numThreads = threadGroup.enumerate(threads); - Collection result = new ArrayList<>(numThreads); - for (int i = 0; i < threads.length && threads[i] != null; i++) { - result.add(threads[i]); - } - return result; // note: result should be modifiable - } - - /** - * Pass any given system properties to the java system properties. - */ - private void setSystemProperties() { - if (systemProperties == null) { - return; - } - // copy otherwise the restore phase does nothing - originalSystemProperties = new Properties(); - originalSystemProperties.putAll(System.getProperties()); - - if (Stream.of(systemProperties).anyMatch(it -> it instanceof ProjectProperties)) { - System.getProperties().putAll(project.getProperties()); - } - - for (AbstractProperty systemProperty : systemProperties) { - if (!(systemProperty instanceof Property)) { - continue; - } - - Property prop = (Property) systemProperty; - String value = prop.getValue(); - System.setProperty(prop.getKey(), value == null ? "" : value); - } - } - - /** - * Set up a classloader for the execution of the main class. - * - * @return the classloader - * @throws MojoExecutionException if a problem happens - */ - private URLClassLoader getClassLoader() throws MojoExecutionException { - List classpathURLs = new ArrayList<>(); - this.addRelevantPluginDependenciesToClasspath(classpathURLs); - this.addRelevantProjectDependenciesToClasspath(classpathURLs); - this.addAdditionalClasspathElements(classpathURLs); - try { - return URLClassLoaderBuilder.builder() - .setLogger(getLog()) - .setPaths(classpathURLs) - .setExclusions(classpathFilenameExclusions) - .setForcedJvmPackages(forcedJvmPackages) - .setExcludedJvmPackages(excludedJvmPackages) - .withTransformers(blockSystemExit) - .build(); - } catch (NullPointerException | IOException e) { - throw new MojoExecutionException(e.getMessage(), e); - } - } - - private void addAdditionalClasspathElements(List path) { - if (additionalClasspathElements != null) { - for (String classPathElement : additionalClasspathElements) { - Path file = Paths.get(classPathElement); - if (!file.isAbsolute()) { - file = project.getBasedir().toPath().resolve(file); - } - getLog().debug("Adding additional classpath element: " + file + " to classpath"); - path.add(file); - } - } - } - - /** - * Add any relevant project dependencies to the classpath. Indirectly takes includePluginDependencies and - * ExecutableDependency into consideration. - * - * @param path classpath of {@link java.net.URL} objects - * @throws MojoExecutionException if a problem happens - */ - private void addRelevantPluginDependenciesToClasspath(List path) throws MojoExecutionException { - if (hasCommandlineArgs()) { - arguments = parseCommandlineArgs(); - } - - for (Artifact classPathElement : this.determineRelevantPluginDependencies()) { - getLog().debug("Adding plugin dependency artifact: " + classPathElement.getArtifactId() + " to classpath"); - path.add(classPathElement.getFile().toPath()); - } - } - - /** - * Add any relevant project dependencies to the classpath. Takes includeProjectDependencies into consideration. - * - * @param path classpath of {@link java.net.URL} objects - */ - private void addRelevantProjectDependenciesToClasspath(List path) { - if (this.includeProjectDependencies) { - getLog().debug("Project Dependencies will be included."); - - List artifacts = new ArrayList<>(); - List theClasspathFiles = new ArrayList<>(); - - collectProjectArtifactsAndClasspath(artifacts, theClasspathFiles); - - for (Path classpathFile : theClasspathFiles) { - getLog().debug("Adding to classpath : " + classpathFile); - path.add(classpathFile); - } - - for (Artifact classPathElement : artifacts) { - getLog().debug("Adding project dependency artifact: " + classPathElement.getArtifactId() - + " to classpath"); - path.add(classPathElement.getFile().toPath()); - } - } else { - getLog().debug("Project Dependencies will be excluded."); - } + super(repositorySystem, container); } /** - * Stop program execution for nn millis. + * Execute using module path (Java 9+ JPMS). + * This base implementation (Java 8) does not support JPMS and will throw an error. + * The Java 9+ version overrides this method with full JPMS support. * - * @param millis the number of millis-seconds to wait for, 0 stops program forever. + * @param moduleName the module name + * @param bootClassName the fully qualified class name + * @throws Throwable if execution fails */ - private void waitFor(long millis) { - Object lock = new Object(); - synchronized (lock) { - try { - lock.wait(millis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); // good practice if don't throw - getLog().warn("Spuriously interrupted while waiting for " + millis + "ms", e); - } - } + @Override + protected void doExecModulePath(final String moduleName, final String bootClassName) throws Throwable { + throw new UnsupportedOperationException( + "Module path execution (moduleName/className syntax) requires Java 9 or later. " + + "Current Java version: " + System.getProperty("java.version") + ". " + + "Either upgrade to Java 9+ or use the classpath-only syntax (just the className without module prefix)."); } } diff --git a/src/main/java9/org/codehaus/mojo/exec/ExecJavaMojo.java b/src/main/java9/org/codehaus/mojo/exec/ExecJavaMojo.java new file mode 100644 index 00000000..1e4b0811 --- /dev/null +++ b/src/main/java9/org/codehaus/mojo/exec/ExecJavaMojo.java @@ -0,0 +1,144 @@ +package org.codehaus.mojo.exec; + +import javax.inject.Inject; + +import java.lang.module.Configuration; +import java.lang.module.ModuleFinder; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.codehaus.plexus.PlexusContainer; +import org.eclipse.aether.RepositorySystem; + +/** + * Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath. + * This is the Java 9+ implementation with full JPMS support. + * + * @author Kaare Nilsen (kaare.nilsen@gmail.com), David Smiley (dsmiley@mitre.org) + * @since 1.0 + */ +@Mojo(name = "java", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST) +public class ExecJavaMojo extends AbstractExecJavaBase { + + @Inject + protected ExecJavaMojo(RepositorySystem repositorySystem, PlexusContainer container) { + super(repositorySystem, container); + } + + /** + * Execute using module path (Java 9+ JPMS). + * This Java 9+ implementation supports JPMS with full ModuleLayer and ServiceLoader support. + * Uses Configuration.resolveAndBind() to ensure service providers are included. + * + * @param moduleName the module name + * @param bootClassName the fully qualified class name + * @throws Throwable if execution fails + */ + @Override + protected void doExecModulePath(final String moduleName, final String bootClassName) throws Throwable { + // Build module path from the same elements as the classpath + List modulePaths = new ArrayList<>(); + this.addRelevantPluginDependenciesToClasspath(modulePaths); + this.addRelevantProjectDependenciesToClasspath(modulePaths); + this.addAdditionalClasspathElements(modulePaths); + + getLog().debug("Module paths for JPMS execution: " + modulePaths); + + ModuleFinder finder = ModuleFinder.of(modulePaths.toArray(new Path[0])); + ModuleLayer parent = ModuleLayer.boot(); + + // Use resolveAndBind to include service providers (for ServiceLoader support) + Configuration cf = parent.configuration().resolveAndBind(finder, ModuleFinder.of(), Set.of(moduleName)); + + getLog().debug("Resolved modules: " + cf.modules()); + + // Create a new module layer with a single class loader and get the controller + ModuleLayer.Controller controller = + ModuleLayer.defineModulesWithOneLoader(cf, List.of(parent), ClassLoader.getSystemClassLoader()); + ModuleLayer layer = controller.layer(); + + // Load the main class from the module layer's class loader + ClassLoader moduleLoader = layer.findLoader(moduleName); + Class bootClass = moduleLoader.loadClass(bootClassName); + + // Open the package containing the main class to allow reflective access + Module bootModule = bootClass.getModule(); + String packageName = bootClass.getPackageName(); + if (bootModule != null && bootModule.isNamed()) { + // Use the controller to add opens - this allows us to access non-exported packages + controller.addOpens(bootModule, packageName, getClass().getModule()); + getLog().debug("Opened package " + packageName + " in module " + bootModule.getName() + " for reflection"); + } + + // Set the context class loader to the module's class loader + Thread currentThread = Thread.currentThread(); + ClassLoader oldContextClassLoader = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(moduleLoader); + executeMainMethodForModule(bootClass); + } finally { + currentThread.setContextClassLoader(oldContextClassLoader); + } + } + + /** + * Execute the main method for a module-loaded class. + * The package has already been opened by the controller in doExecModulePath. + * + * @param bootClass the class containing the main method + * @throws Throwable if execution fails + */ + private void executeMainMethodForModule(Class bootClass) throws Throwable { + try { + java.lang.reflect.Method mainMethod = bootClass.getDeclaredMethod("main", String[].class); + if (!Modifier.isPublic(mainMethod.getModifiers()) || !Modifier.isStatic(mainMethod.getModifiers())) { + throw new NoSuchMethodException("main method must be public and static in " + bootClass); + } + mainMethod.setAccessible(true); + mainMethod.invoke(null, (Object) arguments); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodException("No static void main(String[] args) method found in " + bootClass); + } + } + + /** + * Execute the main method with Java 9+ module opening support. + * This overrides the base implementation to add Module.implAddOpens logic. + * + * @param bootClass the class containing the main method + * @throws Throwable if execution fails + */ + @Override + protected void executeMainMethod(Class bootClass) throws Throwable { + // Try to find and invoke main method using reflection + // For JPMS modules, we need to open the package to allow reflective access + + // Open the package if it's in a module + Module bootModule = bootClass.getModule(); + if (bootModule != null && bootModule.isNamed()) { + String packageName = bootClass.getPackageName(); + Module unnamedModule = getClass().getModule(); // This is the unnamed module + if (!bootModule.isOpen(packageName, unnamedModule)) { + try { + // Use Module.implAddOpens to open the package + java.lang.reflect.Method implAddOpensMethod = + Module.class.getDeclaredMethod("implAddOpens", String.class); + implAddOpensMethod.setAccessible(true); + implAddOpensMethod.invoke(bootModule, packageName); + getLog().debug("Opened package " + packageName + " in module " + bootModule.getName()); + } catch (Exception e) { + getLog().warn("Could not open package " + packageName + " for reflection: " + e.getMessage()); + } + } + } + + // Delegate to base class for the actual main method invocation + super.executeMainMethod(bootClass); + } +} From 712e21d1dfba1454d02c902f5ffc728578d7b045 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Sun, 19 Oct 2025 17:35:21 +1000 Subject: [PATCH 4/5] [maven-release-plugin] prepare release 3.1.2 --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 20569b2e..b8285eff 100644 --- a/pom.xml +++ b/pom.xml @@ -127,7 +127,7 @@ scm:git:https://github.com/mojohaus/exec-maven-plugin.git scm:git:ssh://git@github.com/mojohaus/exec-maven-plugin.git - HEAD + 3.1.2 https://github.com/mojohaus/exec-maven-plugin/tree/master @@ -139,7 +139,6 @@ 1C 2025-10-05T19:10:54Z 3.2.0 - 8 From 416fdf1d1277bad7a16250305d42d35fb929ba0c Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Sun, 19 Oct 2025 17:40:20 +1000 Subject: [PATCH 5/5] [maven-release-plugin] prepare release 3.6.2 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b8285eff..a4e2ac13 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ exec-maven-plugin - 3.6.2-SNAPSHOT + 3.6.2 maven-plugin Exec Maven Plugin @@ -127,7 +127,7 @@ scm:git:https://github.com/mojohaus/exec-maven-plugin.git scm:git:ssh://git@github.com/mojohaus/exec-maven-plugin.git - 3.1.2 + 3.6.2 https://github.com/mojohaus/exec-maven-plugin/tree/master @@ -137,7 +137,7 @@ 1.7.36 9.9 1C - 2025-10-05T19:10:54Z + 2025-10-19T07:39:48Z 3.2.0 8