Skip to content

Commit a7090d0

Browse files
rmannibucauslawekjaranowski
authored andcommitted
Fixes #408, enable to exec:java runnables and not only mains with loosely coupled injections
1 parent f9e0c69 commit a7090d0

File tree

7 files changed

+422
-29
lines changed

7 files changed

+422
-29
lines changed

pom.xml

+17
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,23 @@
258258
</pluginManagement>
259259

260260
<plugins>
261+
<plugin>
262+
<groupId>org.apache.maven.plugins</groupId>
263+
<artifactId>maven-compiler-plugin</artifactId>
264+
<executions>
265+
<execution>
266+
<id>default-testCompile</id>
267+
<goals>
268+
<goal>testCompile</goal>
269+
</goals>
270+
<phase>test-compile</phase>
271+
<configuration>
272+
<parameters>true</parameters>
273+
</configuration>
274+
</execution>
275+
</executions>
276+
</plugin>
277+
261278
<plugin>
262279
<groupId>org.codehaus.mojo</groupId>
263280
<artifactId>animal-sniffer-maven-plugin</artifactId>

src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java

+193-16
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@
44
import java.lang.invoke.MethodHandle;
55
import java.lang.invoke.MethodHandles;
66
import java.lang.invoke.MethodType;
7+
import java.lang.reflect.Constructor;
78
import java.lang.reflect.InvocationTargetException;
9+
import java.lang.reflect.Modifier;
810
import java.net.URLClassLoader;
911
import java.nio.file.Path;
1012
import java.nio.file.Paths;
1113
import java.util.ArrayList;
1214
import java.util.Collection;
1315
import java.util.Collections;
16+
import java.util.Comparator;
1417
import java.util.HashSet;
1518
import java.util.List;
1619
import java.util.Properties;
1720
import java.util.Set;
1821
import java.util.concurrent.CountDownLatch;
1922
import java.util.concurrent.ExecutorService;
2023
import java.util.concurrent.ForkJoinPool;
24+
import java.util.function.BiConsumer;
25+
import java.util.function.Function;
2126
import java.util.stream.Collectors;
2227
import java.util.stream.Stream;
2328

@@ -29,16 +34,24 @@
2934
import org.apache.maven.plugins.annotations.Mojo;
3035
import org.apache.maven.plugins.annotations.Parameter;
3136
import org.apache.maven.plugins.annotations.ResolutionScope;
37+
import org.codehaus.plexus.PlexusContainer;
38+
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
3239
import org.eclipse.aether.RepositorySystem;
40+
import org.eclipse.aether.artifact.DefaultArtifact;
3341
import org.eclipse.aether.collection.CollectRequest;
3442
import org.eclipse.aether.graph.Dependency;
3543
import org.eclipse.aether.graph.DependencyFilter;
3644
import org.eclipse.aether.resolution.ArtifactResult;
3745
import org.eclipse.aether.resolution.DependencyRequest;
3846
import org.eclipse.aether.resolution.DependencyResolutionException;
3947
import org.eclipse.aether.resolution.DependencyResult;
48+
import org.eclipse.aether.resolution.VersionRangeRequest;
49+
import org.eclipse.aether.resolution.VersionRangeResolutionException;
50+
import org.eclipse.aether.resolution.VersionRangeResult;
4051
import org.eclipse.aether.util.filter.DependencyFilterUtils;
4152

53+
import static java.util.stream.Collectors.toList;
54+
4255
/**
4356
* Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath.
4457
*
@@ -58,6 +71,20 @@ public class ExecJavaMojo extends AbstractExecMojo {
5871
* The main class to execute.<br>
5972
* With Java 9 and above you can prefix it with the modulename, e.g. <code>com.greetings/com.greetings.Main</code>
6073
* Without modulename the classpath will be used, with modulename a new modulelayer will be created.
74+
* <p>
75+
* Note that you can also provide a {@link Runnable} fully qualified name.
76+
* The runnable can get constructor injections either by type if you have maven in your classpath (can be provided)
77+
* or by name (ensure to enable {@code -parameters} Java compiler option) for loose coupling.
78+
* Current support loose injections are:
79+
* <ul>
80+
* <li><code>systemProperties</code>: <code>Properties</code>, session system properties</li>
81+
* <li><code>systemPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, session system properties update callback (pass the key/value to update, null value means removal of the key)</li>
82+
* <li><code>userProperties</code>: <code>Properties</code>, session user properties</li>
83+
* <li><code>userPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, session user properties update callback (pass the key/value to update, null value means removal of the key)</li>
84+
* <li><code>projectProperties</code>: <code>Properties</code>, project properties</li>
85+
* <li><code>projectPropertiesUpdater</code>: <code>BiConsumer&lt;String, String&gt;</code>, project properties update callback (pass the key/value to update, null value means removal of the key)</li>
86+
* <li><code>highestVersionResolver</code>: <code>Function&lt;String, String&gt;</code>, passing a <code>groupId:artifactId</code> you get the latest resolved version from the project repositories</li>
87+
* </ul>
6188
*
6289
* @since 1.0
6390
*/
@@ -196,10 +223,11 @@ public class ExecJavaMojo extends AbstractExecMojo {
196223

197224
/**
198225
* Whether to try and prohibit the called Java program from terminating the JVM (and with it the whole Maven build)
199-
* by calling {@link System#exit(int)}. When active, a special security manager will intercept those calls. In case
200-
* of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called. Otherwise, it will
201-
* throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself had exited with an
202-
* exception. This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users
226+
* by calling {@link System#exit(int)}. When active, loaded classes will replace this call by a custom callback.
227+
* In case of an exit code 0 (OK), it will simply log the fact that {@link System#exit(int)} was called.
228+
* Otherwise, it will throw a {@link SystemExitException}, failing the Maven goal as if the called Java code itself
229+
* had exited with an exception.
230+
* This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users
203231
* had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no
204232
* longer necessary.
205233
*
@@ -208,6 +236,9 @@ public class ExecJavaMojo extends AbstractExecMojo {
208236
@Parameter(property = "exec.blockSystemExit", defaultValue = "false")
209237
private boolean blockSystemExit;
210238

239+
@Component // todo: for maven4 move to Lookup instead
240+
private PlexusContainer container;
241+
211242
/**
212243
* Execute goal.
213244
*
@@ -249,7 +280,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
249280
// See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to
250281
// block
251282
// System::exit in post-removal JDKs (still undecided at the time of writing this comment).
252-
Thread bootstrapThread = new Thread(
283+
Thread bootstrapThread = new Thread( // TODO: drop this useless thread 99% of the time
253284
threadGroup,
254285
() -> {
255286
int sepIndex = mainClass.indexOf('/');
@@ -262,15 +293,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
262293
}
263294

264295
try {
265-
Class<?> bootClass =
266-
Thread.currentThread().getContextClassLoader().loadClass(bootClassName);
267-
268-
MethodHandles.Lookup lookup = MethodHandles.lookup();
269-
270-
MethodHandle mainHandle =
271-
lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class));
272-
273-
mainHandle.invoke(arguments);
296+
doExec(bootClassName);
274297
} catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on
275298
Thread.currentThread()
276299
.getThreadGroup()
@@ -295,7 +318,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
295318
}
296319
},
297320
mainClass + ".main()");
298-
URLClassLoader classLoader = getClassLoader();
321+
URLClassLoader classLoader = getClassLoader(); // TODO: enable to cache accross executions
299322
bootstrapThread.setContextClassLoader(classLoader);
300323
setSystemProperties();
301324

@@ -315,7 +338,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
315338

316339
try {
317340
threadGroup.destroy();
318-
} catch (RuntimeException /* missing method in future java version */ e) {
341+
} catch (RuntimeException | Error /* missing method in future java version */ e) {
319342
getLog().warn("Couldn't destroy threadgroup " + threadGroup, e);
320343
}
321344
}
@@ -344,6 +367,160 @@ public void execute() throws MojoExecutionException, MojoFailureException {
344367
registerSourceRoots();
345368
}
346369

