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 index 51f24e31314..64e1ea1b4f2 100644 --- 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 @@ -29,32 +29,54 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kin 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.context.ApplicationContext; import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; 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; +import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.ServletContextResource; +import org.springframework.web.context.support.ServletContextResourcePatternResolver; /** * A {@code ResourcePatternResolver} that considers {@link ClassLoaderFiles} when * resolving resources. * * @author Andy Wilkinson + * @author Phillip Webb */ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver { private static final String[] LOCATION_PATTERN_PREFIXES = { CLASSPATH_ALL_URL_PREFIX, CLASSPATH_URL_PREFIX }; - private final ResourcePatternResolver delegate = new PathMatchingResourcePatternResolver(); + private static final String WEB_CONTEXT_CLASS = "org.springframework.web.context." + + "WebApplicationContext"; - private final AntPathMatcher antPathMatcher = new AntPathMatcher(); + private final ResourcePatternResolver delegate; + + private final PathMatcher antPathMatcher = new AntPathMatcher(); private final ClassLoaderFiles classLoaderFiles; - ClassLoaderFilesResourcePatternResolver(ClassLoaderFiles classLoaderFiles) { + ClassLoaderFilesResourcePatternResolver(ApplicationContext applicationContext, + ClassLoaderFiles classLoaderFiles) { this.classLoaderFiles = classLoaderFiles; + this.delegate = getResourcePatternResolverFactory() + .getResourcePatternResolver(applicationContext); + } + + private ResourcePatternResolverFactory getResourcePatternResolverFactory() { + if (ClassUtils.isPresent(WEB_CONTEXT_CLASS, null)) { + return new WebResourcePatternResolverFactory(); + } + return new ResourcePatternResolverFactory(); } @Override @@ -137,7 +159,7 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe * A {@link Resource} that represents a {@link ClassLoaderFile} that has been * {@link Kind#DELETED deleted}. */ - private final class DeletedClassLoaderFileResource extends AbstractResource { + static final class DeletedClassLoaderFileResource extends AbstractResource { private final String name; @@ -162,4 +184,60 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe } + /** + * Factory used to create the {@link ResourcePatternResolver} delegate. + */ + private static class ResourcePatternResolverFactory { + + public ResourcePatternResolver getResourcePatternResolver( + ApplicationContext applicationContext) { + return new PathMatchingResourcePatternResolver(); + } + + } + + /** + * {@link ResourcePatternResolverFactory} to be used when the classloader can access + * {@link WebApplicationContext}. + */ + private static class WebResourcePatternResolverFactory + extends ResourcePatternResolverFactory { + + @Override + public ResourcePatternResolver getResourcePatternResolver( + ApplicationContext applicationContext) { + if (applicationContext instanceof WebApplicationContext) { + return new ServletContextResourcePatternResolver( + new WebApplicationContextResourceLoader( + (WebApplicationContext) applicationContext)); + } + return super.getResourcePatternResolver(applicationContext); + } + + } + + /** + * {@link ResourceLoader} that optionally supports {@link ServletContextResource + * ServletContextResources}. + */ + private static class WebApplicationContextResourceLoader + extends DefaultResourceLoader { + + private final WebApplicationContext applicationContext; + + WebApplicationContextResourceLoader(WebApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + protected Resource getResourceByPath(String path) { + if (this.applicationContext.getServletContext() != null) { + return new ServletContextResource( + this.applicationContext.getServletContext(), path); + } + return super.getResourceByPath(path); + } + + } + } 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 c66d9bb8faa..04306f2761d 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 @@ -51,6 +51,7 @@ 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.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -426,8 +427,9 @@ public class Restarter { } private void prepare(GenericApplicationContext applicationContext) { - applicationContext.setResourceLoader( - new ClassLoaderFilesResourcePatternResolver(this.classLoaderFiles)); + ResourceLoader resourceLoader = new ClassLoaderFilesResourcePatternResolver( + applicationContext, this.classLoaderFiles); + applicationContext.setResourceLoader(resourceLoader); } private LeakSafeThread getLeakSafeThread() { diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolverTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolverTests.java new file mode 100644 index 00000000000..c709ef822b4 --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ClassLoaderFilesResourcePatternResolverTests.java @@ -0,0 +1,120 @@ +/* + * 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.File; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.springframework.boot.devtools.restart.ClassLoaderFilesResourcePatternResolver.DeletedClassLoaderFileResource; +import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile; +import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind; +import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.mock.web.MockServletContext; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.context.support.ServletContextResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ClassLoaderFilesResourcePatternResolver}. + * + * @author Phillip Webb + */ +public class ClassLoaderFilesResourcePatternResolverTests { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private ClassLoaderFiles files; + + private ClassLoaderFilesResourcePatternResolver resolver; + + @Before + public void setup() { + this.files = new ClassLoaderFiles(); + this.resolver = new ClassLoaderFilesResourcePatternResolver( + new GenericApplicationContext(), this.files); + } + + @Test + public void getClassLoaderShouldReturnClassLoader() throws Exception { + assertThat(this.resolver.getClassLoader()).isNotNull(); + } + + @Test + public void getResourceShouldReturnResource() throws Exception { + Resource resource = this.resolver.getResource("index.html"); + assertThat(resource).isNotNull().isInstanceOf(ClassPathResource.class); + } + + @Test + public void getResourceWhenHasServeletContextShouldReturnServletResource() + throws Exception { + GenericWebApplicationContext context = new GenericWebApplicationContext( + new MockServletContext()); + this.resolver = new ClassLoaderFilesResourcePatternResolver(context, this.files); + Resource resource = this.resolver.getResource("index.html"); + assertThat(resource).isNotNull().isInstanceOf(ServletContextResource.class); + } + + @Test + public void getResourceWhenDeletedShouldReturnDeletedResource() throws Exception { + File folder = this.temp.newFolder(); + File file = createFile(folder, "name.class"); + this.files.addFile(folder.getName(), "name.class", + new ClassLoaderFile(Kind.DELETED, null)); + Resource resource = this.resolver.getResource("file:" + file.getAbsolutePath()); + assertThat(resource).isNotNull() + .isInstanceOf(DeletedClassLoaderFileResource.class); + } + + @Test + public void getResourcesShouldReturnResources() throws Exception { + File folder = this.temp.newFolder(); + createFile(folder, "name.class"); + Resource[] resources = this.resolver + .getResources("file:" + folder.getAbsolutePath() + "/**"); + assertThat(resources).isNotEmpty(); + } + + @Test + public void getResourcesWhenDeletedShouldFilterDeleted() throws Exception { + File folder = this.temp.newFolder(); + createFile(folder, "name.class"); + this.files.addFile(folder.getName(), "name.class", + new ClassLoaderFile(Kind.DELETED, null)); + Resource[] resources = this.resolver + .getResources("file:" + folder.getAbsolutePath() + "/**"); + assertThat(resources).isEmpty(); + } + + private File createFile(File folder, String name) throws IOException { + File file = new File(folder, name); + FileCopyUtils.copy("test".getBytes(), file); + return file; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-war/pom.xml b/spring-boot-samples/spring-boot-sample-war/pom.xml index fee8d09f598..7a44a9cdfb2 100644 --- a/spring-boot-samples/spring-boot-sample-war/pom.xml +++ b/spring-boot-samples/spring-boot-sample-war/pom.xml @@ -42,6 +42,13 @@ + + + org.springframework.boot + spring-boot-devtools + provided + + org.springframework.boot spring-boot-starter-test