Merge branch '1.4.x' into 1.5.x

This commit is contained in:
Andy Wilkinson 2016-11-17 19:28:01 +00:00
commit eff0fc0221
17 changed files with 917 additions and 2 deletions

View File

@ -0,0 +1,165 @@
/*
* Copyright 2012-2016 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.devtools.restart;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFileURLStreamHandler;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
/**
* A {@code ResourcePatternResolver} that considers {@link ClassLoaderFiles} when
* resolving resources.
*
* @author Andy Wilkinson
*/
final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver {
private static final Set<String> LOCATION_PATTERN_PREFIXES = Collections
.unmodifiableSet(new HashSet<String>(
Arrays.asList(CLASSPATH_ALL_URL_PREFIX, CLASSPATH_URL_PREFIX)));
private final ResourcePatternResolver delegate = new PathMatchingResourcePatternResolver();
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final ClassLoaderFiles classLoaderFiles;
ClassLoaderFilesResourcePatternResolver(ClassLoaderFiles classLoaderFiles) {
this.classLoaderFiles = classLoaderFiles;
}
@Override
public Resource getResource(String location) {
Resource candidate = this.delegate.getResource(location);
if (isExcludedResource(candidate)) {
return new DeletedClassLoaderFileResource(location);
}
return candidate;
}
@Override
public ClassLoader getClassLoader() {
return this.delegate.getClassLoader();
}
@Override
public Resource[] getResources(String locationPattern) throws IOException {
List<Resource> resources = new ArrayList<Resource>();
Resource[] candidates = this.delegate.getResources(locationPattern);
for (Resource candidate : candidates) {
if (!isExcludedResource(candidate)) {
resources.add(candidate);
}
}
resources.addAll(getAdditionalResources(locationPattern));
return resources.toArray(new Resource[resources.size()]);
}
private String trimLocationPattern(String locationPattern) {
for (String prefix : LOCATION_PATTERN_PREFIXES) {
if (locationPattern.startsWith(prefix)) {
return locationPattern.substring(prefix.length());
}
}
return locationPattern;
}
private List<Resource> getAdditionalResources(String locationPattern)
throws MalformedURLException {
List<Resource> additionalResources = new ArrayList<Resource>();
String trimmedLocationPattern = trimLocationPattern(locationPattern);
for (SourceFolder sourceFolder : this.classLoaderFiles.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : sourceFolder.getFilesEntrySet()) {
if (entry.getValue().getKind() == Kind.ADDED && this.antPathMatcher
.match(trimmedLocationPattern, entry.getKey())) {
additionalResources.add(new UrlResource(new URL("reloaded", null, -1,
"/" + entry.getKey(),
new ClassLoaderFileURLStreamHandler(entry.getValue()))));
}
}
}
return additionalResources;
}
private boolean isExcludedResource(Resource resource) {
for (SourceFolder sourceFolder : this.classLoaderFiles.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : sourceFolder.getFilesEntrySet()) {
try {
if (entry.getValue().getKind() == Kind.DELETED && resource.exists()
&& resource.getURI().toString().endsWith(entry.getKey())) {
return true;
}
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to retrieve URI from '" + resource + "'", ex);
}
}
}
return false;
}
/**
* A {@link Resource} that represents a {@link ClassLoaderFile} that has been
* {@link Kind#DELETED deleted}.
*
* @author Andy Wilkinson
*/
private final class DeletedClassLoaderFileResource extends AbstractResource {
private final String name;
private DeletedClassLoaderFileResource(String name) {
this.name = name;
}
@Override
public boolean exists() {
return false;
}
@Override
public String getDescription() {
return "Deleted: " + this.name;
}
@Override
public InputStream getInputStream() throws IOException {
throw new IOException(this.name + " has been deleted");
}
}
}

View File

@ -49,6 +49,7 @@ import org.springframework.boot.devtools.restart.classloader.RestartClassLoader;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.cglib.core.ClassNameReader;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
@ -419,6 +420,10 @@ public class Restarter {
if (applicationContext != null && applicationContext.getParent() != null) {
return;
}
if (applicationContext instanceof GenericApplicationContext) {
((GenericApplicationContext) applicationContext).setResourceLoader(
new ClassLoaderFilesResourcePatternResolver(this.classLoaderFiles));
}
this.rootContexts.add(applicationContext);
}

View File