370+
private void doExec(final String bootClassName) throws Throwable {
371+
Class<?> bootClass = Thread.currentThread().getContextClassLoader().loadClass(bootClassName);
372+
MethodHandles.Lookup lookup = MethodHandles.lookup();
373+
try {
374+
doMain(lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class)));
375+
} catch (final NoSuchMethodException nsme) {
376+
if (Runnable.class.isAssignableFrom(bootClass)) {
377+
doRun(bootClass);
378+
} else {
379+
throw nsme;
380+
}
381+
}
382+
}
383+
384+
private void doMain(final MethodHandle mainHandle) throws Throwable {
385+
mainHandle.invoke(arguments);
386+
}
387+
388+
private void doRun(final Class<?> bootClass)
389+
throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
390+
final Class<? extends Runnable> runnableClass = bootClass.asSubclass(Runnable.class);
391+
final Constructor<? extends Runnable> constructor = Stream.of(runnableClass.getDeclaredConstructors())
392+
.map(i -> (Constructor<? extends Runnable>) i)
393+
.filter(i -> Modifier.isPublic(i.getModifiers()))
394+
.max(Comparator.<Constructor<? extends Runnable>, Integer>comparing(Constructor::getParameterCount))
395+
.orElseThrow(() -> new IllegalArgumentException("No public constructor found for " + bootClass));
396+
if (getLog().isDebugEnabled()) {
397+
getLog().debug("Using constructor " + constructor);
398+
}
399+
400+
Runnable runnable;
401+
try { // todo: enhance that but since injection API is being defined at mvn4 level it is
402+
// good enough
403+
final Object[] args = Stream.of(constructor.getParameters())
404+
.map(param -> {
405+
try {
406+
return lookupParam(param);
407+
} catch (final ComponentLookupException e) {
408+
getLog().error(e.getMessage(), e);
409+
throw new IllegalStateException(e);
410+
}
411+
})
412+
.toArray(Object[]::new);
413+
constructor.setAccessible(true);
414+
runnable = constructor.newInstance(args);
415+
} catch (final RuntimeException re) {
416+
if (getLog().isDebugEnabled()) {
417+
getLog().debug(
418+
"Can't inject " + runnableClass + "': " + re.getMessage() + ", will ignore injections",
419+
re);
420+
}
421+
final Constructor<? extends Runnable> declaredConstructor = runnableClass.getDeclaredConstructor();
422+
declaredConstructor.setAccessible(true);
423+
runnable = declaredConstructor.newInstance();
424+
}
425+
runnable.run();
426+
}
427+
428+
private Object lookupParam(final java.lang.reflect.Parameter param) throws ComponentLookupException {
429+
final String name = param.getName();
430+
switch (name) {
431+
// loose coupled to maven (wrapped with standard jvm types to not require it)
432+
case "systemProperties": // Properties
433+
return getSession().getSystemProperties();
434+
case "systemPropertiesUpdater": // BiConsumer<String, String>
435+
return propertiesUpdater(getSession().getSystemProperties());
436+
case "userProperties": // Properties
437+
return getSession().getUserProperties();
438+
case "userPropertiesUpdater": // BiConsumer<String, String>
439+
return propertiesUpdater(getSession().getUserProperties());
440+
case "projectProperties": // Properties
441+
return project.getProperties();
442+
case "projectPropertiesUpdater": // BiConsumer<String, String>
443+
return propertiesUpdater(project.getProperties());
444+
case "highestVersionResolver": // Function<String, String>
445+
return resolveVersion(VersionRangeResult::getHighestVersion);
446+
// standard bindings
447+
case "session": // MavenSession
448+
return getSession();
449+
case "container": // PlexusContainer
450+
return container;
451+
default: // Any
452+
return lookup(param, name);
453+
}
454+
}
455+
456+
private Object lookup(final java.lang.reflect.Parameter param, final String name) throws ComponentLookupException {
457+
// try injecting a real instance but loose coupled - will use reflection
458+
if (param.getType() == Object.class && name.contains("_")) {
459+
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
460+
461+
try {
462+
final int hintIdx = name.indexOf("__hint_");
463+
if (hintIdx > 0) {
464+
final String hint = name.substring(hintIdx + "__hint_".length());
465+
final String typeName = name.substring(0, hintIdx).replace('_', '.');
466+
return container.lookup(loader.loadClass(typeName), hint);
467+
}
468+
469+
final String typeName = name.replace('_', '.');
470+
return container.lookup(loader.loadClass(typeName));
471+
} catch (final ClassNotFoundException cnfe) {
472+
if (getLog().isDebugEnabled()) {
473+
getLog().debug("Can't load param (" + name + "): " + cnfe.getMessage(), cnfe);
474+
}
475+
// let's try to lookup object, unlikely but not impossible
476+
}
477+
}
478+
479+
// just lookup by type
480+
return container.lookup(param.getType());
481+
}
482+
483+
private Function<String, String> resolveVersion(final Function<VersionRangeResult, Object> fn) {
484+
return ga -> {
485+
final int sep = ga.indexOf(':');
486+
if (sep < 0) {
487+
throw new IllegalArgumentException("Invalid groupId:artifactId argument: '" + ga + "'");
488+
}
489+
490+
final org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(ga + ":[0,)");
491+
final VersionRangeRequest rangeRequest = new VersionRangeRequest();
492+
rangeRequest.setArtifact(artifact);
493+
try {
494+
if (includePluginDependencies && includeProjectDependencies) {
495+
rangeRequest.setRepositories(Stream.concat(
496+
project.getRemoteProjectRepositories().stream(),
497+
project.getRemotePluginRepositories().stream())
498+
.distinct()
499+
.collect(toList()));
500+
} else if (includePluginDependencies) {
501+
rangeRequest.setRepositories(project.getRemotePluginRepositories());
502+
} else if (includeProjectDependencies) {
503+
rangeRequest.setRepositories(project.getRemoteProjectRepositories());
504+
}
505+
final VersionRangeResult rangeResult =
506+
repositorySystem.resolveVersionRange(getSession().getRepositorySession(), rangeRequest);
507+
return String.valueOf(fn.apply(rangeResult));
508+
} catch (final VersionRangeResolutionException e) {
509+
throw new IllegalStateException(e);
510+
}
511+
};
512+
}
513+
514+
private BiConsumer<String, String> propertiesUpdater(final Properties props) {
515+
return (k, v) -> {
516+
if (v == null) {
517+
props.remove(k);
518+
} else {
519+
props.setProperty(k, v);
520+
}
521+
};
522+
}
523+
347524
/**
348525
* To avoid the exec:java to consider common pool threads leaked, let's pre-create them.
349526
*/

0 commit comments

Comments
 (0)