diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolver.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolver.java new file mode 100644 index 0000000000..7cc768e7cb --- /dev/null +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolver.java @@ -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 LOCATION_PATTERN_PREFIXES = Collections + .unmodifiableSet(new HashSet( + 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 resources = new ArrayList(); + 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 getAdditionalResources(String locationPattern) + throws MalformedURLException { + List additionalResources = new ArrayList(); + String trimmedLocationPattern = trimLocationPattern(locationPattern); + for (SourceFolder sourceFolder : this.classLoaderFiles.getSourceFolders()) { + for (Entry 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 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"); + } + } +} \ No newline at end of file diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index 1c94bc5506..6137a29a53 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -48,6 +48,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; @@ -418,6 +419,10 @@ public class Restarter { if (applicationContext != null && applicationContext.getParent() != null) { return; } + if (applicationContext instanceof GenericApplicationContext) { + ((GenericApplicationContext) applicationContext).setResourceLoader( + new ClassLoaderFilesResourcePatternResolver(this.classLoaderFiles)); + } this.rootContext = applicationContext; } diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFileURLStreamHandler.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFileURLStreamHandler.java index c754eeb469..cd4da3c642 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFileURLStreamHandler.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFileURLStreamHandler.java @@ -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; } diff --git a/spring-boot-integration-tests/pom.xml b/spring-boot-integration-tests/pom.xml index 2beb79b59a..30b7135d5e 100644 --- a/spring-boot-integration-tests/pom.xml +++ b/spring-boot-integration-tests/pom.xml @@ -21,6 +21,7 @@ 1.8 + spring-boot-devtools-tests spring-boot-gradle-tests spring-boot-launch-script-tests spring-boot-security-tests diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml b/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml new file mode 100644 index 0000000000..4e9338936b --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-integration-tests + 1.4.3.BUILD-SNAPSHOT + + spring-boot-devtools-tests + Spring Boot DevTools Tests + ${project.name} + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-devtools + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + net.bytebuddy + byte-buddy + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + process-test-resources + + copy-dependencies + + + runtime + ${project.build.directory}/dependencies + true + true + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + [2.10,) + + copy-dependencies + + + + + + + + + + + + + + diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/ControllerOne.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/ControllerOne.java new file mode 100644 index 0000000000..874073ab94 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/ControllerOne.java @@ -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"; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/DevToolsTestApplication.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/DevToolsTestApplication.java new file mode 100644 index 0000000000..0f121c58c9 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/com/example/DevToolsTestApplication.java @@ -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); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java new file mode 100644 index 0000000000..7b9c05b0b8 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java @@ -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; + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java new file mode 100644 index 0000000000..e970b0aade --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsIntegrationTests.java @@ -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 mappings = new ArrayList(); + + 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 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); + } + } +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ExplodedRemoteApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ExplodedRemoteApplicationLauncher.java new file mode 100644 index 0000000000..a9ceb95084 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ExplodedRemoteApplicationLauncher.java @@ -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 entries = new ArrayList(); + 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"; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JarFileRemoteApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JarFileRemoteApplicationLauncher.java new file mode 100644 index 0000000000..9bdda3e207 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JarFileRemoteApplicationLauncher.java @@ -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 entries = new ArrayList(); + 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"; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JavaLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JavaLauncher.java new file mode 100644 index 0000000000..114cf4bf18 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/JavaLauncher.java @@ -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 command = new ArrayList(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(); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LaunchedApplication.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LaunchedApplication.java new file mode 100644 index 0000000000..3848fb2a05 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LaunchedApplication.java @@ -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; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java new file mode 100644 index 0000000000..debb41dbb6 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java @@ -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 entries = new ArrayList(); + 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"; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java new file mode 100644 index 0000000000..c5ff5e5442 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java @@ -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 entries = new ArrayList(); + entries.add("target/remote"); + for (File jar : new File("target/dependencies").listFiles()) { + entries.add(jar.getAbsolutePath()); + } + return StringUtils.collectionToDelimitedString(entries, File.pathSeparator); + } + +} diff --git a/spring-boot-parent/pom.xml b/spring-boot-parent/pom.xml index 1b417894c3..34d00bf22a 100644 --- a/spring-boot-parent/pom.xml +++ b/spring-boot-parent/pom.xml @@ -77,6 +77,11 @@ jline 2.11 + + net.bytebuddy + byte-buddy + 1.5.4 + net.sf.jopt-simple jopt-simple diff --git a/spring-boot-parent/src/checkstyle/checkstyle-suppressions.xml b/spring-boot-parent/src/checkstyle/checkstyle-suppressions.xml index 0e6ca91c65..df411c7057 100644 --- a/spring-boot-parent/src/checkstyle/checkstyle-suppressions.xml +++ b/spring-boot-parent/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,7 @@ +