From 25be3db507bddc0f6546354efad9791a07e572ef Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 11 Nov 2024 13:42:41 -0600 Subject: [PATCH] Remove the ClassLoaderPlus I do not know when we ever need it these days. Jaunch (https://github.com/apposed/jaunch) and other smart native launchers can take care of constructing the correct system classpath for us. It was quite cool e.g. to be able to add entire folders of JARs to the classpath via the `-jarpath` option. However, the custom class loading logic has downsides, too: * Starting in Java 9, the system class loader is not necessarily a URLClassLoader anymore, which causes ClassCastException whenever we assume it is and do a hard cast. * The ClassLoaderPlus invokes Thread#setContextClassLoader() statically, meaning any time you start using it, the class loader for the current thread gets overwritten. This fact makes it hard to unit test, as evidenced by the failing test in the previous commit. I tried adding a static clear() method to ClassLoaderPlus that cleared out all the static data structures and reset the context class loader back to what it was before, but I was unable to make the test pass as part of the entire ClassLoaderPlusTest suite; only when run alone. * The custom class loading logic is significantly more complex than allowing Java's system class loader to take care of class loading via the system class path. It is a separate code path from the other ways an application might get invoked, such as from within an Integrated Development Environment (IDE), meaning some class-loading-related issues might occur only when the application is loaded and launched via the ClassLoaderPlus mechanism, but not when launched from an IDE. Therefore, to simplify the situation, I am paring back the scope of this app-launcher component to avoid use of custom class loaders in favor of only the default class loading mechanism. Obviously, this breaks backwards compatibility, so we bump to 2.x. --- pom.xml | 2 +- .../org/scijava/launcher/ClassLauncher.java | 39 +---- .../org/scijava/launcher/ClassLoaderPlus.java | 164 ------------------ .../java/org/scijava/launcher/Config.java | 1 - .../org/scijava/launcher/JarLauncher.java | 96 ---------- .../scijava/launcher/ClassLoaderPlusTest.java | 76 -------- src/test/resources/ask.jar | Bin 1362 -> 0 bytes src/test/resources/greet.jar | Bin 1253 -> 0 bytes 8 files changed, 5 insertions(+), 373 deletions(-) delete mode 100644 src/main/java/org/scijava/launcher/ClassLoaderPlus.java delete mode 100644 src/main/java/org/scijava/launcher/JarLauncher.java delete mode 100644 src/test/java/org/scijava/launcher/ClassLoaderPlusTest.java delete mode 100644 src/test/resources/ask.jar delete mode 100644 src/test/resources/greet.jar diff --git a/pom.xml b/pom.xml index 4cdea45..b439504 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ app-launcher - 1.2.0-SNAPSHOT + 2.0.0-SNAPSHOT SciJava App Launcher Launcher for SciJava applications. diff --git a/src/main/java/org/scijava/launcher/ClassLauncher.java b/src/main/java/org/scijava/launcher/ClassLauncher.java index 8c3d319..d125aa8 100644 --- a/src/main/java/org/scijava/launcher/ClassLauncher.java +++ b/src/main/java/org/scijava/launcher/ClassLauncher.java @@ -29,7 +29,6 @@ package org.scijava.launcher; -import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URLClassLoader; @@ -41,12 +40,12 @@ * Benefits: *

*
    - *
  1. Call all Java main classes via this class, to be able to generate - * appropriate class paths using the platform-independent convenience provided - * by Java's class library.
  2. *
  3. Verify that the version of Java being used is appropriate * (e.g. new enough) for the application before attempting to launch it.
  4. *
  5. Display a splash window while the application is starting up.
  6. + *
  7. Call all Java main classes via this class, to be able to invoke + * the above-mentioned pre-application-startup routines without + * application code needing to depend on the app-launcher directly.
  8. *
* * @author Johannes Schindelin @@ -78,30 +77,12 @@ private static void tryToRun(Runnable r) { } private static void run(String[] args) { - boolean passClasspath = false; URLClassLoader classLoader = null; int i = 0; for (; i < args.length && args[i].charAt(0) == '-'; i++) { final String option = args[i]; switch (option) { - case "-cp": - case "-classpath": - classLoader = ClassLoaderPlus.get(classLoader, new File(args[++i])); - break; - case "-jarpath": - final String jarPaths = args[++i]; - for (final String jarPath : jarPaths.split(File.pathSeparator)) { - final File jarDir = new File(jarPath); - if (!jarDir.exists()) continue; - classLoader = ClassLoaderPlus.getRecursively(classLoader, true, jarDir); - } - break; - case "-pass-classpath": - passClasspath = true; - break; - case "-freeze-classloader": - ClassLoaderPlus.freeze(classLoader); - break; + // Note: No options for now, but might add some in the future. default: Log.error("Unknown option: " + option + "!"); System.exit(1); @@ -116,10 +97,6 @@ private static void run(String[] args) { String mainClass = args[i]; args = slice(args, i + 1); - if (passClasspath && classLoader != null) { - args = prepend(args, "-classpath", ClassLoaderPlus.getClassPath(classLoader)); - } - Log.debug("Launching main class " + mainClass + " with parameters " + Arrays.toString(args)); try { @@ -143,14 +120,6 @@ private static String[] slice(final String[] array, final int from, return result; } - private static String[] prepend(final String[] array, final String... before) { - if (before.length == 0) return array; - final String[] result = new String[before.length + array.length]; - System.arraycopy(before, 0, result, 0, before.length); - System.arraycopy(array, 0, result, before.length, array.length); - return result; - } - static void launch(ClassLoader classLoader, final String className, final String... args) { diff --git a/src/main/java/org/scijava/launcher/ClassLoaderPlus.java b/src/main/java/org/scijava/launcher/ClassLoaderPlus.java deleted file mode 100644 index 68bd975..0000000 --- a/src/main/java/org/scijava/launcher/ClassLoaderPlus.java +++ /dev/null @@ -1,164 +0,0 @@ -/*- - * #%L - * Launcher for SciJava applications. - * %% - * Copyright (C) 2007 - 2024 SciJava developers. - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ - -package org.scijava.launcher; - -import java.io.File; -import java.lang.reflect.Method; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A {@link ClassLoader} whose classpath can be augmented after instantiation. - * - * @author Johannes Schindelin - */ -class ClassLoaderPlus extends URLClassLoader { - - // A frozen ClassLoaderPlus will add only to the urls array - protected static Set frozen = new HashSet<>(); - protected static Map> urlsMap = new HashMap<>(); - protected static Method addURL; - - public static URLClassLoader get(final URLClassLoader classLoader, final File... files) { - try { - final URL[] urls = new URL[files.length]; - for (int i = 0; i < urls.length; i++) { - urls[i] = files[i].toURI().toURL(); - } - return get(classLoader, urls); - } - catch (final Exception e) { - e.printStackTrace(); - throw new RuntimeException("Uh oh: " + e.getMessage()); - } - } - - public static URLClassLoader get(URLClassLoader classLoader, final URL... urls) { - if (classLoader == null) { - final ClassLoader currentClassLoader = ClassLoaderPlus.class.getClassLoader(); - if (currentClassLoader instanceof URLClassLoader) { - classLoader = (URLClassLoader)currentClassLoader; - } else { - final ClassLoader contextClassLoader = - Thread.currentThread().getContextClassLoader(); - if (contextClassLoader instanceof URLClassLoader) { - classLoader = (URLClassLoader)contextClassLoader; - } - } - } - if (classLoader == null) return new ClassLoaderPlus(urls); - for (final URL url : urls) { - add(classLoader, url); - } - return classLoader; - } - - public static URLClassLoader getRecursively(URLClassLoader classLoader, final boolean onlyJars, - final File directory) - { - try { - if (!onlyJars) - classLoader = get(classLoader, directory); - final File[] list = directory.listFiles(); - if (list != null) { - Arrays.sort(list); - for (final File file : list) { - if (file.isDirectory()) classLoader = getRecursively(classLoader, onlyJars, file); - else if (file.getName().endsWith(".jar")) classLoader = get(classLoader, file); - } - } - return classLoader; - } - catch (final Exception e) { - e.printStackTrace(); - throw new RuntimeException("Uh oh: " + e.getMessage()); - } - } - - public ClassLoaderPlus(final URL... urls) { - super(urls, Thread.currentThread().getContextClassLoader()); - Thread.currentThread().setContextClassLoader(this); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getClass().getName()).append("("); - for (final URL url : getURLs()) { - builder.append(" ").append(url.toString()); - } - builder.append(" )"); - return builder.toString(); - } - - public static void add(final URLClassLoader classLoader, final URL url) { - List urls = urlsMap.computeIfAbsent(classLoader, k -> new ArrayList<>()); - urls.add(url); - if (!frozen.contains(classLoader)) { - if (classLoader instanceof ClassLoaderPlus) { - ((ClassLoaderPlus) classLoader).addURL(url); - } - else try { - if (addURL == null) { - addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addURL.setAccessible(true); - } - addURL.invoke(classLoader, url); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - } - - public static void freeze(final ClassLoader classLoader) { - frozen.add(classLoader); - } - - public static String getClassPath(final ClassLoader classLoader) { - List urls = urlsMap.get(classLoader); - if (urls == null) return ""; - - final StringBuilder builder = new StringBuilder(); - String sep = ""; - for (final URL url : urls) - if (url.getProtocol().equals("file")) { - builder.append(sep).append(url.getPath()); - sep = File.pathSeparator; - } - return builder.toString(); - } -} diff --git a/src/main/java/org/scijava/launcher/Config.java b/src/main/java/org/scijava/launcher/Config.java index b485e80..0b95bc3 100644 --- a/src/main/java/org/scijava/launcher/Config.java +++ b/src/main/java/org/scijava/launcher/Config.java @@ -32,7 +32,6 @@ import java.io.*; import java.nio.file.Files; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; diff --git a/src/main/java/org/scijava/launcher/JarLauncher.java b/src/main/java/org/scijava/launcher/JarLauncher.java deleted file mode 100644 index c9921b9..0000000 --- a/src/main/java/org/scijava/launcher/JarLauncher.java +++ /dev/null @@ -1,96 +0,0 @@ -/*- - * #%L - * Launcher for SciJava applications. - * %% - * Copyright (C) 2007 - 2024 SciJava developers. - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ - -package org.scijava.launcher; - -import java.io.File; -import java.io.IOException; -import java.net.URLClassLoader; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -/** - * A convenience class to launch main classes defined in {@code .jar} files' - * manifests. - *

- * The idea is to simulate what {@code java -jar ...} does, but through the - * app launcher. - *

- * - * @author Johannes Schindelin - */ -public class JarLauncher { - - public static void main(final String[] args) { - if (args.length < 1) { - System.err.println("Missing argument"); - System.exit(1); - } - final String[] shifted = new String[args.length - 1]; - System.arraycopy(args, 1, shifted, 0, shifted.length); - launchJar(args[0], shifted); - } - - /** - * Helper to launch .jar files (by inspecting their Main-Class attribute). - */ - private static void launchJar(final String jarPath, final String[] arguments) { - JarFile jar = null; - try { - jar = new JarFile(jarPath); - } - catch (final IOException e) { - System.err.println("Could not read '" + jarPath + "'."); - System.exit(1); - return; // NB: Avoids warnings below. - } - Manifest manifest = null; - try { - manifest = jar.getManifest(); - } - catch (final IOException e) { - // no action needed - } - if (manifest == null) { - System.err.println("No manifest found in '" + jarPath + "'."); - System.exit(1); - return; // NB: Avoids warnings below. - } - final Attributes attributes = manifest.getMainAttributes(); - final String className = - attributes == null ? null : attributes.getValue("Main-Class"); - if (className == null) { - System.err.println("No main class attribute found in '" + jarPath + "'."); - System.exit(1); - } - final URLClassLoader loader = ClassLoaderPlus.get(null, new File(jarPath)); - ClassLauncher.launch(loader, className, arguments); - } -} diff --git a/src/test/java/org/scijava/launcher/ClassLoaderPlusTest.java b/src/test/java/org/scijava/launcher/ClassLoaderPlusTest.java deleted file mode 100644 index c1126dd..0000000 --- a/src/test/java/org/scijava/launcher/ClassLoaderPlusTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.scijava.launcher; - -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.lang.reflect.Method; -import java.net.URLClassLoader; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -/** - * Tests {@link ClassLoaderPlus}. - * - * @author Curtis Rueden - */ -public class ClassLoaderPlusTest { - - /** Tests {@link ClassLoaderPlus#get}. */ - @Test - public void testGet() throws Exception { - File jarFile = Paths.get("src", "test", "resources", "greet.jar").toFile(); - assumeTrue(jarFile.isFile()); - URLClassLoader greetLoader = ClassLoaderPlus.get(null, jarFile); - assertNotNull(greetLoader); - Class greetClass = greetLoader.loadClass("com.example.greet.Greet"); - assertNotNull(greetClass); - assertEquals("com.example.greet.Greet", greetClass.getName()); - } - - @Test - public void testGetFailure() throws Exception { - File jarFile = Paths.get("src", "test", "resources", "ask.jar").toFile(); - assumeTrue(jarFile.isFile()); - URLClassLoader askLoader = ClassLoaderPlus.get(null, jarFile); - assertNotNull(askLoader); - Class askClass = askLoader.loadClass("com.example.ask.Ask"); - assertNotNull(askClass); - Exception expected = null; - Object value = null; - try { - value = askClass.getMethod("question").invoke(null); - } - catch (Exception e) { - expected = e; - assertEquals( - "java.lang.NoClassDefFoundError: com/example/greet/Greet", - e.getCause().toString() - ); - } - if (expected == null) fail("Expected exception but got value: " + value); - } - - /** Tests {@link ClassLoaderPlus#getRecursively}. */ - @Test - public void testGetRecursively() throws Exception { - File jarDir = Paths.get("src", "test", "resources").toFile(); - assumeTrue(jarDir.isDirectory()); - URLClassLoader classLoader = ClassLoaderPlus.getRecursively(null, true, jarDir); - assertNotNull(classLoader); - Class askClass = classLoader.loadClass("com.example.ask.Ask"); - Method questionMethod = askClass.getMethod("question()"); - Object question = questionMethod.invoke(null); - assertEquals("Hello! How are you?", question); - } - - /** Tests {@link ClassLoaderPlus#getRecursively} failure cases. */ - @Test - public void testGetRecursivelyFailure() throws Exception { - File jarDir = Paths.get("src", "main", "java").toFile(); - assumeTrue(jarDir.isDirectory()); - URLClassLoader classLoader = ClassLoaderPlus.getRecursively(null, true, jarDir); - assertNull(classLoader); - } -} diff --git a/src/test/resources/ask.jar b/src/test/resources/ask.jar deleted file mode 100644 index 5eef78c7042f1a1102c33c329599c9bbae0bd08e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1362 zcmWIWW@Zs#;Nak3xYCs#$$$hn8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1guf{y0}D_#IX_n)p<)r53LdD6)QZI1f}B(&mFLjHLjX->VsSQDH6-*dpsAL`qS~=I zTQ4~$vA8%j;I!W%2NB!DtG9-AH$328Yi%$w?@p#k)*`(QJp(m8_8BIvmkwU~^+Wq^ z!^I=~9~SqNH@Glww)j4=_}$+0ef4(x7-SBZZD^R&wx^M$h>a^EG&0cL^QbBBYbi4c zBloxmuE%*!dVTFvEY-W+bZ}L5Lz~d?39h*`8n;4<)fM3f9)&yvEcQEoVGXjkIYVU-=Y-tY5t2gCzo?eovhMhE=C!h z`CTixMe>{WXT4{=zVB7)t5no=-#9HlAF9u|?2q8Akg1wRT`%_v-jG&J=`b+PoRlJY zZAz5#%BrLOD(~Dq_9>gXSWQ=yaH^hrQR__K^|gPPKnd|ztJq3MMh1q5K#WL;=&>e& zBObC6%MwF-yt$YR1zf&cx*BS`o?uN=Tvzg*^VWry3F|%C-P?BV{uf{y0}D_#IX_n)w+bGpiqwk4+=84`B$a#7!b1p6WqMI+Y6)09B>d3T%V1US4$`HU zl~|Uj+wINOV8Fxr-SX(=sd;-E*e~o=?@$#nT)VVap`-kJ^J+;pwT;m?x$lTsSz4$} zJ(n){y7Q9V>qif!cpv=WHizZUPKn#iCqD_j?O;X zeW6ODD&u9XaQlYT$x|jw`=?_oYV(8nPvE0BN*wPF%)WX2`0Ss*{{3JnXzylGJZELm zf3f?bx=V2ItY?h^J;$=0enjjwTDaXn)&70gl-E=J)>_Qnla}W*Q+S=*o>;0<)`fKAXivueyub?>nn+R{dsgTeRG6_ituU zNY$-Msk{#i2Sr9uNHH>rFra1$Sb7I#2~+@2wy2mpfxStl&zBLWJfA33>!(mnz> qVAYSv3CQ6FN}kBk0ZN_-@Engx;KUl>&B_K+&JKk8fQki~K|BEPCsS