Fix remote DevTools' support for adding and removing classes
Previously, remote DevTools only correctly supported modifying existing classes. New classes that were added would be missed, and deleted classes could cause a failure as they would be found by component scanning but hidden by RestartClassLoader. This commit introduces a DevTools-specific ResourcePatternResolver that is installed as the application context's resource loader. This custom resolver is aware of the files that have been added and deleted and modifies the result returned from getResource and getResources accordingly. New intergration tests have been introduced to verify DevTools' behaviour. The tests cover four scenarios: - Adding a new controller - Removing an existing controller - Adding a request mapping to a controller - Removing a request mapping from a controller These four scenarios are tested with: - DevTools updating a local application - DevTools updating a remote application packaged in a jar file - DevTools updating a remote application that's been exploded Closes gh-7379pull/7438/head
parent
b3e0a37197
commit
918e122ddc
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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.4.3.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>
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue