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
This commit is contained in:
Phillip Webb 2014-06-24 00:35:01 -07:00
parent 5f8fbfd73a
commit f30b962ff9
24 changed files with 477 additions and 39 deletions

View File

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

View File

@ -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.
|===

View File

@ -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"]
----
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<requiresUnpack>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</plugin>
</plugins>
</build>
----
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

View File

@ -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<String> 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;
}

View File

@ -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<ResolvedArtifact> custom = getArtifacts(this.customConfigurationName);
if (custom != null) {
libraries(LibraryScope.CUSTOM, custom, callback);
}
else {
FileCollection compile = this.project.getConfigurations()
.getByName("compile");
Set<ResolvedArtifact> compile = getArtifacts("compile");
FileCollection runtime = this.project.getConfigurations()
.getByName("runtime");
runtime = runtime.minus(compile);
FileCollection provided = this.project.getConfigurations()
.findByName(this.providedConfigurationName);
Set<ResolvedArtifact> runtime = getArtifacts("runtime");
runtime = minus(runtime, compile);
Set<ResolvedArtifact> 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<ResolvedArtifact> getArtifacts(String configurationName) {
Configuration configuration = (configurationName == null ? null : this.project
.getConfigurations().findByName(configurationName));
return (configuration == null ? null : configuration.getResolvedConfiguration()
.getResolvedArtifacts());
}
private Set<ResolvedArtifact> minus(Set<ResolvedArtifact> source,
Set<ResolvedArtifact> toRemove) {
if (source == null || toRemove == null) {
return source;
}
Set<File> filesToRemove = new HashSet<File>();
for (ResolvedArtifact artifact : toRemove) {
filesToRemove.add(artifact.getFile());
}
Set<ResolvedArtifact> result = new LinkedHashSet<ResolvedArtifact>();
for (ResolvedArtifact artifact : source) {
if (!toRemove.contains(artifact.getFile())) {
result.add(artifact);
}
}
return result;
}
private void libraries(LibraryScope scope, Set<ResolvedArtifact> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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<Entry> 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() {

View File

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

View File

@ -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

View File

@ -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());

View File

@ -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

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-with-unpack</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<requiresUnpack>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestEntries>
<Not-Used>Foo</Not-Used>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,8 @@
package org.test;
public class SampleApplication {
public static void main(String[] args) {
}
}

View File

@ -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();

View File

@ -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<Artifact> artifacts;
public ArtifactsLibraries(Set<Artifact> artifacts) {
private final Collection<Dependency> unpacks;
public ArtifactsLibraries(Set<Artifact> artifacts, Collection<Dependency> 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;
}
}

View File

@ -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<Dependency> requiresUnpack;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (this.project.getPackaging().equals("pom")) {
@ -144,7 +153,7 @@ public class RepackageMojo extends AbstractDependencyFilterMojo {
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
getFilters());
Libraries libraries = new ArtifactsLibraries(artifacts);
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack);
try {
repackager.repackage(target, libraries);
}

View File

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

View File

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