Introduce bootJar and bootWar tasks for creating fat jars and wars

Previously, the BootRepackage task would take the output of a Jar
or War task and repackage it in a similar manner to Spring Boot's
Maven plugin. This caused several problems in Gradle including
broken up-to-date checks and a lack of configurability. See the issues
referenced below for full details.

This commit replaces BootRepackage with BootJar and BootWar
for building executable jars and wars respectively. BootJar extends
Gradle's standard Jar task and BootWar extends Gradle's standard War
task. This means that terms of configuration, the creation of
executable jars and wars is now as flexible as the creation of
standards jars and wars.

Closes gh-8167
Closes gh-8099
Closes gh-6846
Closes gh-5861
Closes gh-5393
Closes gh-5259
Closes gh-3931
This commit is contained in:
Andy Wilkinson
2017-03-02 16:46:27 +00:00
parent 20fe95b276
commit 2ce8556976
22 changed files with 1555 additions and 832 deletions

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
import java.io.IOException;
import org.gradle.testkit.runner.InvalidRunnerConfigurationException;
import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link BootJar}.
*
* @author Andy Wilkinson
*/
public abstract class AbstractBootArchiveIntegrationTests {
@Rule
public final GradleBuild gradleBuild = new GradleBuild();
private final String taskName;
protected AbstractBootArchiveIntegrationTests(String taskName) {
this.taskName = taskName;
}
@Test
public void basicBuild() throws InvalidRunnerConfigurationException,
UnexpectedBuildFailure, IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@Test
public void upToDateWhenBuiltTwice() throws InvalidRunnerConfigurationException,
UnexpectedBuildFailure, IOException {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE);
}
@Test
public void upToDateWhenBuiltTwiceWithLaunchScriptIncluded()
throws InvalidRunnerConfigurationException, UnexpectedBuildFailure,
IOException {
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName)
.task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.UP_TO_DATE);
}
@Test
public void notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded() {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@Test
public void notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded() {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName)
.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@Test
public void notUpToDateWhenLaunchScriptPropertyChanges() {
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true",
"-PlaunchScriptProperty=foo", this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.build("-PincludeLaunchScript=true",
"-PlaunchScriptProperty=bar", this.taskName).task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.jar.JarFile;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.testfixtures.ProjectBuilder;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.tools.DefaultLaunchScript;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for testing {@link BootArchive} implementations.
*
* @param <T> the type of the concrete BootArchive implementation
* @author Andy Wilkinson
*/
public abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
@Rule
public final TemporaryFolder temp = new TemporaryFolder();
private final Class<T> taskClass;
private final String launcherClass;
private final String libPath;
private final String classesPath;
private T task;
protected AbstractBootArchiveTests(Class<T> taskClass, String launcherClass,
String libPath, String classesPath) {
this.taskClass = taskClass;
this.launcherClass = launcherClass;
this.libPath = libPath;
this.classesPath = classesPath;
}
@Before
public void createTask() {
try {
Project project = ProjectBuilder.builder()
.withProjectDir(this.temp.newFolder()).build();
this.task = configure(
project.getTasks().create("testArchive", this.taskClass));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Test
public void basicArchiveCreation() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.execute();
assertThat(this.task.getArchivePath().exists());
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class"))
.isEqualTo(this.launcherClass);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class"))
.isEqualTo("com.example.Main");
}
}
@Test
public void classpathJarsArePackagedBeneathLibPath() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar"));
this.task.execute();
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getEntry(this.libPath + "/one.jar")).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "/two.jar")).isNotNull();
}
}
@Test
public void classpathFoldersArePackagedBeneathClassesPath() throws IOException {
this.task.setMainClass("com.example.Main");
File classpathFolder = this.temp.newFolder();
File applicationClass = new File(classpathFolder,
"com/example/Application.class");
applicationClass.getParentFile().mkdirs();
applicationClass.createNewFile();
this.task.classpath(classpathFolder);
this.task.execute();
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(
jarFile.getEntry(this.classesPath + "/com/example/Application.class"))
.isNotNull();
}
}
@Test
public void loaderIsWrittenToTheRootOfTheJar() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.execute();
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getEntry(
"org/springframework/boot/loader/LaunchedURLClassLoader.class"))
.isNotNull();
}
}
@Test
public void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar"));
this.task.requiresUnpack("**/one.jar");
this.task.execute();
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getEntry(this.libPath + "/one.jar").getComment())
.startsWith("UNPACK:");
assertThat(jarFile.getEntry(this.libPath + "/two.jar").getComment()).isNull();
}
}
@Test
public void unpackCommentIsAddedToEntryIdentifiedByASpec() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.classpath(this.temp.newFile("one.jar"), this.temp.newFile("two.jar"));
this.task.requiresUnpack((element) -> element.getName().endsWith("two.jar"));
this.task.execute();
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getEntry(this.libPath + "/two.jar").getComment())
.startsWith("UNPACK:");
assertThat(jarFile.getEntry(this.libPath + "/one.jar").getComment()).isNull();
}
}
@Test
public void launchScriptCanBePrepended() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.getLaunchScript().setIncluded(true);
this.task.execute();
assertThat(Files.readAllBytes(this.task.getArchivePath().toPath()))
.startsWith(new DefaultLaunchScript(null, null).toByteArray());
}
@Test
public void customLaunchScriptCanBePrepended() throws IOException {
this.task.setMainClass("com.example.Main");
LaunchScriptConfiguration launchScript = this.task.getLaunchScript();
launchScript.setIncluded(true);
File customScript = this.temp.newFile("custom.script");
Files.write(customScript.toPath(), Arrays.asList("custom script"),
StandardOpenOption.CREATE);
launchScript.setScript(customScript);
this.task.execute();
assertThat(Files.readAllBytes(this.task.getArchivePath().toPath()))
.startsWith("custom script".getBytes());
}
@Test
public void launchScriptPropertiesAreReplaced() throws IOException {
this.task.setMainClass("com.example.Main");
LaunchScriptConfiguration launchScript = this.task.getLaunchScript();
launchScript.setIncluded(true);
launchScript.getProperties().put("initInfoProvides", "test property value");
this.task.execute();
assertThat(Files.readAllBytes(this.task.getArchivePath().toPath()))
.containsSequence("test property value".getBytes());
}
@Test
public void customMainClassInTheManifestIsHonored() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.getManifest().getAttributes().put("Main-Class",
"com.example.CustomLauncher");
this.task.execute();
assertThat(this.task.getArchivePath().exists());
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class"))
.isEqualTo("com.example.CustomLauncher");
assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class"))
.isEqualTo("com.example.Main");
}
}
@Test
public void customStartClassInTheManifestIsHonored() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.getManifest().getAttributes().put("Start-Class",
"com.example.CustomMain");
this.task.execute();
assertThat(this.task.getArchivePath().exists());
try (JarFile jarFile = new JarFile(this.task.getArchivePath())) {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class"))
.isEqualTo(this.launcherClass);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class"))
.isEqualTo("com.example.CustomMain");
}
}
private T configure(T task) throws IOException {
AbstractArchiveTask archiveTask = task;
archiveTask.setBaseName("test");
archiveTask.setDestinationDir(this.temp.newFolder());
return task;
}
protected T getTask() {
return this.task;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
/**
* Integration tests for {@link BootJar}.
*
* @author Andy Wilkinson
*/
public class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
public BootJarIntegrationTests() {
super("bootJar");
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
/**
* Tests for {@link BootJar}.
*
* @author Andy Wilkinson
*/
public class BootJarTests extends AbstractBootArchiveTests<BootJar> {
public BootJarTests() {
super(BootJar.class, "org.springframework.boot.loader.JarLauncher",
"BOOT-INF/lib", "BOOT-INF/classes");
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
/**
* Integration tests for {@link BootJar}.
*
* @author Andy Wilkinson
*/
public class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests {
public BootWarIntegrationTests() {
super("bootWar");
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.bundling;
import java.io.IOException;
import java.util.jar.JarFile;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BootWar}.
*
* @author Andy Wilkinson
*/
public class BootWarTests extends AbstractBootArchiveTests<BootWar> {
public BootWarTests() {
super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib",
"WEB-INF/classes");
}
@Test
public void providedClasspathJarsArePackagedInWebInfLibProvided() throws IOException {
getTask().setMainClass("com.example.Main");
getTask().providedClasspath(this.temp.newFile("one.jar"),
this.temp.newFile("two.jar"));
getTask().execute();
try (JarFile jarFile = new JarFile(getTask().getArchivePath())) {
assertThat(jarFile.getEntry("WEB-INF/lib-provided/one.jar")).isNotNull();
assertThat(jarFile.getEntry("WEB-INF/lib-provided/two.jar")).isNotNull();
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012-2017 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.
*/
package org.springframework.boot.gradle.testkit;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.spring.gradle.dependencymanagement.DependencyManagementPlugin;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.loader.tools.LaunchScript;
/**
* A {@link TestRule} for running a Gradle build using {@link GradleRunner}.
*
* @author Andy Wilkinson
*/
public class GradleBuild implements TestRule {
private final TemporaryFolder temp = new TemporaryFolder();
private File projectDir;
private String script;
@Override
public Statement apply(Statement base, Description description) {
String name = description.getTestClass().getSimpleName() + ".gradle";
URL scriptUrl = description.getTestClass().getResource(name);
if (scriptUrl != null) {
script(scriptUrl.getFile());
}
return this.temp.apply(new Statement() {
@Override
public void evaluate() throws Throwable {
before();
try {
base.evaluate();
}
finally {
after();
}
}
}, description);
}
private void before() throws IOException {
this.projectDir = this.temp.newFolder();
}
private void after() {
GradleBuild.this.script = null;
}
private String pluginClasspath() {
return new File("build/classes/main").getAbsolutePath() + ","
+ new File("build/resources/main").getAbsolutePath() + ","
+ LaunchScript.class.getProtectionDomain().getCodeSource().getLocation()
.getPath()
+ "," + DependencyManagementPlugin.class.getProtectionDomain()
.getCodeSource().getLocation().getPath();
}
public GradleBuild script(String script) {
this.script = script;
return this;
}
public BuildResult build(String... arguments) {
try {
Files.copy(new File(this.script).toPath(),
new File(this.projectDir, "build.gradle").toPath(),
StandardCopyOption.REPLACE_EXISTING);
GradleRunner gradleRunner = GradleRunner.create()
.withProjectDir(this.projectDir).forwardOutput();
List<String> allArguments = new ArrayList<String>();
allArguments.add("-PpluginClasspath=" + pluginClasspath());
allArguments.addAll(Arrays.asList(arguments));
return gradleRunner.withArguments(allArguments).build();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}