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);
}