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 extends Runnable> runnableClass = bootClass.asSubclass(Runnable.class);
+ final Constructor extends Runnable> constructor = Stream.of(runnableClass.getDeclaredConstructors())
+ .map(i -> (Constructor extends Runnable>) 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 extends Runnable> 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 extends Runnable> runnableClass = bootClass.asSubclass(Runnable.class);
- final Constructor extends Runnable> constructor = Stream.of(runnableClass.getDeclaredConstructors())
- .map(i -> (Constructor extends Runnable>) 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 extends Runnable> 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);
+ }
+}