diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java index 89825113db6..eb2be18dd18 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java @@ -56,6 +56,8 @@ import org.springframework.boot.cli.jar.PackagedSpringApplicationLauncher; import org.springframework.boot.loader.tools.JarWriter; import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.Layouts; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.Assert; @@ -248,7 +250,8 @@ public class JarCommand extends OptionParsingCommand { private void addDependency(JarWriter writer, File dependency) throws FileNotFoundException, IOException { if (dependency.isFile()) { - writer.writeNestedLibrary("lib/", dependency); + writer.writeNestedLibrary("lib/", new Library(dependency, + LibraryScope.COMPILE)); } } diff --git a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc index 36191316124..9a88a544a56 100644 --- a/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc +++ b/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc @@ -511,6 +511,11 @@ The following configuration options are available: |`layout` |The type of archive, corresponding to how the dependencies are laid out inside (defaults to a guess based on the archive type). + +|`requiresUnpack` +|A list of dependencies (in the form ``groupId:artifactId'' that must be unpacked from + fat jars in order to run. Items are still packaged into the fat jar, but they will be + automatically unpacked when it runs. |=== diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 48f685795c6..cd9508c0e76 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -1618,6 +1618,50 @@ For Gradle users the steps are similar. Example: +[[howto-extract-specific-libraries-when-an-executable-jar-runs]] +=== Extract specific libraries when an executable jar runs +Most nested libraries in an executable jar do not need to be unpacked in order to run, +however, certain libraries can have problems. For example, JRuby includes its own nested +jar support which assumes that the `jruby-complete.jar` is always directly available as a +file in its own right. + +To deal with any problematic libraries, you can flag that specific nested jars should be +automatically unpacked to the ``temp folder'' when the executable jar first runs. + +For example, to indicate that JRuby should be flagged for unpack using the Maven Plugin +you would add the following configuration: + +[source,xml,indent=0,subs="verbatim,quotes,attributes"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.jruby + jruby-complete + + + + + + +---- + +And to do that same with Gradle: + +[source,groovy,indent=0,subs="verbatim,attributes"] +---- + springBoot { + requiresUnpack = ['org.jruby:jruby-complete'] + } +---- + + + [[howto-create-a-nonexecutable-jar]] === Create a non-executable JAR with exclusions Often if you have an executable and a non-executable jar as build products, the executable diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy index 1aa5c0df0ec..bac7f934ef2 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy @@ -107,6 +107,12 @@ public class SpringBootPluginExtension { (layout == null ? null : layout.layout) } + /** + * Libraries that must be unpacked from fat jars in order to run. Use Strings in the + * form {@literal groupId:artifactId}. + */ + Set requiresUnpack; + /** * Location of an agent jar to attach to the VM when running the application with runJar task. */ @@ -121,4 +127,5 @@ public class SpringBootPluginExtension { * If exclude rules should be applied to dependencies based on the spring-dependencies-bom */ boolean applyExcludeRules = true; + } diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java index 17d783a09c7..0394ef61a32 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java @@ -18,10 +18,15 @@ package org.springframework.boot.gradle.repackage; import java.io.File; import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.file.FileCollection; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.springframework.boot.gradle.SpringBootPluginExtension; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; @@ -37,22 +42,24 @@ class ProjectLibraries implements Libraries { private final Project project; + private final SpringBootPluginExtension extension; + private String providedConfigurationName = "providedRuntime"; private String customConfigurationName = null; /** * Create a new {@link ProjectLibraries} instance of the specified {@link Project}. - * * @param project the gradle project + * @param extension the extension */ - public ProjectLibraries(Project project) { + public ProjectLibraries(Project project, SpringBootPluginExtension extension) { this.project = project; + this.extension = extension; } /** * Set the name of the provided configuration. Defaults to 'providedRuntime'. - * * @param providedConfigurationName the providedConfigurationName to set */ public void setProvidedConfigurationName(String providedConfigurationName) { @@ -65,27 +72,20 @@ class ProjectLibraries implements Libraries { @Override public void doWithLibraries(LibraryCallback callback) throws IOException { - - FileCollection custom = this.customConfigurationName != null ? this.project - .getConfigurations().findByName(this.customConfigurationName) : null; - + Set custom = getArtifacts(this.customConfigurationName); if (custom != null) { libraries(LibraryScope.CUSTOM, custom, callback); } else { - FileCollection compile = this.project.getConfigurations() - .getByName("compile"); - - FileCollection runtime = this.project.getConfigurations() - .getByName("runtime"); - runtime = runtime.minus(compile); + Set compile = getArtifacts("compile"); - FileCollection provided = this.project.getConfigurations() - .findByName(this.providedConfigurationName); + Set runtime = getArtifacts("runtime"); + runtime = minus(runtime, compile); + Set provided = getArtifacts(this.providedConfigurationName); if (provided != null) { - compile = compile.minus(provided); - runtime = runtime.minus(provided); + compile = minus(compile, provided); + runtime = minus(runtime, provided); } libraries(LibraryScope.COMPILE, compile, callback); @@ -94,12 +94,47 @@ class ProjectLibraries implements Libraries { } } - private void libraries(LibraryScope scope, FileCollection files, + private Set getArtifacts(String configurationName) { + Configuration configuration = (configurationName == null ? null : this.project + .getConfigurations().findByName(configurationName)); + return (configuration == null ? null : configuration.getResolvedConfiguration() + .getResolvedArtifacts()); + } + + private Set minus(Set source, + Set toRemove) { + if (source == null || toRemove == null) { + return source; + } + Set filesToRemove = new HashSet(); + for (ResolvedArtifact artifact : toRemove) { + filesToRemove.add(artifact.getFile()); + } + Set result = new LinkedHashSet(); + for (ResolvedArtifact artifact : source) { + if (!toRemove.contains(artifact.getFile())) { + result.add(artifact); + } + } + return result; + } + + private void libraries(LibraryScope scope, Set artifacts, LibraryCallback callback) throws IOException { - if (files != null) { - for (File file: files) { - callback.library(new Library(file, scope)); + if (artifacts != null) { + for (ResolvedArtifact artifact : artifacts) { + callback.library(new Library(artifact.getFile(), scope, isUnpackRequired(artifact))); } } } + + private boolean isUnpackRequired(ResolvedArtifact artifact) { + if (this.extension.getRequiresUnpack() != null) { + ModuleVersionIdentifier id = artifact.getModuleVersion().getId(); + return this.extension.getRequiresUnpack().contains( + id.getGroup() + ":" + id.getName()); + } + return false; + } + } diff --git a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java index 6dbf1515a92..f083a5f84f1 100644 --- a/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java +++ b/spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java @@ -101,7 +101,7 @@ public class RepackageTask extends DefaultTask { Project project = getProject(); SpringBootPluginExtension extension = project.getExtensions().getByType( SpringBootPluginExtension.class); - ProjectLibraries libraries = new ProjectLibraries(project); + ProjectLibraries libraries = new ProjectLibraries(project, extension); if (extension.getProvidedConfiguration() != null) { libraries.setProvidedConfigurationName(extension.getProvidedConfiguration()); } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java index cdf1d543f58..e7dcbdd9c12 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java @@ -17,13 +17,19 @@ package org.springframework.boot.loader.tools; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; /** * Utilities for manipulating files and directories in Spring Boot tooling. * * @author Dave Syer + * @author Phillip Webb */ -public class FileUtils { +public abstract class FileUtils { /** * Utility to remove duplicate files from an "output" directory if they already exist @@ -50,4 +56,37 @@ public class FileUtils { } } + /** + * Generate a SHA.1 Hash for a given file. + * @param file the file to hash + * @return the hash value as a String + * @throws IOException + */ + public static String sha1Hash(File file) throws IOException { + try { + DigestInputStream inputStream = new DigestInputStream(new FileInputStream( + file), MessageDigest.getInstance("SHA-1")); + try { + byte[] buffer = new byte[4098]; + while (inputStream.read(buffer) != -1) { + // Read the entire stream + } + return bytesToHex(inputStream.getMessageDigest().digest()); + } + finally { + inputStream.close(); + } + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder hex = new StringBuilder(); + for (byte b : bytes) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java index dbef42207f7..55daa0a3039 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java @@ -50,7 +50,7 @@ public class JarWriter { private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; - private static final int BUFFER_SIZE = 4096; + private static final int BUFFER_SIZE = 32 * 1024; private final JarOutputStream jarOutput; @@ -122,11 +122,16 @@ public class JarWriter { /** * Write a nested library. * @param destination the destination of the library - * @param file the library file + * @param library the library * @throws IOException if the write fails */ - public void writeNestedLibrary(String destination, File file) throws IOException { + public void writeNestedLibrary(String destination, Library library) + throws IOException { + File file = library.getFile(); JarEntry entry = new JarEntry(destination + file.getName()); + if (library.isUnpackRequired()) { + entry.setComment("UNPACK:" + FileUtils.sha1Hash(file)); + } new CrcAndSize(file).setupStoredEntry(entry); writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true)); } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index 584b2028cfd..4fa9a9d8182 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -31,14 +31,27 @@ public class Library { private final LibraryScope scope; + private final boolean unpackRequired; + /** * Create a new {@link Library}. * @param file the source file * @param scope the scope of the library */ public Library(File file, LibraryScope scope) { + this(file, scope, false); + } + + /** + * Create a new {@link Library}. + * @param file the source file + * @param scope the scope of the library + * @param unpackRequired if the library needs to be unpacked before it can be used + */ + public Library(File file, LibraryScope scope, boolean unpackRequired) { this.file = file; this.scope = scope; + this.unpackRequired = unpackRequired; } /** @@ -55,4 +68,12 @@ public class Library { return this.scope; } + /** + * @return if the file cannot be used directly as a nested jar and needs to be + * unpacked. + */ + public boolean isUnpackRequired() { + return this.unpackRequired; + } + } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 2b8c7855b7e..785d6a2c0ec 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -147,7 +147,7 @@ public class Repackager { String destination = Repackager.this.layout .getLibraryDestination(file.getName(), library.getScope()); if (destination != null) { - writer.writeNestedLibrary(destination, file); + writer.writeNestedLibrary(destination, library); } } } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java index 475f16e2a22..4fe75b33038 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java @@ -17,22 +17,32 @@ package org.springframework.boot.loader.tools; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.springframework.util.FileSystemUtils; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** * Tests fir {@link FileUtils}. * * @author Dave Syer + * @author Phillip Webb */ public class FileUtilsTests { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private File outputDirectory; private File originDirectory; @@ -91,4 +101,18 @@ public class FileUtilsTests { assertTrue(file.exists()); } + @Test + public void hash() throws Exception { + File file = this.temporaryFolder.newFile(); + OutputStream outputStream = new FileOutputStream(file); + try { + outputStream.write(new byte[] { 1, 2, 3 }); + } + finally { + outputStream.close(); + } + assertThat(FileUtils.sha1Hash(file), + equalTo("7037807198c22a7d2b0807371d763779a84fdfcf")); + } + } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index 1fa28536a8e..042be2570b7 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.loader.tools; import java.io.File; import java.io.IOException; import java.util.jar.Attributes; +import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -33,6 +34,7 @@ import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod; import org.springframework.util.FileCopyUtils; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyString; @@ -258,6 +260,7 @@ public class RepackagerTests { TestJarFile libJar = new TestJarFile(this.temporaryFolder); libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); final File libJarFile = libJar.getFile(); + final File libJarFileToUnpack = libJar.getFile(); final File libNonJarFile = this.temporaryFolder.newFile(); FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile); this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); @@ -267,11 +270,17 @@ public class RepackagerTests { @Override public void doWithLibraries(LibraryCallback callback) throws IOException { callback.library(new Library(libJarFile, LibraryScope.COMPILE)); + callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE, + true)); callback.library(new Library(libNonJarFile, LibraryScope.COMPILE)); } }); assertThat(hasEntry(file, "lib/" + libJarFile.getName()), equalTo(true)); + assertThat(hasEntry(file, "lib/" + libJarFileToUnpack.getName()), equalTo(true)); assertThat(hasEntry(file, "lib/" + libNonJarFile.getName()), equalTo(false)); + JarEntry entry = getEntry(file, "lib/" + libJarFileToUnpack.getName()); + assertThat(entry.getComment(), startsWith("UNPACK:")); + assertThat(entry.getComment().length(), equalTo(47)); } @Test @@ -345,7 +354,6 @@ public class RepackagerTests { finally { jarFile.close(); } - } private boolean hasLauncherClasses(File file) throws IOException { @@ -354,9 +362,13 @@ public class RepackagerTests { } private boolean hasEntry(File file, String name) throws IOException { + return getEntry(file, name) != null; + } + + private JarEntry getEntry(File file, String name) throws IOException { JarFile jarFile = new JarFile(file); try { - return jarFile.getEntry(name) != null; + return jarFile.getJarEntry(name); } finally { jarFile.close(); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index f55aff6821b..4a3a944d212 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -17,7 +17,10 @@ package org.springframework.boot.loader.archive; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -27,6 +30,7 @@ import java.util.List; import java.util.jar.JarEntry; import java.util.jar.Manifest; +import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess; import org.springframework.boot.loader.jar.JarEntryData; import org.springframework.boot.loader.jar.JarEntryFilter; import org.springframework.boot.loader.jar.JarFile; @@ -39,12 +43,23 @@ import org.springframework.boot.loader.util.AsciiBytes; */ public class JarFileArchive extends Archive { + private static final AsciiBytes UNPACK_MARKER = new AsciiBytes("UNPACK:"); + + private static final int BUFFER_SIZE = 32 * 1024; + private final JarFile jarFile; private final List entries; + private URL url; + public JarFileArchive(File file) throws IOException { + this(file, null); + } + + public JarFileArchive(File file, URL url) throws IOException { this(new JarFile(file)); + this.url = url; } public JarFileArchive(JarFile jarFile) { @@ -58,6 +73,9 @@ public class JarFileArchive extends Archive { @Override public URL getUrl() throws MalformedURLException { + if (this.url != null) { + return this.url; + } return this.jarFile.getUrl(); } @@ -84,10 +102,54 @@ public class JarFileArchive extends Archive { protected Archive getNestedArchive(Entry entry) throws IOException { JarEntryData data = ((JarFileEntry) entry).getJarEntryData(); + if (data.getComment().startsWith(UNPACK_MARKER)) { + return getUnpackedNestedArchive(data); + } JarFile jarFile = this.jarFile.getNestedJarFile(data); return new JarFileArchive(jarFile); } + private Archive getUnpackedNestedArchive(JarEntryData data) throws IOException { + AsciiBytes hash = data.getComment().substring(UNPACK_MARKER.length()); + String name = data.getName().toString(); + if (name.lastIndexOf("/") != -1) { + name = name.substring(name.lastIndexOf("/") + 1); + } + File file = new File(getTempUnpackFolder(), hash.toString() + "-" + name); + if (!file.exists() || file.length() != data.getSize()) { + unpack(data, file); + } + return new JarFileArchive(file, file.toURI().toURL()); + } + + private File getTempUnpackFolder() { + File tempFolder = new File(System.getProperty("java.io.tmpdir")); + File unpackFolder = new File(tempFolder, "spring-boot-libs"); + unpackFolder.mkdirs(); + return unpackFolder; + } + + private void unpack(JarEntryData data, File file) throws IOException { + InputStream inputStream = data.getData().getInputStream(ResourceAccess.ONCE); + try { + OutputStream outputStream = new FileOutputStream(file); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } + finally { + outputStream.close(); + } + } + finally { + inputStream.close(); + } + } + @Override public Archive getFilteredArchive(final EntryRenameFilter filter) throws IOException { JarFile filteredJar = this.jarFile.getFilteredJarFile(new JarEntryFilter() { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 0a32e26ce03..a9eeb8b6463 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -96,10 +96,11 @@ public class Handler extends URLStreamHandler { return openConnection(getFallbackHandler(), url); } catch (Exception ex) { - this.logger.log(Level.WARNING, "Unable to open fallback handler", ex); if (reason instanceof IOException) { + this.logger.log(Level.FINEST, "Unable to open fallback handler", ex); throw (IOException) reason; } + this.logger.log(Level.WARNING, "Unable to open fallback handler", ex); if (reason instanceof RuntimeException) { throw (RuntimeException) reason; } @@ -111,7 +112,6 @@ public class Handler extends URLStreamHandler { if (this.fallbackHandler != null) { return this.fallbackHandler; } - for (String handlerClassName : FALLBACK_HANDLERS) { try { Class handlerClass = Class.forName(handlerClassName); diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java index 1a336d93eb3..9f0bca4801f 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryData.java @@ -93,7 +93,13 @@ public final class JarEntryData { return inputStream; } - RandomAccessData getData() throws IOException { + /** + * @return the underlying {@link RandomAccessData} for this entry. Generally this + * method should not be called directly and instead data should be accessed via + * {@link JarFile#getInputStream(ZipEntry)}. + * @throws IOException + */ + public RandomAccessData getData() throws IOException { if (this.data == null) { // aspectjrt-1.7.4.jar has a different ext bytes length in the // local directory to the central directory. We need to re-read diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 2c100efbf93..f113f6d0ab2 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -35,6 +35,10 @@ import java.util.zip.ZipEntry; public abstract class TestJarCreator { public static void createTestJar(File file) throws Exception { + createTestJar(file, false); + } + + public static void createTestJar(File file, boolean unpackNested) throws Exception { FileOutputStream fileOutputStream = new FileOutputStream(file); JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream); try { @@ -50,6 +54,9 @@ public abstract class TestJarCreator { byte[] nestedJarData = getNestedJarData(); nestedEntry.setSize(nestedJarData.length); nestedEntry.setCompressedSize(nestedJarData.length); + if (unpackNested) { + nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000"); + } CRC32 crc32 = new CRC32(); crc32.update(nestedJarData); nestedEntry.setCrc(crc32.getValue()); diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index 7bd767dcf4e..ff8f7e1a38e 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -29,7 +29,9 @@ import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.archive.Archive.Entry; import org.springframework.boot.loader.util.AsciiBytes; +import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThat; /** @@ -48,8 +50,12 @@ public class JarFileArchiveTests { @Before public void setup() throws Exception { + setup(false); + } + + private void setup(boolean unpackNested) throws Exception { this.rootJarFile = this.temporaryFolder.newFile(); - TestJarCreator.createTestJar(this.rootJarFile); + TestJarCreator.createTestJar(this.rootJarFile, unpackNested); this.archive = new JarFileArchive(this.rootJarFile); } @@ -80,6 +86,15 @@ public class JarFileArchiveTests { equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/")); } + @Test + public void getNestedUnpackedArchive() throws Exception { + setup(true); + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + Archive nested = this.archive.getNestedArchive(entry); + assertThat(nested.getUrl().toString(), startsWith("file:")); + assertThat(nested.getUrl().toString(), endsWith(".jar")); + } + @Test public void getFilteredArchive() throws Exception { Archive filteredArchive = this.archive diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml new file mode 100644 index 00000000000..ae5eeffbb83 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-unpack + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + org.springframework + spring-core + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + Foo + + + + + + + + + org.springframework + spring-context + 4.0.5.RELEASE + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + log4j + log4j + 1.2.17 + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..0b3b431677d --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,8 @@ +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy new file mode 100644 index 00000000000..9abbeb593d7 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2014 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 + * + * 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. + */ + +import java.io.*; +import org.springframework.boot.maven.*; + +File f = new File( basedir, "target/jar-with-unpack-0.0.1.BUILD-SNAPSHOT.jar"); +new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { + @Override + protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { + super.verifyZipEntries(verifier) + verifier.hasUnpackEntry("lib/spring-core-4.0.5.RELEASE.jar") + verifier.hasNonUnpackEntry("lib/spring-context-4.0.5.RELEASE.jar") + } +}.verify(); diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java index e32ea54d204..56d027faa42 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java @@ -17,12 +17,14 @@ package org.springframework.boot.maven; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Dependency; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; @@ -47,8 +49,11 @@ public class ArtifactsLibraries implements Libraries { private final Set artifacts; - public ArtifactsLibraries(Set artifacts) { + private final Collection unpacks; + + public ArtifactsLibraries(Set artifacts, Collection unpacks) { this.artifacts = artifacts; + this.unpacks = unpacks; } @Override @@ -56,8 +61,21 @@ public class ArtifactsLibraries implements Libraries { for (Artifact artifact : this.artifacts) { LibraryScope scope = SCOPES.get(artifact.getScope()); if (scope != null && artifact.getFile() != null) { - callback.library(new Library(artifact.getFile(), scope)); + callback.library(new Library(artifact.getFile(), scope, + isUnpackRequired(artifact))); + } + } + } + + private boolean isUnpackRequired(Artifact artifact) { + if (this.unpacks != null) { + for (Dependency unpack : this.unpacks) { + if (artifact.getGroupId().equals(unpack.getGroupId()) + && artifact.getArtifactId().equals(unpack.getArtifactId())) { + return true; + } } } + return false; } } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 8d45615272b..4d80c9f940e 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -18,11 +18,13 @@ package org.springframework.boot.maven; import java.io.File; import java.io.IOException; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.jar.JarFile; import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; @@ -108,6 +110,13 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { @Parameter private LayoutType layout; + /** + * A list of the libraries that must be unpacked from fat jars in order to run. + * @since 1.1 + */ + @Parameter + private List requiresUnpack; + @Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.project.getPackaging().equals("pom")) { @@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters()); - Libraries libraries = new ArtifactsLibraries(artifacts); + Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack); try { repackager.repackage(target, libraries); } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java index 9eaf7271d1c..15c05f33f3e 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Set; import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Dependency; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -62,7 +63,7 @@ public class ArtifactsLibrariesTests { public void setup() { MockitoAnnotations.initMocks(this); this.artifacts = Collections.singleton(this.artifact); - this.libs = new ArtifactsLibraries(this.artifacts); + this.libs = new ArtifactsLibraries(this.artifacts, null); given(this.artifact.getFile()).willReturn(this.file); } @@ -75,6 +76,21 @@ public class ArtifactsLibrariesTests { Library library = this.libraryCaptor.getValue(); assertThat(library.getFile(), equalTo(this.file)); assertThat(library.getScope(), equalTo(LibraryScope.COMPILE)); + assertThat(library.isUnpackRequired(), equalTo(false)); } + @Test + public void callbackWithUnpack() throws Exception { + given(this.artifact.getGroupId()).willReturn("gid"); + given(this.artifact.getArtifactId()).willReturn("aid"); + given(this.artifact.getType()).willReturn("jar"); + given(this.artifact.getScope()).willReturn("compile"); + Dependency unpack = new Dependency(); + unpack.setGroupId("gid"); + unpack.setArtifactId("aid"); + this.libs = new ArtifactsLibraries(this.artifacts, Collections.singleton(unpack)); + this.libs.doWithLibraries(this.callback); + verify(this.callback).library(this.libraryCaptor.capture()); + assertThat(this.libraryCaptor.getValue().isUnpackRequired(), equalTo(true)); + } } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java index f93218cc595..84e11f692ac 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java @@ -87,6 +87,15 @@ public class Verify { } } + public boolean hasNonUnpackEntry(String entry) { + return !hasUnpackEntry(entry); + } + + public boolean hasUnpackEntry(String entry) { + String comment = this.content.get(entry).getComment(); + return comment != null && comment.startsWith("UNPACK:"); + } + public boolean hasEntry(String entry) { return this.content.containsKey(entry); }