diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index 0283e11e48..b86d12edbc 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; @@ -93,6 +94,7 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @author Chris Beams * @author Stephane Nicoll + * @author Sam Brannen * @since 1.1.2 * @see #registerBeanDefinition * @see #refresh() @@ -223,13 +225,23 @@ public class GenericApplicationContext extends AbstractApplicationContext implem //--------------------------------------------------------------------- /** - * This implementation delegates to this context's ResourceLoader if set, - * falling back to the default superclass behavior else. - * @see #setResourceLoader + * This implementation delegates to this context's {@code ResourceLoader} if set, + * falling back to the default superclass behavior otherwise. + *

As of Spring Framework 5.3.22, this method also honors registered + * {@linkplain #getProtocolResolvers() protocol resolvers} when a custom + * {@code ResourceLoader} has been set. + * @see #setResourceLoader(ResourceLoader) + * @see #addProtocolResolver(ProtocolResolver) */ @Override public Resource getResource(String location) { if (this.resourceLoader != null) { + for (ProtocolResolver protocolResolver : getProtocolResolvers()) { + Resource resource = protocolResolver.resolve(location, this); + if (resource != null) { + return resource; + } + } return this.resourceLoader.getResource(location); } return super.getResource(location); @@ -238,7 +250,7 @@ public class GenericApplicationContext extends AbstractApplicationContext implem /** * This implementation delegates to this context's ResourceLoader if it * implements the ResourcePatternResolver interface, falling back to the - * default superclass behavior else. + * default superclass behavior otherwise. * @see #setResourceLoader */ @Override diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index 8aca19e643..000bafad69 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -16,6 +16,7 @@ package org.springframework.context.support; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -31,90 +32,100 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.FileSystemResourceLoader; +import org.springframework.core.io.FileUrlResource; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.metrics.jfr.FlightRecorderApplicationStartup; -import org.springframework.util.ObjectUtils; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** + * Tests for {@link GenericApplicationContext}. + * * @author Juergen Hoeller * @author Chris Beams * @author Stephane Nicoll + * @author Sam Brannen */ class GenericApplicationContextTests { + private final GenericApplicationContext context = new GenericApplicationContext(); + + @AfterEach + void closeContext() { + context.close(); + } + @Test void getBeanForClass() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); - ac.refresh(); + context.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); + context.refresh(); - assertThat(ac.getBean("testBean")).isEqualTo(""); - assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); - assertThat(ac.getBean(CharSequence.class)).isSameAs(ac.getBean("testBean")); + assertThat(context.getBean("testBean")).isEqualTo(""); + assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); + assertThat(context.getBean(CharSequence.class)).isSameAs(context.getBean("testBean")); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - ac.getBean(Object.class)); - ac.close(); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(Object.class)); } @Test void withSingletonSupplier() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class, ac::toString)); - ac.refresh(); + context.registerBeanDefinition("testBean", new RootBeanDefinition(String.class, context::toString)); + context.refresh(); - assertThat(ac.getBean("testBean")).isSameAs(ac.getBean("testBean")); - assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); - assertThat(ac.getBean(CharSequence.class)).isSameAs(ac.getBean("testBean")); - assertThat(ac.getBean("testBean")).isEqualTo(ac.toString()); - ac.close(); + assertThat(context.getBean("testBean")).isSameAs(context.getBean("testBean")); + assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); + assertThat(context.getBean(CharSequence.class)).isSameAs(context.getBean("testBean")); + assertThat(context.getBean("testBean")).isEqualTo(context.toString()); } @Test void withScopedSupplier() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("testBean", - new RootBeanDefinition(String.class, BeanDefinition.SCOPE_PROTOTYPE, ac::toString)); - ac.refresh(); + context.registerBeanDefinition("testBean", + new RootBeanDefinition(String.class, BeanDefinition.SCOPE_PROTOTYPE, context::toString)); + context.refresh(); - assertThat(ac.getBean("testBean")).isNotSameAs(ac.getBean("testBean")); - assertThat(ac.getBean(String.class)).isEqualTo(ac.getBean("testBean")); - assertThat(ac.getBean(CharSequence.class)).isEqualTo(ac.getBean("testBean")); - assertThat(ac.getBean("testBean")).isEqualTo(ac.toString()); - ac.close(); + assertThat(context.getBean("testBean")).isNotSameAs(context.getBean("testBean")); + assertThat(context.getBean(String.class)).isEqualTo(context.getBean("testBean")); + assertThat(context.getBean(CharSequence.class)).isEqualTo(context.getBean("testBean")); + assertThat(context.getBean("testBean")).isEqualTo(context.toString()); } @Test void accessAfterClosing() { - GenericApplicationContext ac = new GenericApplicationContext(); - ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); - ac.refresh(); + context.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); + context.refresh(); - assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); - assertThat(ac.getAutowireCapableBeanFactory().getBean(String.class)).isSameAs(ac.getAutowireCapableBeanFactory().getBean("testBean")); + assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); + assertThat(context.getAutowireCapableBeanFactory().getBean(String.class)) + .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); - ac.close(); + context.close(); - assertThatIllegalStateException().isThrownBy(() -> - ac.getBean(String.class)); - - assertThatIllegalStateException().isThrownBy(() -> { - ac.getAutowireCapableBeanFactory().getBean("testBean"); - ac.getAutowireCapableBeanFactory().getBean(String.class); - }); - ac.close(); + assertThatIllegalStateException() + .isThrownBy(() -> context.getBean(String.class)); + assertThatIllegalStateException() + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); + assertThatIllegalStateException() + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); } @Test void individualBeans() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(BeanA.class); context.registerBean(BeanB.class); context.registerBean(BeanC.class); @@ -123,12 +134,10 @@ class GenericApplicationContextTests { assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); - context.close(); } @Test void individualNamedBeans() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean("a", BeanA.class); context.registerBean("b", BeanB.class); context.registerBean("c", BeanC.class); @@ -137,12 +146,10 @@ class GenericApplicationContextTests { assertThat(context.getBean("a", BeanA.class).b).isSameAs(context.getBean("b")); assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); - context.close(); } @Test void individualBeanWithSupplier() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(BeanA.class, () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); context.registerBean(BeanB.class, BeanB::new); @@ -154,14 +161,14 @@ class GenericApplicationContextTests { assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); - assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanB.class.getName())).isEqualTo(new String[] {BeanA.class.getName()}); - assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanC.class.getName())).isEqualTo(new String[] {BeanA.class.getName()}); - context.close(); + assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanB.class.getName())) + .containsExactly(BeanA.class.getName()); + assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanC.class.getName())) + .containsExactly(BeanA.class.getName()); } @Test void individualBeanWithSupplierAndCustomizer() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean(BeanA.class, () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), bd -> bd.setLazyInit(true)); @@ -173,12 +180,10 @@ class GenericApplicationContextTests { assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); - context.close(); } @Test void individualNamedBeanWithSupplier() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean("a", BeanA.class, () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); context.registerBean("b", BeanB.class, BeanB::new); @@ -189,12 +194,10 @@ class GenericApplicationContextTests { assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); - context.close(); } @Test void individualNamedBeanWithSupplierAndCustomizer() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean("a", BeanA.class, () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), bd -> bd.setLazyInit(true)); @@ -206,33 +209,63 @@ class GenericApplicationContextTests { assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); - context.close(); } @Test void individualBeanWithNullReturningSupplier() { - GenericApplicationContext context = new GenericApplicationContext(); context.registerBean("a", BeanA.class, () -> null); context.registerBean("b", BeanB.class, BeanB::new); context.registerBean("c", BeanC.class, BeanC::new); context.refresh(); - assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanA.class), "a")).isTrue(); - assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanB.class), "b")).isTrue(); - assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue(); - assertThat(context.getBeansOfType(BeanA.class).isEmpty()).isTrue(); - assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class)); - assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class)); - context.close(); + assertThat(context.getBeanNamesForType(BeanA.class)).containsExactly("a"); + assertThat(context.getBeanNamesForType(BeanB.class)).containsExactly("b"); + assertThat(context.getBeanNamesForType(BeanC.class)).containsExactly("c"); + assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); + assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()) + .isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()) + .isSameAs(context.getBean(BeanC.class)); } @Test void configureApplicationStartupOnBeanFactory() { FlightRecorderApplicationStartup applicationStartup = new FlightRecorderApplicationStartup(); - GenericApplicationContext context = new GenericApplicationContext(); context.setApplicationStartup(applicationStartup); assertThat(context.getBeanFactory().getApplicationStartup()).isEqualTo(applicationStartup); - context.close(); + } + + @Test + void getResourceWithDefaultResourceLoader() { + assertGetResourceSemantics(null, ClassPathResource.class); + } + + @Test + void getResourceWithCustomResourceLoader() { + assertGetResourceSemantics(new FileSystemResourceLoader(), FileSystemResource.class); + } + + private void assertGetResourceSemantics(ResourceLoader resourceLoader, Class defaultResouceType) { + if (resourceLoader != null) { + context.setResourceLoader(resourceLoader); + } + + String pingLocation = "ping:foo"; + String fileLocation = "file:foo"; + + Resource resource = context.getResource(pingLocation); + assertThat(resource).isInstanceOf(defaultResouceType); + resource = context.getResource(fileLocation); + assertThat(resource).isInstanceOf(FileUrlResource.class); + + context.addProtocolResolver(new PingPongProtocolResolver()); + + resource = context.getResource(pingLocation); + assertThat(resource).asInstanceOf(type(ByteArrayResource.class)) + .extracting(bar -> new String(bar.getByteArray(), UTF_8)) + .isEqualTo("pong:foo"); + resource = context.getResource(fileLocation); + assertThat(resource).isInstanceOf(FileUrlResource.class); } @Test @@ -455,4 +488,15 @@ class GenericApplicationContextTests { } } + static class PingPongProtocolResolver implements ProtocolResolver { + + @Override + public Resource resolve(String location, ResourceLoader resourceLoader) { + if (location.startsWith("ping:")) { + return new ByteArrayResource(("pong:" + location.substring(5)).getBytes(UTF_8)); + } + return null; + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java index 27ed3bf1a0..ef24b3803e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -32,7 +32,8 @@ import org.springframework.util.StringUtils; /** * Default implementation of the {@link ResourceLoader} interface. - * Used by {@link ResourceEditor}, and serves as base class for + * + *

Used by {@link ResourceEditor}, and serves as base class for * {@link org.springframework.context.support.AbstractApplicationContext}. * Can also be used standalone. * @@ -114,6 +115,7 @@ public class DefaultResourceLoader implements ResourceLoader { * Return the collection of currently registered protocol resolvers, * allowing for introspection as well as modification. * @since 4.3 + * @see #addProtocolResolver(ProtocolResolver) */ public Collection getProtocolResolvers() { return this.protocolResolvers;