@ -28,11 +28,11 @@ import java.net.URLStreamHandler;
*
* @author Phillip Webb
*/
class ClassLoaderFileURLStreamHandler extends URLStreamHandler {
public class ClassLoaderFileURLStreamHandler extends URLStreamHandler {
private ClassLoaderFile file;
ClassLoaderFileURLStreamHandler(ClassLoaderFile file) {
public ClassLoaderFileURLStreamHandler(ClassLoaderFile file) {
this.file = file;
}

View File

@ -21,6 +21,7 @@
<java.version>1.8</java.version>
</properties>
<modules>
<module>spring-boot-devtools-tests</module>
<module>spring-boot-gradle-tests</module>
<module>spring-boot-launch-script-tests</module>
<module>spring-boot-security-tests</module>

View File

@ -0,0 +1,90 @@
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-integration-tests</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-devtools-tests</artifactId>
<name>Spring Boot DevTools Tests</name>
<description>${project.name}</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>process-test-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/dependencies</outputDirectory>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<versionRange>[2.10,)</versionRange>
<goals>
<goal>copy-dependencies</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@ -0,0 +1,30 @@
/*
* Copyright 2012-2016 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 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,32 @@
/*
* Copyright 2012-2016 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 com.example;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.system.EmbeddedServerPortFileWriter;
@SpringBootApplication
public class DevToolsTestApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DevToolsTestApplication.class)
.listeners(new EmbeddedServerPortFileWriter("target/server.port"))
.run(args);
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2012-2016 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.devtools.tests;
/**
* Launches an application with DevTools.
*
* @author Andy Wilkinson
*/
public interface ApplicationLauncher {
LaunchedApplication launchApplication(JavaLauncher javaLauncher) throws Exception;
}

View File

@ -0,0 +1,191 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
import java.io.FileReader;
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.Builder;
import net.bytebuddy.implementation.FixedValue;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for DevTools.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class DevToolsIntegrationTests {
private LaunchedApplication launchedApplication;
private final File serverPortFile = new File("target/server.port");
private final ApplicationLauncher applicationLauncher;
@Rule
public JavaLauncher javaLauncher = new JavaLauncher();
@Parameters(name = "{0}")
public static Object[] parameters() {
return new Object[] { new Object[] { new LocalApplicationLauncher() },
new Object[] { new ExplodedRemoteApplicationLauncher() },
new Object[] { new JarFileRemoteApplicationLauncher() } };
}
public DevToolsIntegrationTests(ApplicationLauncher applicationLauncher) {
this.applicationLauncher = applicationLauncher;
}
@Before
public void launchApplication() throws Exception {
this.serverPortFile.delete();
this.launchedApplication = this.applicationLauncher
.launchApplication(this.javaLauncher);
}
@After
public void stopApplication() {
this.launchedApplication.stop();
}
@Test
public void addARequestMappingToAnExistingController() throws Exception {
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();
assertThat(template.getForObject(urlBase + "/one", String.class))
.isEqualTo("one");
assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/two",
String.class)).isEqualTo("two");
}
@Test
public void removeARequestMappingFromAnExistingController() throws Exception {
TestRestTemplate template = new TestRestTemplate();
assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/one",
String.class)).isEqualTo("one");
controller("com.example.ControllerOne").build();
assertThat(template.getForEntity("http://localhost:" + awaitServerPort() + "/one",
String.class).getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void createAController() throws Exception {
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.ControllerTwo").withRequestMapping("two").build();
assertThat(template.getForObject(urlBase + "/one", String.class))
.isEqualTo("one");
assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/two",
String.class)).isEqualTo("two");
}
@Test
public void deleteAController() throws Exception {
TestRestTemplate template = new TestRestTemplate();
assertThat(template.getForObject("http://localhost:" + awaitServerPort() + "/one",
String.class)).isEqualTo("one");
assertThat(new File(this.launchedApplication.getClassesDirectory(),
"com/example/ControllerOne.class").delete()).isTrue();
assertThat(template.getForEntity("http://localhost:" + awaitServerPort() + "/one",
String.class).getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
private int awaitServerPort() throws Exception {
long end = System.currentTimeMillis() + 20000;
while (!this.serverPortFile.exists()) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(
"server.port file was not written within 20 seconds");
}
Thread.sleep(100);
}
int port = Integer
.valueOf(FileCopyUtils.copyToString(new FileReader(this.serverPortFile)));
this.serverPortFile.delete();
return port;
}
private ControllerBuilder controller(String name) {
return new ControllerBuilder(name,
this.launchedApplication.getClassesDirectory());
}
private static final class ControllerBuilder {
private final List<String> mappings = new ArrayList<String>();
private final String name;
private final File classesDirectory;
private ControllerBuilder(String name, File classesDirectory) {
this.name = name;
this.classesDirectory = classesDirectory;
}
public ControllerBuilder withRequestMapping(String mapping) {
this.mappings.add(mapping);
return this;
}
public void build() throws Exception {
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,54 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.FileSystemUtils;
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 {
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
FileSystemUtils.deleteRecursively(appDirectory);
appDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
List<String> entries = new ArrayList<String>();
entries.add("target/app");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
@Override
public String toString() {
return "exploded remote";
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2012-2016 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.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.FileSystemUtils;
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 {
@Override
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
FileSystemUtils.deleteRecursively(appDirectory);
appDirectory.mkdirs();
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
JarOutputStream output = new JarOutputStream(
new FileOutputStream(new File(appDirectory, "app.jar")), manifest);
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
addToJar(output, new File("target/app/"), new File("target/app/"));
output.close();
List<String> entries = new ArrayList<String>();
entries.add("target/app/app.jar");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
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)
+ (file.isDirectory() ? "/" : "")));
if (file.isFile()) {
StreamUtils.copy(new FileInputStream(file), output);
}
output.closeEntry();
}
}
@Override
public String toString() {
return "jar file remote";
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* @author awilkinson
*/
public class JavaLauncher implements TestRule {
private File outputDirectory;
@Override
public Statement apply(Statement base, Description description) {
this.outputDirectory = new File("target/output/" + "/"
+ description.getMethodName().replaceAll("[^A-Za-z]+", ""));
this.outputDirectory.mkdirs();
return base;
}
Process launch(String name, String classpath, String... args) throws IOException {
List<String> command = new ArrayList<String>(Arrays
.asList(System.getProperty("java.home") + "/bin/java", "-cp", classpath));
command.addAll(Arrays.asList(args));
return new ProcessBuilder(command.toArray(new String[command.size()]))
.redirectError(new File(this.outputDirectory, name + ".err"))
.redirectOutput(new File(this.outputDirectory, name + ".out")).start();
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
/**
* An application launched by {@link ApplicationLauncher}.
*
* @author Andy Wilkinson
*/
class LaunchedApplication {
private final File classesDirectory;
private final Process[] processes;
LaunchedApplication(File classesDirectory, Process... processes) {
this.classesDirectory = classesDirectory;
this.processes = processes;
}
void stop() {
for (Process process : this.processes) {
process.destroy();
}
}
File getClassesDirectory() {
return this.classesDirectory;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationLauncher} that launches a local application with DevTools enabled.
*
* @author Andy Wilkinson
*/
public class LocalApplicationLauncher implements ApplicationLauncher {
@Override
public LaunchedApplication launchApplication(JavaLauncher javaLauncher)
throws Exception {
Process process = javaLauncher.launch("local", createApplicationClassPath(),
"com.example.DevToolsTestApplication", "--server.port=0");
return new LaunchedApplication(new File("target/app"), process);
}
protected String createApplicationClassPath() throws Exception {
File appDirectory = new File("target/app");
FileSystemUtils.deleteRecursively(appDirectory);
appDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/app/com"));
List<String> entries = new ArrayList<String>();
entries.add("target/app");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
@Override
public String toString() {
return "local";
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2012-2016 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.devtools.tests;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.devtools.RemoteSpringApplication;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.SocketUtils;
import org.springframework.util.StringUtils;
/**
* Base class for {@link ApplicationLauncher} implementations that use
* {@link RemoteSpringApplication}.
*
* @author Andy Wilkinson
*/
abstract class RemoteApplicationLauncher implements ApplicationLauncher {
@Override
public LaunchedApplication launchApplication(JavaLauncher javaLauncher)
throws Exception {
int port = SocketUtils.findAvailableTcpPort();
Process application = javaLauncher.launch("app", createApplicationClassPath(),
"com.example.DevToolsTestApplication", "--server.port=" + port,
"--spring.devtools.remote.secret=secret");
Process remoteSpringApplication = javaLauncher.launch("remote-spring-application",
createRemoteSpringApplicationClassPath(),
RemoteSpringApplication.class.getName(),
"--spring.devtools.remote.secret=secret", "http://localhost:" + port);
return new LaunchedApplication(new File("target/remote"), application,
remoteSpringApplication);
}
protected abstract String createApplicationClassPath() throws Exception;
private String createRemoteSpringApplicationClassPath() throws Exception {
File remoteDirectory = new File("target/remote");
FileSystemUtils.deleteRecursively(remoteDirectory);
remoteDirectory.mkdirs();
FileSystemUtils.copyRecursively(new File("target/test-classes/com"),
new File("target/remote/com"));
List<String> entries = new ArrayList<String>();
entries.add("target/remote");
for (File jar : new File("target/dependencies").listFiles()) {
entries.add(jar.getAbsolutePath());
}
return StringUtils.collectionToDelimitedString(entries, File.pathSeparator);
}
}

View File

@ -77,6 +77,11 @@
<artifactId>jline</artifactId>
<version>2.11</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>

View File

@ -6,6 +6,7 @@
<suppress files="SpringApplicationTests\.java" checks="FinalClass" />
<suppress files=".+Configuration\.java" checks="HideUtilityClassConstructor" />
<suppress files="LaunchScriptTestApplication\.java" checks="HideUtilityClassConstructor" />
<suppress files="DevToolsTestApplication\.java" checks="HideUtilityClassConstructor" />
<suppress files="SignalUtils\.java" checks="IllegalImport" />
<suppress files="[\\/]src[\\/]test[\\/]java[\\/]cli[\\/]command[\\/]" checks="ImportControl" />
<suppress files="[\\/]src[\\/]main[\\/]java[\\/]sample[\\/]" checks="ImportControl" />