Commit 61c93194 authored by Phillip Webb's avatar Phillip Webb

Fix Devtools PatternResolver Servlet support

Update ClassLoaderFilesResourcePatternResolver to support servlet
resources when it's being used with a WebApplicationContext.

Prior to commit 918e122d a `ResourceLoader` was not added to the
`ApplicationContext`, meaning that servlet resources could be found by
virtue of the protected `getResourceByPath()` method. Following commit
918e122d, the context `ResourceLoader` is set, meaning that all calls
to `getResource` delegate to the `ResourceLoader` and the
`ApplicationContext` methods are not invoked. Since the devtools
`ResourceLoader` wasn't Servlet aware, servlet resources could not
be found.

Fixes gh-7752
parent 3389af2c
...@@ -29,32 +29,54 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kin ...@@ -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.ClassLoaderFileURLStreamHandler;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder; 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.AbstractResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.UrlResource; import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.AntPathMatcher; 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 * A {@code ResourcePatternResolver} that considers {@link ClassLoaderFiles} when
* resolving resources. * resolving resources.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
*/ */
final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver { final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver {
private static final String[] LOCATION_PATTERN_PREFIXES = { CLASSPATH_ALL_URL_PREFIX, private static final String[] LOCATION_PATTERN_PREFIXES = { CLASSPATH_ALL_URL_PREFIX,
CLASSPATH_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; private final ClassLoaderFiles classLoaderFiles;
ClassLoaderFilesResourcePatternResolver(ClassLoaderFiles classLoaderFiles) { ClassLoaderFilesResourcePatternResolver(ApplicationContext applicationContext,
ClassLoaderFiles classLoaderFiles) {
this.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 @Override
...@@ -137,7 +159,7 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe ...@@ -137,7 +159,7 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe
* A {@link Resource} that represents a {@link ClassLoaderFile} that has been * A {@link Resource} that represents a {@link ClassLoaderFile} that has been
* {@link Kind#DELETED deleted}. * {@link Kind#DELETED deleted}.
*/ */
private final class DeletedClassLoaderFileResource extends AbstractResource { static final class DeletedClassLoaderFileResource extends AbstractResource {
private final String name; private final String name;
...@@ -162,4 +184,60 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe ...@@ -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);
}
}
} }
...@@ -51,6 +51,7 @@ import org.springframework.context.ConfigurableApplicationContext; ...@@ -51,6 +51,7 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
...@@ -426,8 +427,9 @@ public class Restarter { ...@@ -426,8 +427,9 @@ public class Restarter {
} }
private void prepare(GenericApplicationContext applicationContext) { private void prepare(GenericApplicationContext applicationContext) {
applicationContext.setResourceLoader( ResourceLoader resourceLoader = new ClassLoaderFilesResourcePatternResolver(
new ClassLoaderFilesResourcePatternResolver(this.classLoaderFiles)); applicationContext, this.classLoaderFiles);
applicationContext.setResourceLoader(resourceLoader);
} }
private LeakSafeThread getLeakSafeThread() { private LeakSafeThread getLeakSafeThread() {
......
/*
* 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;
}
}
...@@ -42,6 +42,13 @@ ...@@ -42,6 +42,13 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- Provided -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment