diff --git a/README.md b/README.md index 6aaec293..d2db7a9d 100644 --- a/README.md +++ b/README.md @@ -319,6 +319,7 @@ You can set a variety of options on the command line or with system properties ( | `thin.force` | false | Force dependency resolution to happen, even if dependencies have been computed, and marked as "computed" in `thin.properties`. | | `thin.classpath` | false | Only print the classpath. Don't run the main class. Two formats are supported: "path" and "properties". For backwards compatibility "true" or empty are equivalent to "path". | | `thin.root` | `${user.home}/.m2` | The location of the local jar cache, laid out as a maven repository. The launcher creates a new directory here called "repository" if it doesn't exist. | +| `thin.libs` | `` | Additional classpath entries to append at runtime in the same form as you would use in `java -classpath ...`. If this property is defined then unresolved dependencies will be ignored when the classpath is computed, possibly leading to runtime class not found exceptions. | | `thin.archive` | the same as the target archive | The archive to launch. Can be used to launch a JAR file that was build with a different version of the thin launcher, for instance, or a fat jar built by Spring Boot without the thin launcher. | | `thin.parent` | `` | A parent archive to use for dependency management and common classpath entries. If you run two apps with the same parent, they will have a classpath that is the same, reading from left to right, until they actually differ. | | `thin.location` | `file:.,classpath:/` | The path to directory containing thin properties files (as per `thin.name`), as a comma-separated list of resource locations (directories). These locations plus the same paths relative /META-INF will be searched. | diff --git a/launcher/src/main/java/org/springframework/boot/loader/thin/ArchiveUtils.java b/launcher/src/main/java/org/springframework/boot/loader/thin/ArchiveUtils.java index 27d45fa6..5caddba4 100644 --- a/launcher/src/main/java/org/springframework/boot/loader/thin/ArchiveUtils.java +++ b/launcher/src/main/java/org/springframework/boot/loader/thin/ArchiveUtils.java @@ -27,7 +27,6 @@ import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.Dependency; - import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.ExplodedArchive; import org.springframework.boot.loader.archive.JarFileArchive; @@ -53,8 +52,7 @@ public static Archive getArchive(String path) { } try { return new JarFileArchive(new JarFile(file)); - } - catch (IOException e) { + } catch (IOException e) { throw new IllegalStateException("Cannot create JAR archive: " + file, e); } } @@ -62,8 +60,7 @@ public static Archive getArchive(String path) { public static File getArchiveRoot(Archive archive) { try { return new File(jarFile(archive.getUrl()).toURI()); - } - catch (Exception e) { + } catch (Exception e) { throw new IllegalStateException("Cannot locate JAR archive: " + archive, e); } } @@ -82,19 +79,16 @@ public static String findMainClass(Archive archive) { return mainClass; } } - } - catch (Exception e) { + } catch (Exception e) { } try { File root = getArchiveRoot(archive); if (archive instanceof ExplodedArchive) { return MainClassFinder.findSingleMainClass(root); - } - else { + } else { return MainClassFinder.findSingleMainClass(new JarFile(root), ""); } - } - catch (Exception e) { + } catch (Exception e) { throw new IllegalStateException("Cannot locate main class in " + archive, e); } } @@ -104,8 +98,7 @@ private static URI findArchive(String path) { if (archive != null) { try { return jarFile(archive.toURL()).toURI(); - } - catch (Exception e) { + } catch (Exception e) { throw new IllegalStateException("Cannot create URI for " + archive); } } @@ -148,8 +141,7 @@ private static URL jarFile(URL url) { } try { url = new URL(path); - } - catch (MalformedURLException e) { + } catch (MalformedURLException e) { throw new IllegalStateException("Bad URL for jar file: " + path, e); } } @@ -165,8 +157,7 @@ public static List nestedClasses(Archive archive, String... paths) { extras.add(classes.getURL()); } } - } - catch (Exception e) { + } catch (Exception e) { throw new IllegalStateException("Cannot create urls for resources", e); } return extras; @@ -189,4 +180,27 @@ private static URL[] locateFiles(URL[] urls) { return urls; } + public static List getArchives(String path) { + List list = new ArrayList<>(); + for (String element : path.split(File.pathSeparator)) { + if (element.endsWith("*")) { + File dir = new File(element.substring(0, element.length() - 1)); + if (dir.isDirectory()) { + for (File file : dir.listFiles()) { + if (file.getName().endsWith(".jar")) { + try { + list.add(getArchive(file.getCanonicalPath())); + } catch (IOException e) { + // ignore + } + } + } + } + } else { + list.add(getArchive(element)); + } + } + return list; + } + } diff --git a/launcher/src/main/java/org/springframework/boot/loader/thin/DependencyResolver.java b/launcher/src/main/java/org/springframework/boot/loader/thin/DependencyResolver.java index 6a6490a7..1c777a66 100644 --- a/launcher/src/main/java/org/springframework/boot/loader/thin/DependencyResolver.java +++ b/launcher/src/main/java/org/springframework/boot/loader/thin/DependencyResolver.java @@ -214,7 +214,8 @@ public List dependencies(final Resource resource, DependencyResolver.globals = null; DependencyResolutionResult dependencies = result .getDependencyResolutionResult(); - if (!dependencies.getUnresolvedDependencies().isEmpty()) { + if (!dependencies.getUnresolvedDependencies().isEmpty() && + properties.getProperty(ThinJarLauncher.THIN_LIBS, "").length()==0) { StringBuilder builder = new StringBuilder(); for (Dependency dependency : dependencies .getUnresolvedDependencies()) { diff --git a/launcher/src/main/java/org/springframework/boot/loader/thin/PathResolver.java b/launcher/src/main/java/org/springframework/boot/loader/thin/PathResolver.java index 46a302f4..5fc95ff2 100644 --- a/launcher/src/main/java/org/springframework/boot/loader/thin/PathResolver.java +++ b/launcher/src/main/java/org/springframework/boot/loader/thin/PathResolver.java @@ -59,6 +59,8 @@ public class PathResolver { private String root; + private String libs; + private Properties overrides = new Properties(); private boolean offline; @@ -79,6 +81,10 @@ public void setRoot(String root) { this.root = root; } + public void setLibs(String libs) { + this.libs = libs; + } + public void setOverrides(Properties overrides) { this.overrides = overrides; } @@ -274,6 +280,9 @@ private Properties getProperties(Archive archive, String name, String[] profiles if (root != null) { properties.setProperty("thin.root", root); } + if (libs != null) { + properties.setProperty("thin.libs", libs); + } if (offline) { properties.setProperty("thin.offline", "true"); } diff --git a/launcher/src/main/java/org/springframework/boot/loader/thin/ThinJarLauncher.java b/launcher/src/main/java/org/springframework/boot/loader/thin/ThinJarLauncher.java index 2a03eb3b..e3dd8791 100644 --- a/launcher/src/main/java/org/springframework/boot/loader/thin/ThinJarLauncher.java +++ b/launcher/src/main/java/org/springframework/boot/loader/thin/ThinJarLauncher.java @@ -20,6 +20,7 @@ import java.net.URL; import java.security.AccessControlException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -55,87 +56,113 @@ public class ThinJarLauncher extends ExecutableArchiveLauncher { public static final String THIN_MAIN = "thin.main"; /** - * System property to signal a "dry run" where dependencies are resolved but the main + * System property to signal a "dry run" where dependencies are resolved but the + * main * method is not executed. */ public static final String THIN_DRYRUN = "thin.dryrun"; /** - * System property to signal that dependency resolution should be attempted even if + * System property to signal that dependency resolution should be attempted even + * if * there is a "computed" flag in thin.properties. */ public static final String THIN_FORCE = "thin.force"; /** * System property to signal offline execution (all dependencies can be resolved - * locally). Defaults to "false" and any value other than "false" is equivalent to + * locally). Defaults to "false" and any value other than "false" is equivalent + * to * "true". */ public static final String THIN_OFFLINE = "thin.offline"; /** - * System property to signal a "classpath run" where dependencies are resolved but the - * main method is not executed and the output is in the form of a classpath. Supported + * System property to signal a "classpath run" where dependencies are resolved + * but the + * main method is not executed and the output is in the form of a classpath. + * Supported * formats are "path" and "properties". */ public static final String THIN_CLASSPATH = "thin.classpath"; /** - * System property holding the path to the root directory, where Maven repository and + * System property holding the path to the root directory, where Maven + * repository and * settings live. Defaults to ${user.home}/.m2. */ public static final String THIN_ROOT = "thin.root"; /** - * System property used by wrapper to communicate the location of the main archive. - * Can also be used in a dryrun or classpath launch to override the archive location + * System property used by wrapper to communicate the location of the main + * archive. + * Can also be used in a dryrun or classpath launch to override the archive + * location * with a file or "maven://..." style URL. */ public static final String THIN_ARCHIVE = "thin.archive"; /** - * A parent archive (URL or "maven://..." locator) that controls the classpath and + * A parent archive (URL or "maven://..." locator) that controls the classpath + * and * dependency management defaults for the main archive. */ public static final String THIN_PARENT = "thin.parent"; /** - * The path to thin properties files (as per thin.name), as a comma-separated list of - * resources (these locations plus relative /META-INF will be searched). Defaults to + * The path to thin properties files (as per thin.name), as a comma-separated + * list of + * resources (these locations plus relative /META-INF will be searched). + * Defaults to * current directory and classpath:/. */ public static final String THIN_LOCATION = "thin.location"; /** - * The name of the launchable (i.e. the properties file name). Defaults to "thin" (so + * The name of the launchable (i.e. the properties file name). Defaults to + * "thin" (so * "thin.properties" is the default file name with no profiles). */ public static final String THIN_NAME = "thin.name"; /** - * The name of the profile to run, changing the location of the properties files to + * The name of the profile to run, changing the location of the properties files + * to * look up. */ public static final String THIN_PROFILE = "thin.profile"; /** - * Flag to say that classloader should be parent first (default true). You may need it - * to be false if the target archive contains classes in the root, and you want to - * also use a Java agent (because the agent and the app classes have to be all on the + * Flag to say that classloader should be parent first (default true). You may + * need it + * to be false if the target archive contains classes in the root, and you want + * to + * also use a Java agent (because the agent and the app classes have to be all + * on the * classpath). Some agents work with the default settings though. */ public static final String THIN_PARENT_FIRST = "thin.parent.first"; /** - * Flag to say that classloader parent should be the boot loader, not the system class - * loader. Default true; + * Flag to say that classloader parent should be the boot loader, not the system + * class + * loader. Default true. */ public static final String THIN_PARENT_BOOT = "thin.parent.boot"; + /** + * Additional path elements to append to classpath, with OS-specific path + * separator. Also + * accepts wildcards on a directory path (like java classpath). Default empty. + */ + public static final String THIN_LIBS = "thin.libs"; + private StandardEnvironment environment = new StandardEnvironment(); private boolean debug; + private List libs = new ArrayList<>(); + public static void main(String[] args) throws Exception { LogUtils.setLogLevel(Level.OFF); new ThinJarLauncher(args).launch(args); @@ -160,19 +187,18 @@ protected void launch(String[] args) throws Exception { if (classpath || compute) { this.debug = false; LogUtils.setLogLevel(Level.OFF); - } - else { + } else { this.debug = trace || !"false".equals( environment.resolvePlaceholders("${thin.debug:${debug:false}}")); } if (debug || trace) { if (trace) { LogUtils.setLogLevel(Level.TRACE); - } - else { + } else { LogUtils.setLogLevel(Level.INFO); } } + this.libs.addAll(ArchiveUtils.getArchives(environment.resolvePlaceholders("${thin.libs:}"))); if (classpath) { List archives = getClassPathArchives(); System.out.println(classpath(archives)); @@ -307,8 +333,7 @@ private void addCommandLineProperties(String[] args) { "commandArgs", args); if (!properties.contains("commandArgs")) { properties.addFirst(source); - } - else { + } else { properties.replace("commandArgs", source); } } @@ -329,8 +354,7 @@ protected ClassLoader createClassLoader(URL[] urls) throws Exception { environment.resolvePlaceholders("${" + THIN_PARENT_FIRST + ":true}"))) { // Use a (traditional) parent first class loader loader.setParentFirst(true); - } - else { + } else { loader.setParentFirst(false); } // Restore default @@ -370,8 +394,12 @@ private List getClassPathArchives(String root) throws Exception { profiles); long t1 = System.currentTimeMillis(); if (log.isInfoEnabled()) { + if (!this.libs.isEmpty()) { + log.info("Adding libraries: " + this.libs); + } log.info("Dependencies resolved in: " + (t1 - t0) + "ms"); } + archives.addAll(this.libs); return archives; } @@ -389,6 +417,7 @@ private PathResolver getResolver() { String locations = environment .resolvePlaceholders("${" + ThinJarLauncher.THIN_LOCATION + ":}"); String root = environment.resolvePlaceholders("${" + THIN_ROOT + ":}"); + String libs = environment.resolvePlaceholders("${" + THIN_LIBS + ":}"); String offline = environment.resolvePlaceholders("${" + THIN_OFFLINE + ":false}"); String force = environment.resolvePlaceholders( "${" + THIN_FORCE + ":${" + THIN_DRYRUN + ":false}}"); @@ -403,6 +432,9 @@ private PathResolver getResolver() { resolver.setPreferLocalSnapshots(false); } } + if (StringUtils.hasText(libs)) { + resolver.setLibs(libs); + } if (!"false".equals(offline)) { resolver.setOffline(true); } @@ -424,8 +456,7 @@ private Properties getSystemProperties() { properties.setProperty(name, system.getProperty(key.toString())); } } - } - catch (AccessControlException e) { + } catch (AccessControlException e) { // ignore } if (environment.getPropertySources().contains("commandArgs")) { @@ -456,8 +487,7 @@ private static Archive computeArchive(String[] args) throws Exception { if (arg.length() <= prefix.length() + 1) { // ... even cancel it by setting it to empty path = null; - } - else { + } else { path = arg.substring(prefix.length() + 1); } } @@ -495,8 +525,7 @@ protected Class loadClass(String name, boolean resolve) if (!parentFirst) { return findClass(name); } - } - catch (ClassNotFoundException e) { + } catch (ClassNotFoundException e) { } return super.loadClass(name, resolve); } diff --git a/launcher/src/test/java/org/springframework/boot/loader/thin/ArchiveUtilsTests.java b/launcher/src/test/java/org/springframework/boot/loader/thin/ArchiveUtilsTests.java new file mode 100644 index 00000000..9464514a --- /dev/null +++ b/launcher/src/test/java/org/springframework/boot/loader/thin/ArchiveUtilsTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed 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 + * + * https://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. + */ +package org.springframework.boot.loader.thin; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +public class ArchiveUtilsTests { + + @Test + public void resolveAll() throws Exception { + assertThat(ArchiveUtils.getArchives("src/test/resources/*").size()).isEqualTo(2); + } + + @Test + public void resolveSome() throws Exception { + assertThat(ArchiveUtils.getArchives("src/test/resources/app-with-web-and-cloud-config.jar" + File.pathSeparator + + "src/test/resources/app-with-web-in-lib-properties.jar").size()).isEqualTo(2); + } + +} diff --git a/launcher/src/test/java/org/springframework/boot/loader/thin/DependencyResolverTests.java b/launcher/src/test/java/org/springframework/boot/loader/thin/DependencyResolverTests.java index 51768a7d..db78aee3 100644 --- a/launcher/src/test/java/org/springframework/boot/loader/thin/DependencyResolverTests.java +++ b/launcher/src/test/java/org/springframework/boot/loader/thin/DependencyResolverTests.java @@ -339,6 +339,15 @@ public void authentication() throws Exception { assertThat(dependencies.size()).isGreaterThan(16); } + @Test + public void unresolvedButLibsProvided() throws Exception { + Resource resource = new ClassPathResource("apps/missing/pom.xml"); + Properties properties = new Properties(); + properties.setProperty("thin.libs", "no/such/file"); + List dependencies = resolver.dependencies(resource, properties); + assertThat(dependencies.size()).isGreaterThan(0); + } + static Condition version(final String version) { return new Condition("artifact matches " + version) { @Override