Skip to content

Commit f30b962

Browse files
author
Phillip Webb
committed
Add support for unpacking nested JARs
Update the executable JAR code to automatically unpack any entries which include an entry comment starting `UNPACK:` to the temp folder. The existing Maven and Gradle plugins have been updated with new configuration options and the `spring-boot-tools` project has been updated to write the appropriate entry comment based on a flag passed in via the `Library` class. This support has been added to allow libraries such a JRuby (which assumes that `jruby-complete.jar` is always accessible as file) to work with Spring Boot executable jars. Fixes gh-1070
1 parent 5f8fbfd commit f30b962

File tree

24 files changed

+477
-39
lines changed

24 files changed

+477
-39
lines changed

spring-boot-cli/src/main/java/org/springframework/boot/cli/command/jar/JarCommand.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import org.springframework.boot.loader.tools.JarWriter;
5757
import org.springframework.boot.loader.tools.Layout;
5858
import org.springframework.boot.loader.tools.Layouts;
59+
import org.springframework.boot.loader.tools.Library;
60+
import org.springframework.boot.loader.tools.LibraryScope;
5961
import org.springframework.core.io.Resource;
6062
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
6163
import org.springframework.util.Assert;
@@ -248,7 +250,8 @@ private void addDependencies(JarWriter writer, List<URL> urls)
248250
private void addDependency(JarWriter writer, File dependency)
249251
throws FileNotFoundException, IOException {
250252
if (dependency.isFile()) {
251-
writer.writeNestedLibrary("lib/", dependency);
253+
writer.writeNestedLibrary("lib/", new Library(dependency,
254+
LibraryScope.COMPILE));
252255
}
253256
}
254257

spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ The following configuration options are available:
511511
|`layout`
512512
|The type of archive, corresponding to how the dependencies are laid out inside
513513
(defaults to a guess based on the archive type).
514+
515+
|`requiresUnpack`
516+
|A list of dependencies (in the form ``groupId:artifactId'' that must be unpacked from
517+
fat jars in order to run. Items are still packaged into the fat jar, but they will be
518+
automatically unpacked when it runs.
514519
|===
515520

516521

spring-boot-docs/src/main/asciidoc/howto.adoc

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,50 @@ For Gradle users the steps are similar. Example:
16181618

16191619

16201620

1621+
[[howto-extract-specific-libraries-when-an-executable-jar-runs]]
1622+
=== Extract specific libraries when an executable jar runs
1623+
Most nested libraries in an executable jar do not need to be unpacked in order to run,
1624+
however, certain libraries can have problems. For example, JRuby includes its own nested
1625+
jar support which assumes that the `jruby-complete.jar` is always directly available as a
1626+
file in its own right.
1627+
1628+
To deal with any problematic libraries, you can flag that specific nested jars should be
1629+
automatically unpacked to the ``temp folder'' when the executable jar first runs.
1630+
1631+
For example, to indicate that JRuby should be flagged for unpack using the Maven Plugin
1632+
you would add the following configuration:
1633+
1634+
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
1635+
----
1636+
<build>
1637+
<plugins>
1638+
<plugin>
1639+
<groupId>org.springframework.boot</groupId>
1640+
<artifactId>spring-boot-maven-plugin</artifactId>
1641+
<configuration>
1642+
<requiresUnpack>
1643+
<dependency>
1644+
<groupId>org.jruby</groupId>
1645+
<artifactId>jruby-complete</artifactId>
1646+
</dependency>
1647+
</requiresUnpack>
1648+
</configuration>
1649+
</plugin>
1650+
</plugins>
1651+
</build>
1652+
----
1653+
1654+
And to do that same with Gradle:
1655+
1656+
[source,groovy,indent=0,subs="verbatim,attributes"]
1657+
----
1658+
springBoot {
1659+
requiresUnpack = ['org.jruby:jruby-complete']
1660+
}
1661+
----
1662+
1663+
1664+
16211665
[[howto-create-a-nonexecutable-jar]]
16221666
=== Create a non-executable JAR with exclusions
16231667
Often if you have an executable and a non-executable jar as build products, the executable

spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/SpringBootPluginExtension.groovy

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ public class SpringBootPluginExtension {
107107
(layout == null ? null : layout.layout)
108108
}
109109

110+
/**
111+
* Libraries that must be unpacked from fat jars in order to run. Use Strings in the
112+
* form {@literal groupId:artifactId}.
113+
*/
114+
Set<String> requiresUnpack;
115+
110116
/**
111117
* Location of an agent jar to attach to the VM when running the application with runJar task.
112118
*/
@@ -121,4 +127,5 @@ public class SpringBootPluginExtension {
121127
* If exclude rules should be applied to dependencies based on the spring-dependencies-bom
122128
*/
123129
boolean applyExcludeRules = true;
130+
124131
}

spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/ProjectLibraries.java

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.util.HashSet;
22+
import java.util.LinkedHashSet;
23+
import java.util.Set;
2124

2225
import org.gradle.api.Project;
2326
import org.gradle.api.artifacts.Configuration;
24-
import org.gradle.api.file.FileCollection;
27+
import org.gradle.api.artifacts.ModuleVersionIdentifier;
28+
import org.gradle.api.artifacts.ResolvedArtifact;
29+
import org.springframework.boot.gradle.SpringBootPluginExtension;
2530
import org.springframework.boot.loader.tools.Libraries;
2631
import org.springframework.boot.loader.tools.Library;
2732
import org.springframework.boot.loader.tools.LibraryCallback;
@@ -37,22 +42,24 @@ class ProjectLibraries implements Libraries {
3742

3843
private final Project project;
3944

45+
private final SpringBootPluginExtension extension;
46+
4047
private String providedConfigurationName = "providedRuntime";
4148

4249
private String customConfigurationName = null;
4350

4451
/**
4552
* Create a new {@link ProjectLibraries} instance of the specified {@link Project}.
46-
*
4753
* @param project the gradle project
54+
* @param extension the extension
4855
*/
49-
public ProjectLibraries(Project project) {
56+
public ProjectLibraries(Project project, SpringBootPluginExtension extension) {
5057
this.project = project;
58+
this.extension = extension;
5159
}
5260

5361
/**
5462
* Set the name of the provided configuration. Defaults to 'providedRuntime'.
55-
*
5663
* @param providedConfigurationName the providedConfigurationName to set
5764
*/
5865
public void setProvidedConfigurationName(String providedConfigurationName) {
@@ -65,27 +72,20 @@ public void setCustomConfigurationName(String customConfigurationName) {
6572

6673
@Override
6774
public void doWithLibraries(LibraryCallback callback) throws IOException {
68-
69-
FileCollection custom = this.customConfigurationName != null ? this.project
70-
.getConfigurations().findByName(this.customConfigurationName) : null;
71-
75+
Set<ResolvedArtifact> custom = getArtifacts(this.customConfigurationName);
7276
if (custom != null) {
7377
libraries(LibraryScope.CUSTOM, custom, callback);
7478
}
7579
else {
76-
FileCollection compile = this.project.getConfigurations()
77-
.getByName("compile");
78-
79-
FileCollection runtime = this.project.getConfigurations()
80-
.getByName("runtime");
81-
runtime = runtime.minus(compile);
80+
Set<ResolvedArtifact> compile = getArtifacts("compile");
8281

83-
FileCollection provided = this.project.getConfigurations()
84-
.findByName(this.providedConfigurationName);
82+
Set<ResolvedArtifact> runtime = getArtifacts("runtime");
83+
runtime = minus(runtime, compile);
8584

85+
Set<ResolvedArtifact> provided = getArtifacts(this.providedConfigurationName);
8686
if (provided != null) {
87-
compile = compile.minus(provided);
88-
runtime = runtime.minus(provided);
87+
compile = minus(compile, provided);
88+
runtime = minus(runtime, provided);
8989
}
9090

9191
libraries(LibraryScope.COMPILE, compile, callback);
@@ -94,12 +94,47 @@ public void doWithLibraries(LibraryCallback callback) throws IOException {
9494
}
9595
}
9696

97-
private void libraries(LibraryScope scope, FileCollection files,
97+
private Set<ResolvedArtifact> getArtifacts(String configurationName) {
98+
Configuration configuration = (configurationName == null ? null : this.project
99+
.getConfigurations().findByName(configurationName));
100+
return (configuration == null ? null : configuration.getResolvedConfiguration()
101+
.getResolvedArtifacts());
102+
}
103+
104+
private Set<ResolvedArtifact> minus(Set<ResolvedArtifact> source,
105+
Set<ResolvedArtifact> toRemove) {
106+
if (source == null || toRemove == null) {
107+
return source;
108+
}
109+
Set<File> filesToRemove = new HashSet<File>();
110+
for (ResolvedArtifact artifact : toRemove) {
111+
filesToRemove.add(artifact.getFile());
112+
}
113+
Set<ResolvedArtifact> result = new LinkedHashSet<ResolvedArtifact>();
114+
for (ResolvedArtifact artifact : source) {
115+
if (!toRemove.contains(artifact.getFile())) {
116+
result.add(artifact);
117+
}
118+
}
119+
return result;
120+
}
121+
122+
private void libraries(LibraryScope scope, Set<ResolvedArtifact> artifacts,
98123
LibraryCallback callback) throws IOException {
99-
if (files != null) {
100-
for (File file: files) {
101-
callback.library(new Library(file, scope));
124+
if (artifacts != null) {
125+
for (ResolvedArtifact artifact : artifacts) {
126+
callback.library(new Library(artifact.getFile(), scope, isUnpackRequired(artifact)));
102127
}
103128
}
104129
}
130+
131+
private boolean isUnpackRequired(ResolvedArtifact artifact) {
132+
if (this.extension.getRequiresUnpack() != null) {
133+
ModuleVersionIdentifier id = artifact.getModuleVersion().getId();
134+
return this.extension.getRequiresUnpack().contains(
135+
id.getGroup() + ":" + id.getName());
136+
}
137+
return false;
138+
}
139+
105140
}

spring-boot-tools/spring-boot-gradle-plugin/src/main/groovy/org/springframework/boot/gradle/repackage/RepackageTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public ProjectLibraries getLibraries() {
101101
Project project = getProject();
102102
SpringBootPluginExtension extension = project.getExtensions().getByType(
103103
SpringBootPluginExtension.class);
104-
ProjectLibraries libraries = new ProjectLibraries(project);
104+
ProjectLibraries libraries = new ProjectLibraries(project, extension);
105105
if (extension.getProvidedConfiguration() != null) {
106106
libraries.setProvidedConfigurationName(extension.getProvidedConfiguration());
107107
}

spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@
1717
package org.springframework.boot.loader.tools;
1818

1919
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.IOException;
22+
import java.security.DigestInputStream;
23+
import java.security.MessageDigest;
24+
import java.security.NoSuchAlgorithmException;
2025

2126
/**
2227
* Utilities for manipulating files and directories in Spring Boot tooling.
2328
*
2429
* @author Dave Syer
30+
* @author Phillip Webb
2531
*/
26-
public class FileUtils {
32+
public abstract class FileUtils {
2733

2834
/**
2935
* Utility to remove duplicate files from an "output" directory if they already exist
@@ -50,4 +56,37 @@ public static void removeDuplicatesFromOutputDirectory(File outputDirectory,
5056
}
5157
}
5258

59+
/**
60+
* Generate a SHA.1 Hash for a given file.
61+
* @param file the file to hash
62+
* @return the hash value as a String
63+
* @throws IOException
64+
*/
65+
public static String sha1Hash(File file) throws IOException {
66+
try {
67+
DigestInputStream inputStream = new DigestInputStream(new FileInputStream(
68+
file), MessageDigest.getInstance("SHA-1"));
69+
try {
70+
byte[] buffer = new byte[4098];
71+
while (inputStream.read(buffer) != -1) {
72+
// Read the entire stream
73+
}
74+
return bytesToHex(inputStream.getMessageDigest().digest());
75+
}
76+
finally {
77+
inputStream.close();
78+
}
79+
}
80+
catch (NoSuchAlgorithmException ex) {
81+
throw new IllegalStateException(ex);
82+
}
83+
}
84+
85+
private static String bytesToHex(byte[] bytes) {
86+
StringBuilder hex = new StringBuilder();
87+
for (byte b : bytes) {
88+
hex.append(String.format("%02x", b));
89+
}
90+
return hex.toString();
91+
}
5392
}

spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/JarWriter.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class JarWriter {
5050

5151
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
5252

53-
private static final int BUFFER_SIZE = 4096;
53+
private static final int BUFFER_SIZE = 32 * 1024;
5454

5555
private final JarOutputStream jarOutput;
5656

@@ -122,11 +122,16 @@ public void writeEntry(String entryName, InputStream inputStream) throws IOExcep
122122
/**
123123
* Write a nested library.
124124
* @param destination the destination of the library
125-
* @param file the library file
125+
* @param library the library
126126
* @throws IOException if the write fails
127127
*/
128-
public void writeNestedLibrary(String destination, File file) throws IOException {
128+
public void writeNestedLibrary(String destination, Library library)
129+
throws IOException {
130+
File file = library.getFile();
129131
JarEntry entry = new JarEntry(destination + file.getName());
132+
if (library.isUnpackRequired()) {
133+
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
134+
}
130135
new CrcAndSize(file).setupStoredEntry(entry);
131136
writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true));
132137
}

spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,27 @@ public class Library {
3131

3232
private final LibraryScope scope;
3333

34+
private final boolean unpackRequired;
35+
3436
/**
3537
* Create a new {@link Library}.
3638
* @param file the source file
3739
* @param scope the scope of the library
3840
*/
3941
public Library(File file, LibraryScope scope) {
42+
this(file, scope, false);
43+
}
44+
45+
/**
46+
* Create a new {@link Library}.
47+
* @param file the source file
48+
* @param scope the scope of the library
49+
* @param unpackRequired if the library needs to be unpacked before it can be used
50+
*/
51+
public Library(File file, LibraryScope scope, boolean unpackRequired) {
4052
this.file = file;
4153
this.scope = scope;
54+
this.unpackRequired = unpackRequired;
4255
}
4356

4457
/**
@@ -55,4 +68,12 @@ public LibraryScope getScope() {
5568
return this.scope;
5669
}
5770

71+
/**
72+
* @return if the file cannot be used directly as a nested jar and needs to be
73+
* unpacked.
74+
*/
75+
public boolean isUnpackRequired() {
76+
return this.unpackRequired;
77+
}
78+
5879
}

spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ public void library(Library library) throws IOException {
147147
String destination = Repackager.this.layout
148148
.getLibraryDestination(file.getName(), library.getScope());
149149
if (destination != null) {
150-
writer.writeNestedLibrary(destination, file);
150+
writer.writeNestedLibrary(destination, library);
151151
}
152152
}
153153
}

0 commit comments

Comments
 (0)