Port the build to Gradle

Closes gh-19609
Closes gh-19608
This commit is contained in:
Andy Wilkinson
2020-01-10 13:48:43 +00:00
parent abe95fa8a7
commit ce99db1902
974 changed files with 17108 additions and 26596 deletions

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2019 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
*
* https://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 com.example;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ControllerOne {
@RequestMapping("/one")
public String one() {
return "one";
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2019 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
*
* https://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 com.example;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.context.WebServerPortFileWriter;
@SpringBootApplication
public class DevToolsTestApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DevToolsTestApplication.class).listeners(new WebServerPortFileWriter(args[0]))
.run(args);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.FileSystemUtils;
/**
* Base class for all {@link ApplicationLauncher} implementations.
*
* @author Andy Wilkinson
*/
abstract class AbstractApplicationLauncher implements ApplicationLauncher {
private final Directories directories;
AbstractApplicationLauncher(Directories directories) {
this.directories = directories;
}
protected final void copyApplicationTo(File location) throws IOException {
FileSystemUtils.deleteRecursively(location);
location.mkdirs();
FileSystemUtils.copyRecursively(new File(this.directories.getTestClassesDirectory(), "com"),
new File(location, "com"));
}
protected final List<String> getDependencyJarPaths() {
return Stream.of(this.directories.getDependenciesDirectory().listFiles()).map(File::getAbsolutePath)
.collect(Collectors.toList());
}
protected final Directories getDirectories() {
return this.directories;
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Base class for DevTools integration tests.
*
* @author Andy Wilkinson
*/
abstract class AbstractDevToolsIntegrationTests {
protected static final BuildOutput buildOutput = new BuildOutput(AbstractDevToolsIntegrationTests.class);
protected final File serverPortFile = new File(buildOutput.getRootLocation(), "server.port");
@RegisterExtension
protected final JvmLauncher javaLauncher = new JvmLauncher();
@TempDir
protected static File temp;
protected LaunchedApplication launchedApplication;
protected void launchApplication(ApplicationLauncher applicationLauncher, String... args) throws Exception {
this.serverPortFile.delete();
this.launchedApplication = applicationLauncher.launchApplication(this.javaLauncher, this.serverPortFile, args);
}
@AfterEach
void stopApplication() throws InterruptedException {
this.launchedApplication.stop();
}
protected int awaitServerPort() throws Exception {
int port = Awaitility.waitAtMost(Duration.ofSeconds(30))
.until(() -> new ApplicationState(this.serverPortFile, this.launchedApplication),
ApplicationState::hasServerPort)
.getServerPort();
this.serverPortFile.delete();
this.launchedApplication.restartRemote(port);
Thread.sleep(1000);
return port;
}
protected ControllerBuilder controller(String name) {
return new ControllerBuilder(name, this.launchedApplication.getClassesDirectory());
}
protected static final class ControllerBuilder {
private final List<String> mappings = new ArrayList<>();
private final String name;
private final File classesDirectory;
protected ControllerBuilder(String name, File classesDirectory) {
this.name = name;
this.classesDirectory = classesDirectory;
}
protected ControllerBuilder withRequestMapping(String mapping) {
this.mappings.add(mapping);
return this;
}
protected void build() throws Exception {
DynamicType.Builder<Object> builder = new ByteBuddy().subclass(Object.class).name(this.name)
.annotateType(AnnotationDescription.Builder.ofType(RestController.class).build());
for (String mapping : this.mappings) {
builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC)
.intercept(FixedValue.value(mapping)).annotateMethod(AnnotationDescription.Builder
.ofType(RequestMapping.class).defineArray("value", mapping).build());
}
builder.make().saveIn(this.classesDirectory);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
/**
* Launches an application with DevTools.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
public interface ApplicationLauncher {
LaunchedApplication launchApplication(JvmLauncher javaLauncher, File serverPortFile) throws Exception;
LaunchedApplication launchApplication(JvmLauncher jvmLauncher, File serverPortFile, String... additionalArgs)
throws Exception;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
/**
* State of an application.
*
* @author Andy Wilkinson
*/
final class ApplicationState {
private final Integer serverPort;
private final FileContents out;
private final FileContents err;
ApplicationState(File serverPortFile, LaunchedJvm jvm) {
this(serverPortFile, jvm.getStandardOut(), jvm.getStandardError());
}
ApplicationState(File serverPortFile, LaunchedApplication application) {
this(serverPortFile, application.getStandardOut(), application.getStandardError());
}
private ApplicationState(File serverPortFile, File out, File err) {
this.serverPort = new FileContents(serverPortFile).get(Integer::parseInt);
this.out = new FileContents(out);
this.err = new FileContents(err);
}
boolean hasServerPort() {
return this.serverPort != null;
}
int getServerPort() {
return this.serverPort;
}
@Override
public String toString() {
return String.format("Application output:%n%s%n%s", this.out, this.err);
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.io.IOException;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for DevTools.
*
* @author Andy Wilkinson
*/
class DevToolsIntegrationTests extends AbstractDevToolsIntegrationTests {
private final TestRestTemplate template = new TestRestTemplate(
new RestTemplateBuilder().requestFactory(() -> new HttpComponentsClientHttpRequestFactory(
HttpClients.custom().setRetryHandler(new StandardHttpRequestRetryHandler(10, false)).build())));
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void addARequestMappingToAnExistingController(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void removeARequestMappingFromAnExistingController(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
controller("com.example.ControllerOne").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForEntity(urlBase + "/one", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void createAController(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerTwo").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void createAControllerAndThenAddARequestMapping(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerTwo").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
controller("com.example.ControllerTwo").withRequestMapping("two").withRequestMapping("three").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/three", String.class)).isEqualTo("three");
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void createAControllerAndThenAddARequestMappingToAnExistingController(ApplicationLauncher applicationLauncher)
throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerTwo").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("three").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
assertThat(this.template.getForObject(urlBase + "/three", String.class)).isEqualTo("three");
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void deleteAController(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(new File(this.launchedApplication.getClassesDirectory(), "com/example/ControllerOne.class").delete())
.isTrue();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForEntity(urlBase + "/one", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void createAControllerAndThenDeleteIt(ApplicationLauncher applicationLauncher) throws Exception {
launchApplication(applicationLauncher);
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerTwo").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(this.template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
assertThat(new File(this.launchedApplication.getClassesDirectory(), "com/example/ControllerTwo.class").delete())
.isTrue();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(this.template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}
static Object[] parameters() throws IOException {
Directories directories = new Directories(buildOutput, temp);
return new Object[] { new Object[] { new LocalApplicationLauncher(directories) },
new Object[] { new ExplodedRemoteApplicationLauncher(directories) },
new Object[] { new JarFileRemoteApplicationLauncher(directories) } };
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.IOException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for DevTools with lazy initialization enabled.
*
* @author Madhura Bhave
*/
class DevToolsWithLazyInitializationIntegrationTests extends AbstractDevToolsIntegrationTests {
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void addARequestMappingToAnExistingControllerWhenLazyInit(ApplicationLauncher applicationLauncher)
throws Exception {
launchApplication(applicationLauncher, "--spring.main.lazy-initialization=true");
TestRestTemplate template = new TestRestTemplate();
String urlBase = "http://localhost:" + awaitServerPort();
assertThat(template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(template.getForEntity(urlBase + "/two", String.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
controller("com.example.ControllerOne").withRequestMapping("one").withRequestMapping("two").build();
urlBase = "http://localhost:" + awaitServerPort();
assertThat(template.getForObject(urlBase + "/one", String.class)).isEqualTo("one");
assertThat(template.getForObject(urlBase + "/two", String.class)).isEqualTo("two");
}
static Object[] parameters() throws IOException {
Directories directories = new Directories(buildOutput, temp);
return new Object[] { new Object[] { new LocalApplicationLauncher(directories) },
new Object[] { new ExplodedRemoteApplicationLauncher(directories) },
new Object[] { new JarFileRemoteApplicationLauncher(directories) } };
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import org.springframework.boot.testsupport.BuildOutput;
/**
* Various directories used by the {@link ApplicationLauncher ApplicationLaunchers}.
*
* @author Andy Wilkinson
*/
class Directories {
private final BuildOutput buildOutput;
private final File temp;
Directories(BuildOutput buildOutput, File temp) {
this.buildOutput = buildOutput;
this.temp = temp;
}
File getTestClassesDirectory() {
return this.buildOutput.getTestClassesLocation();
}
File getRemoteAppDirectory() {
return new File(this.temp, "remote");
}
File getDependenciesDirectory() {
return new File(this.buildOutput.getRootLocation(), "dependencies");
}
File getAppDirectory() {
return new File(this.temp, "app");
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationLauncher} that launches a remote application with its classes
* available directly on the file system.
*
* @author Andy Wilkinson
*/
public class ExplodedRemoteApplicationLauncher extends RemoteApplicationLauncher {
public ExplodedRemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
List<String> entries = new ArrayList<>();
entries.add(appDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
@Override
public String toString() {
return "exploded remote";
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.function.Function;
import org.springframework.util.FileCopyUtils;
/**
* Provides access to the contents of a file.
*
* @author Andy Wilkinson
*/
class FileContents {
private final File file;
FileContents(File file) {
this.file = file;
}
String get() {
return get(Function.identity());
}
<T> T get(Function<String, T> transformer) {
if ((!this.file.exists()) || this.file.length() == 0) {
return null;
}
try {
return transformer.apply(FileCopyUtils.copyToString(new FileReader(this.file)));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Override
public String toString() {
return get();
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationLauncher} that launches a remote application with its classes in a
* jar file.
*
* @author Andy Wilkinson
*/
public class JarFileRemoteApplicationLauncher extends RemoteApplicationLauncher {
public JarFileRemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
File appJar = new File(appDirectory, "app.jar");
JarOutputStream output = new JarOutputStream(new FileOutputStream(appJar), manifest);
addToJar(output, appDirectory, appDirectory);
output.close();
List<String> entries = new ArrayList<>();
entries.add(appJar.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
String classpath = StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
return classpath;
}
private void addToJar(JarOutputStream output, File root, File current) throws IOException {
for (File file : current.listFiles()) {
if (file.isDirectory()) {
addToJar(output, root, file);
}
output.putNextEntry(new ZipEntry(
file.getAbsolutePath().substring(root.getAbsolutePath().length() + 1).replace("\\", "/")
+ (file.isDirectory() ? "/" : "")));
if (file.isFile()) {
try (FileInputStream input = new FileInputStream(file)) {
StreamUtils.copy(input, output);
}
}
output.closeEntry();
}
}
@Override
public String toString() {
return "jar file remote";
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.boot.testsupport.BuildOutput;
import org.springframework.util.StringUtils;
/**
* {@link Extension} that launches a JVM and redirects its output to a test
* method-specific location.
*
* @author Andy Wilkinson
*/
class JvmLauncher implements BeforeTestExecutionCallback {
private static final Pattern NON_ALPHABET_PATTERN = Pattern.compile("[^A-Za-z]+");
private final BuildOutput buildOutput = new BuildOutput(getClass());
private File outputDirectory;
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
this.outputDirectory = new File(this.buildOutput.getRootLocation(),
"output/" + NON_ALPHABET_PATTERN.matcher(context.getRequiredTestMethod().getName()).replaceAll(""));
this.outputDirectory.mkdirs();
}
LaunchedJvm launch(String name, String classpath, String... args) throws IOException {
List<String> command = new ArrayList<>(
Arrays.asList(System.getProperty("java.home") + "/bin/java", "-cp", classpath));
command.addAll(Arrays.asList(args));
File standardOut = new File(this.outputDirectory, name + ".out");
File standardError = new File(this.outputDirectory, name + ".err");
Process process = new ProcessBuilder(StringUtils.toStringArray(command)).redirectError(standardError)
.redirectOutput(standardOut).start();
return new LaunchedJvm(process, standardOut, standardError);
}
static class LaunchedJvm {
private final Process process;
private final File standardOut;
private final File standardError;
LaunchedJvm(Process process, File standardOut, File standardError) {
this.process = process;
this.standardOut = standardOut;
this.standardError = standardError;
}
Process getProcess() {
return this.process;
}
File getStandardOut() {
return this.standardOut;
}
File getStandardError() {
return this.standardError;
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.util.function.BiFunction;
/**
* An application launched by {@link ApplicationLauncher}.
*
* @author Andy Wilkinson
*/
class LaunchedApplication {
private final File classesDirectory;
private final File standardOut;
private final File standardError;
private final Process localProcess;
private Process remoteProcess;
private final BiFunction<Integer, File, Process> remoteProcessRestarter;
LaunchedApplication(File classesDirectory, File standardOut, File standardError, Process localProcess,
Process remoteProcess, BiFunction<Integer, File, Process> remoteProcessRestarter) {
this.classesDirectory = classesDirectory;
this.standardOut = standardOut;
this.standardError = standardError;
this.localProcess = localProcess;
this.remoteProcess = remoteProcess;
this.remoteProcessRestarter = remoteProcessRestarter;
}
void restartRemote(int port) throws InterruptedException {
if (this.remoteProcessRestarter != null) {
stop(this.remoteProcess);
this.remoteProcess = this.remoteProcessRestarter.apply(port, this.classesDirectory);
}
}
void stop() throws InterruptedException {
stop(this.localProcess);
stop(this.remoteProcess);
}
private void stop(Process process) throws InterruptedException {
if (process != null) {
process.destroy();
process.waitFor();
}
}
File getStandardOut() {
return this.standardOut;
}
File getStandardError() {
return this.standardError;
}
File getClassesDirectory() {
return this.classesDirectory;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationLauncher} that launches a local application with DevTools enabled.
*
* @author Andy Wilkinson
*/
public class LocalApplicationLauncher extends AbstractApplicationLauncher {
LocalApplicationLauncher(Directories directories) {
super(directories);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher jvmLauncher, File serverPortFile) throws Exception {
LaunchedJvm jvm = jvmLauncher.launch("local", createApplicationClassPath(),
"com.example.DevToolsTestApplication", serverPortFile.getAbsolutePath(), "--server.port=0");
return new LaunchedApplication(getDirectories().getAppDirectory(), jvm.getStandardOut(), jvm.getStandardError(),
jvm.getProcess(), null, null);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher jvmLauncher, File serverPortFile, String... additionalArgs)
throws Exception {
List<String> args = new ArrayList<>(Arrays.asList("com.example.DevToolsTestApplication",
serverPortFile.getAbsolutePath(), "--server.port=0"));
args.addAll(Arrays.asList(additionalArgs));
LaunchedJvm jvm = jvmLauncher.launch("local", createApplicationClassPath(), args.toArray(new String[] {}));
return new LaunchedApplication(getDirectories().getAppDirectory(), jvm.getStandardOut(), jvm.getStandardError(),
jvm.getProcess(), null, null);
}
protected String createApplicationClassPath() throws Exception {
File appDirectory = getDirectories().getAppDirectory();
copyApplicationTo(appDirectory);
List<String> entries = new ArrayList<>();
entries.add(appDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
@Override
public String toString() {
return "local";
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2012-2019 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
*
* https://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.devtools.tests;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import org.awaitility.Awaitility;
import org.springframework.boot.devtools.RemoteSpringApplication;
import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.containsString;
/**
* Base class for {@link ApplicationLauncher} implementations that use
* {@link RemoteSpringApplication}.
*
* @author Andy Wilkinson
*/
abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher {
RemoteApplicationLauncher(Directories directories) {
super(directories);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher javaLauncher, File serverPortFile) throws Exception {
LaunchedJvm applicationJvm = javaLauncher.launch("app", createApplicationClassPath(),
"com.example.DevToolsTestApplication", serverPortFile.getAbsolutePath(), "--server.port=0",
"--spring.devtools.remote.secret=secret");
int port = awaitServerPort(applicationJvm, serverPortFile);
BiFunction<Integer, File, Process> remoteRestarter = getRemoteRestarter(javaLauncher);
return new LaunchedApplication(getDirectories().getRemoteAppDirectory(), applicationJvm.getStandardOut(),
applicationJvm.getStandardError(), applicationJvm.getProcess(), remoteRestarter.apply(port, null),
remoteRestarter);
}
@Override
public LaunchedApplication launchApplication(JvmLauncher javaLauncher, File serverPortFile,
String... additionalArgs) throws Exception {
List<String> args = new ArrayList<>(Arrays.asList("com.example.DevToolsTestApplication",
serverPortFile.getAbsolutePath(), "--server.port=0", "--spring.devtools.remote.secret=secret"));
args.addAll(Arrays.asList(additionalArgs));
LaunchedJvm applicationJvm = javaLauncher.launch("app", createApplicationClassPath(),
args.toArray(new String[] {}));
int port = awaitServerPort(applicationJvm, serverPortFile);
BiFunction<Integer, File, Process> remoteRestarter = getRemoteRestarter(javaLauncher);
return new LaunchedApplication(getDirectories().getRemoteAppDirectory(), applicationJvm.getStandardOut(),
applicationJvm.getStandardError(), applicationJvm.getProcess(), remoteRestarter.apply(port, null),
remoteRestarter);
}
private BiFunction<Integer, File, Process> getRemoteRestarter(JvmLauncher javaLauncher) {
return (port, classesDirectory) -> {
try {
LaunchedJvm remoteSpringApplicationJvm = javaLauncher.launch("remote-spring-application",
createRemoteSpringApplicationClassPath(classesDirectory),
RemoteSpringApplication.class.getName(), "--spring.devtools.remote.secret=secret",
"http://localhost:" + port);
awaitRemoteSpringApplication(remoteSpringApplicationJvm.getStandardOut());
return remoteSpringApplicationJvm.getProcess();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
};
}
protected abstract String createApplicationClassPath() throws Exception;
private String createRemoteSpringApplicationClassPath(File classesDirectory) throws Exception {
File remoteAppDirectory = getDirectories().getRemoteAppDirectory();
if (classesDirectory == null) {
copyApplicationTo(remoteAppDirectory);
}
List<String> entries = new ArrayList<>();
entries.add(remoteAppDirectory.getAbsolutePath());
entries.addAll(getDependencyJarPaths());
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
private int awaitServerPort(LaunchedJvm jvm, File serverPortFile) throws Exception {
return Awaitility.waitAtMost(Duration.ofSeconds(30))
.until(() -> new ApplicationState(serverPortFile, jvm), ApplicationState::hasServerPort)
.getServerPort();
}
private void awaitRemoteSpringApplication(File standardOut) throws Exception {
FileContents contents = new FileContents(standardOut);
Awaitility.waitAtMost(Duration.ofSeconds(30)).until(contents::get,
containsString("Started RemoteSpringApplication"));
}
}