All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.codehaus.mojo.exec.ExecJavaMojo Maven / Gradle / Ivy

package org.codehaus.mojo.exec;

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.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.Component;
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.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.
 *
 * @author Kaare Nilsen ([email protected]), David Smiley ([email protected])
 * @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; /** * 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; @Component // todo: for maven4 move to Lookup instead private PlexusContainer 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 */); // TODO: // Adjust implementation for future JDKs after removal of SecurityManager. // See https://openjdk.org/jeps/411 for basic information. // See https://bugs.openjdk.org/browse/JDK-8199704 for details about how users might be able to // block // System::exit in post-removal JDKs (still undecided at the time of writing this comment). 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) { getLog().info(systemExitException.getMessage()); if (systemExitException.getExitCode() != 0) { throw systemExitException; } } 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 { doMain(lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class))); } catch (final NoSuchMethodException nsme) { if (Runnable.class.isAssignableFrom(bootClass)) { doRun(bootClass); } else { throw nsme; } } } private void doMain(final MethodHandle mainHandle) throws Throwable { mainHandle.invoke(arguments); } 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) { // loose coupled to maven (wrapped with standard jvm types to not require it) 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); // standard bindings 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 { final URLClassLoaderBuilder builder = URLClassLoaderBuilder.builder() .setLogger(getLog()) .setPaths(classpathURLs) .setExclusions(classpathFilenameExclusions); if (blockSystemExit) { builder.setTransformer(new BlockExitTransformer()); } return builder.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."); } } /** * Stop program execution for nn millis. * * @param millis the number of millis-seconds to wait for, 0 stops program forever. */ 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); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy