diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java index 770f1a3c87..90575862c6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -151,9 +151,9 @@ class ComponentScanAnnotationParser { case CUSTOM: Assert.isAssignable(TypeFilter.class, filterClass, "@ComponentScan CUSTOM type filter requires a TypeFilter implementation"); - TypeFilter filter = BeanUtils.instantiateClass(filterClass, TypeFilter.class); - ParserStrategyUtils.invokeAwareMethods( - filter, this.environment, this.resourceLoader, this.registry); + + TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class, + this.environment, this.resourceLoader, this.registry); typeFilters.add(filter); break; default: diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 40917a7011..5be7fe892b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -559,9 +559,8 @@ class ConfigurationClassParser { if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports Class candidateClass = candidate.loadClass(); - ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class); - ParserStrategyUtils.invokeAwareMethods( - selector, this.environment, this.resourceLoader, this.registry); + ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, + this.environment, this.resourceLoader, this.registry); if (selector instanceof DeferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } @@ -576,9 +575,8 @@ class ConfigurationClassParser { // delegate to it to register additional bean definitions Class candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = - BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class); - ParserStrategyUtils.invokeAwareMethods( - registrar, this.environment, this.resourceLoader, this.registry); + ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, + this.environment, this.resourceLoader, this.registry); configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { @@ -822,8 +820,7 @@ class ConfigurationClassParser { private Group createGroup(@Nullable Class type) { Class effectiveType = (type != null ? type : DefaultDeferredImportSelectorGroup.class); - Group group = BeanUtils.instantiateClass(effectiveType); - ParserStrategyUtils.invokeAwareMethods(group, + Group group = ParserStrategyUtils.instantiateClass(effectiveType, Group.class, ConfigurationClassParser.this.environment, ConfigurationClassParser.this.resourceLoader, ConfigurationClassParser.this.registry); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Import.java b/spring-context/src/main/java/org/springframework/context/annotation/Import.java index 11adc55ce6..88e189c7f4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Import.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Import.java @@ -46,6 +46,7 @@ import java.lang.annotation.Target; * @since 3.0 * @see Configuration * @see ImportSelector + * @see ImportBeanDefinitionRegistrar * @see ImportResource */ @Target(ElementType.TYPE) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java index a453e4a7ac..68fdeda874 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java @@ -40,6 +40,15 @@ import org.springframework.core.type.AnnotationMetadata; *
  • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware} * * + *

    Alternatively, the class may provide a single constructor with one or more of + * the following supported parameter types: + *

    + * *

    See implementations and associated unit tests for usage examples. * * @author Chris Beams diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index 2e41305958..94edc6374e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -33,6 +33,15 @@ import org.springframework.core.type.AnnotationMetadata; *

  • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
  • * * + *

    Alternatively, the class may provide a single constructor with one or more of + * the following supported parameter types: + *

    + * *

    {@code ImportSelector} implementations are usually processed in the same way * as regular {@code @Import} annotations, however, it is also possible to defer * selection of imports until all {@code @Configuration} classes have been processed diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java index e5ff931e60..94f64af36c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java @@ -16,6 +16,10 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -26,31 +30,101 @@ import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Common delegate code for the handling of parser strategies, e.g. * {@code TypeFilter}, {@code ImportSelector}, {@code ImportBeanDefinitionRegistrar} * * @author Juergen Hoeller + * @author Phillip Webb * @since 4.3.3 */ abstract class ParserStrategyUtils { /** - * Invoke {@link BeanClassLoaderAware}, {@link BeanFactoryAware}, + * Instantiate a class using an appropriate constructor and return the new + * instance as the specified assignable type. The returned instance will + * have {@link BeanClassLoaderAware}, {@link BeanFactoryAware}, * {@link EnvironmentAware}, and {@link ResourceLoaderAware} contracts - * if implemented by the given object. + * invoked if they are implemented by the given object. + * @since 5.2 */ - public static void invokeAwareMethods(Object parserStrategyBean, Environment environment, - ResourceLoader resourceLoader, BeanDefinitionRegistry registry) { + @SuppressWarnings("unchecked") + static T instantiateClass(Class clazz, Class assignableTo, + Environment environment, ResourceLoader resourceLoader, + BeanDefinitionRegistry registry) { + + Assert.notNull(clazz, "Class must not be null"); + Assert.isAssignable(assignableTo, clazz); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) registry).getBeanClassLoader() : resourceLoader.getClassLoader()); + T instance = (T) createInstance(clazz, environment, resourceLoader, registry, classLoader); + ParserStrategyUtils.invokeAwareMethods(instance, environment, resourceLoader, registry, classLoader); + return instance; + } + + private static Object createInstance(Class clazz, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry, + @Nullable ClassLoader classLoader) { + + Constructor[] constructors = clazz.getDeclaredConstructors(); + if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { + try { + Constructor constructor = constructors[0]; + Object[] args = resolveArgs(constructor.getParameterTypes(), + environment, resourceLoader, registry, classLoader); + return BeanUtils.instantiateClass(constructor, args); + } + catch (Exception ex) { + throw new BeanInstantiationException(clazz, "No suitable constructor found", ex); + } + } + return BeanUtils.instantiateClass(clazz); + } + + private static Object[] resolveArgs(Class[] parameterTypes, + Environment environment, ResourceLoader resourceLoader, + BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { + + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameters[i] = resolveParameter(parameterTypes[i], environment, + resourceLoader, registry, classLoader); + } + return parameters; + } + + private static Object resolveParameter(Class parameterType, + Environment environment, ResourceLoader resourceLoader, + BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { + + if (parameterType == Environment.class) { + return environment; + } + if (parameterType == ResourceLoader.class) { + return resourceLoader; + } + if (parameterType == BeanFactory.class) { + return (registry instanceof BeanFactory) ? registry : null; + } + if (parameterType == ClassLoader.class) { + return classLoader; + } + throw new IllegalStateException( + "Illegal method parameter type " + parameterType.getName()); + } + + private static void invokeAwareMethods(Object parserStrategyBean, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry, ClassLoader classLoader) { if (parserStrategyBean instanceof Aware) { - if (parserStrategyBean instanceof BeanClassLoaderAware) { - ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory ? - ((ConfigurableBeanFactory) registry).getBeanClassLoader() : resourceLoader.getClassLoader()); - if (classLoader != null) { - ((BeanClassLoaderAware) parserStrategyBean).setBeanClassLoader(classLoader); - } + if (parserStrategyBean instanceof BeanClassLoaderAware && classLoader != null) { + ((BeanClassLoaderAware) parserStrategyBean).setBeanClassLoader(classLoader); } if (parserStrategyBean instanceof BeanFactoryAware && registry instanceof BeanFactory) { ((BeanFactoryAware) parserStrategyBean).setBeanFactory((BeanFactory) registry); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java new file mode 100644 index 0000000000..d5e5fc5466 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.context.annotation; + +import java.io.InputStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.reset; + +/** + * Tests for {@link ParserStrategyUtils}. + * + * @author Phillip Webb + */ +public class ParserStrategyUtilsTests { + + @Mock + private Environment environment; + + @Mock(extraInterfaces = BeanFactory.class) + private BeanDefinitionRegistry registry; + + @Mock + private ClassLoader beanClassLoader; + + @Mock + private ResourceLoader resourceLoader; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.resourceLoader.getClassLoader()).willReturn(this.beanClassLoader); + } + + @Test + public void instantiateClassWhenHasNoArgsConstructorCallsAware() { + NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); + assertThat(instance.setEnvironment).isSameAs(this.environment); + assertThat(instance.setBeanFactory).isSameAs(this.registry); + assertThat(instance.setBeanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.setResourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasSingleContructorInjectsParams() { + ArgsConstructor instance = instantiateClass(ArgsConstructor.class); + assertThat(instance.environment).isSameAs(this.environment); + assertThat(instance.beanFactory).isSameAs(this.registry); + assertThat(instance.beanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.resourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasSingleContructorAndAwareInjectsParamsAndCallsAware() { + ArgsConstructorAndAware instance = instantiateClass(ArgsConstructorAndAware.class); + assertThat(instance.environment).isSameAs(this.environment); + assertThat(instance.setEnvironment).isSameAs(this.environment); + assertThat(instance.beanFactory).isSameAs(this.registry); + assertThat(instance.setBeanFactory).isSameAs(this.registry); + assertThat(instance.beanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.setBeanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.resourceLoader).isSameAs(this.resourceLoader); + assertThat(instance.setResourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasMultipleConstructorsUsesNoArgsConstructor() { + // Remain back-compatible by using the default constructor if there's more then one + MultipleConstructors instance = instantiateClass(MultipleConstructors.class); + assertThat(instance.usedDefaultConstructor).isTrue(); + } + + @Test + public void instantiateClassWhenHasMutlipleConstructorsAndNotDefaultThrowsException() { + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(MultipleConstructorsWithNoDefault.class)); + } + + @Test + public void instantiateClassWhenHasUnsupportedParameterThrowsException() { + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(InvalidConstructorParameterType.class)) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("No suitable constructor found"); + } + + @Test + public void instantiateClassHasSubclassParameterThrowsException() { + // To keep the algorithm simple we don't support subtypes + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(InvalidConstructorParameterSubType.class)) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("No suitable constructor found"); + } + + @Test + public void instantiateClassWhenHasNoBeanClassLoaderInjectsNull() { + reset(this.resourceLoader); + ArgsConstructor instance = instantiateClass(ArgsConstructor.class); + assertThat(instance.beanClassLoader).isNull(); + } + + @Test + public void instantiateClassWhenHasNoBeanClassLoaderDoesNotCallAware() { + reset(this.resourceLoader); + NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); + assertThat(instance.setBeanClassLoader).isNull(); + assertThat(instance.setBeanClassLoaderCalled).isFalse(); + } + + private T instantiateClass(Class clazz) { + return ParserStrategyUtils.instantiateClass(clazz, clazz, this.environment, + this.resourceLoader, this.registry); + } + + static class NoArgsConstructor implements BeanClassLoaderAware, + BeanFactoryAware, EnvironmentAware, ResourceLoaderAware { + + Environment setEnvironment; + + BeanFactory setBeanFactory; + + ClassLoader setBeanClassLoader; + + boolean setBeanClassLoaderCalled; + + ResourceLoader setResourceLoader; + + @Override + public void setEnvironment(Environment environment) { + this.setEnvironment = environment; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.setBeanFactory = beanFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.setBeanClassLoader = classLoader; + this.setBeanClassLoaderCalled = true; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.setResourceLoader = resourceLoader; + } + + } + + static class ArgsConstructor { + + final Environment environment; + + final BeanFactory beanFactory; + + final ClassLoader beanClassLoader; + + final ResourceLoader resourceLoader; + + ArgsConstructor(Environment environment, BeanFactory beanFactory, + ClassLoader beanClassLoader, ResourceLoader resourceLoader) { + this.environment = environment; + this.beanFactory = beanFactory; + this.beanClassLoader = beanClassLoader; + this.resourceLoader = resourceLoader; + } + + } + + static class ArgsConstructorAndAware extends NoArgsConstructor { + + final Environment environment; + + final BeanFactory beanFactory; + + final ClassLoader beanClassLoader; + + final ResourceLoader resourceLoader; + + ArgsConstructorAndAware(Environment environment, BeanFactory beanFactory, + ClassLoader beanClassLoader, ResourceLoader resourceLoader) { + this.environment = environment; + this.beanFactory = beanFactory; + this.beanClassLoader = beanClassLoader; + this.resourceLoader = resourceLoader; + } + + } + + static class MultipleConstructors { + + final boolean usedDefaultConstructor; + + MultipleConstructors() { + this.usedDefaultConstructor = true; + } + + MultipleConstructors(Environment environment) { + this.usedDefaultConstructor = false; + } + + } + + static class MultipleConstructorsWithNoDefault { + + MultipleConstructorsWithNoDefault(Environment environment, BeanFactory beanFactory) { + } + + MultipleConstructorsWithNoDefault(Environment environment) { + } + + } + + static class InvalidConstructorParameterType { + + InvalidConstructorParameterType(Environment environment, InputStream inputStream) { + } + + } + + static class InvalidConstructorParameterSubType { + + InvalidConstructorParameterSubType(ConfigurableEnvironment environment) { + } + + } + + +}