From cbdab9edb365c02a8c16001e1525d716503a6f1e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 6 Jun 2017 16:04:12 +0200 Subject: [PATCH] Add support for custom ProtocolResolver with Devtools Prior to this commit, custom `ProtocolResolvers` set on the `ApplicationContext` were lost when Devtools is used as the customized `ResourceLoader` did not copy any customization made to the default resource loader. This commit makes sure to copy any `ProtocolResolver` set on the context. Closes gh-9331 --- ...assLoaderFilesResourcePatternResolver.java | 35 ++++++++++++++++-- ...aderFilesResourcePatternResolverTests.java | 37 +++++++++++++++++++ .../src/main/asciidoc/using-spring-boot.adoc | 4 ++ 3 files changed, 73 insertions(+), 3 deletions(-) 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 cbba9e9870..df3fafe0c1 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 @@ -22,6 +22,7 @@ import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map.Entry; @@ -33,6 +34,7 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.So import org.springframework.context.ApplicationContext; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.UrlResource; @@ -52,6 +54,7 @@ import org.springframework.web.context.support.ServletContextResourcePatternReso * * @author Andy Wilkinson * @author Phillip Webb + * @author Stephane Nicoll */ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver { @@ -206,7 +209,20 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe public ResourcePatternResolver getResourcePatternResolver( ApplicationContext applicationContext, ResourceLoader resourceLoader) { return new PathMatchingResourcePatternResolver(resourceLoader == null - ? new DefaultResourceLoader() : resourceLoader); + ? createResourceLoader(applicationContext) : resourceLoader); + } + + private ResourceLoader createResourceLoader( + ApplicationContext applicationContext) { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + if (applicationContext instanceof DefaultResourceLoader) { + Collection protocolResolvers = + ((DefaultResourceLoader) applicationContext).getProtocolResolvers(); + for (ProtocolResolver protocolResolver : protocolResolvers) { + resourceLoader.addProtocolResolver(protocolResolver); + } + } + return resourceLoader; } } @@ -223,13 +239,26 @@ final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternRe ApplicationContext applicationContext, ResourceLoader resourceLoader) { if (applicationContext instanceof WebApplicationContext) { return new ServletContextResourcePatternResolver(resourceLoader == null - ? new WebApplicationContextResourceLoader( - (WebApplicationContext) applicationContext) + ? createResourceLoader((WebApplicationContext) applicationContext) : resourceLoader); } return super.getResourcePatternResolver(applicationContext, resourceLoader); } + private ResourceLoader createResourceLoader( + WebApplicationContext applicationContext) { + WebApplicationContextResourceLoader resourceLoader = + new WebApplicationContextResourceLoader(applicationContext); + if (applicationContext instanceof DefaultResourceLoader) { + Collection protocolResolvers = + ((DefaultResourceLoader) applicationContext).getProtocolResolvers(); + for (ProtocolResolver protocolResolver : protocolResolvers) { + resourceLoader.addProtocolResolver(protocolResolver); + } + } + return resourceLoader; + } + } /** 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 index 789920b94d..7a3254b8c8 100644 --- 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 @@ -30,6 +30,7 @@ import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kin 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.ProtocolResolver; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockServletContext; @@ -38,6 +39,9 @@ import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.ServletContextResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -46,6 +50,7 @@ import static org.mockito.Mockito.verify; * * @author Phillip Webb * @author Andy Wilkinson + * @author Stephane Nicoll */ public class ClassLoaderFilesResourcePatternResolverTests { @@ -125,6 +130,18 @@ public class ClassLoaderFilesResourcePatternResolverTests { verify(resourceLoader).getResource("foo.txt"); } + @Test + public void customProtocolResolverIsUsedInNonWebApplication() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + Resource resource = mock(Resource.class); + ProtocolResolver resolver = mockProtocolResolver("foo:some-file.txt", resource); + context.addProtocolResolver(resolver); + this.resolver = new ClassLoaderFilesResourcePatternResolver(context, this.files); + Resource actual = this.resolver.getResource("foo:some-file.txt"); + assertThat(actual).isSameAs(resource); + verify(resolver).resolve(eq("foo:some-file.txt"), any(ResourceLoader.class)); + } + @Test public void customResourceLoaderIsUsedInWebApplication() throws Exception { GenericWebApplicationContext context = new GenericWebApplicationContext( @@ -136,6 +153,26 @@ public class ClassLoaderFilesResourcePatternResolverTests { verify(resourceLoader).getResource("foo.txt"); } + @Test + public void customProtocolResolverIsUsedInWebApplication() throws Exception { + GenericWebApplicationContext context = new GenericWebApplicationContext( + new MockServletContext()); + Resource resource = mock(Resource.class); + ProtocolResolver resolver = mockProtocolResolver("foo:some-file.txt", resource); + context.addProtocolResolver(resolver); + this.resolver = new ClassLoaderFilesResourcePatternResolver(context, this.files); + Resource actual = this.resolver.getResource("foo:some-file.txt"); + assertThat(actual).isSameAs(resource); + verify(resolver).resolve(eq("foo:some-file.txt"), any(ResourceLoader.class)); + } + + private ProtocolResolver mockProtocolResolver(String path, Resource resource) { + ProtocolResolver resolver = mock(ProtocolResolver.class); + given(resolver.resolve(eq(path), any(ResourceLoader.class))) + .willReturn(resource); + return resolver; + } + private File createFile(File folder, String name) throws IOException { File file = new File(folder, name); FileCopyUtils.copy("test".getBytes(), file); diff --git a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc index 2022817918..5560228514 100644 --- a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc +++ b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc @@ -853,6 +853,10 @@ NOTE: When deciding if an entry on the classpath should trigger a restart when i DevTools automatically ignores projects named `spring-boot`, `spring-boot-devtools`, `spring-boot-autoconfigure`, `spring-boot-actuator`, and `spring-boot-starter`. +NOTE: DevTools needs to customize the `ResourceLoader` used by the `ApplicationContext`: +if your application provides one already, it is going to be wrapped. Direct override of +the `getResource` method on the `ApplicationContext` is not supported. + [[using-spring-boot-restart-vs-reload]] .Restart vs Reload ****