From 3209d7f12620c6b97c8cc71c45c55cf4987361e0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 17:13:17 -0700 Subject: [PATCH] Add InstanceSupplier and RegisteredBean support Add a new `InstanceSupplier` interface that can be used as an alternative to a regular bean `Supplier` when details about the bean being supplied are required to instantiate it. The new interface accepts a `RegisteredBean` instance which provides access to the bean name, the bean factory creating the bean and the bean definition. This interface is primarily designed to allow AOT generated code to autowire dependencies into the instance. See gh-28414 --- .../AbstractAutowireCapableBeanFactory.java | 37 ++- .../factory/support/InstanceSupplier.java | 97 +++++++ .../beans/factory/support/RegisteredBean.java | 268 ++++++++++++++++++ .../support/BeanFactorySupplierTests.java | 91 ++++++ .../support/InstanceSupplierTests.java | 92 ++++++ .../factory/support/RegisteredBeanTests.java | 215 ++++++++++++++ 6 files changed, 789 insertions(+), 11 deletions(-) create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 8e363f7d091..a05635d1dee 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -79,6 +79,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingSupplier; /** * Abstract bean factory superclass that implements default bean creation, @@ -1199,19 +1200,40 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac /** * Obtain a bean instance from the given supplier. - * @param instanceSupplier the configured supplier + * @param supplier the configured supplier * @param beanName the corresponding bean name * @return a BeanWrapper for the new instance * @since 5.0 * @see #getObjectForBeanInstance */ - protected BeanWrapper obtainFromSupplier(Supplier instanceSupplier, String beanName) { - Object instance; + protected BeanWrapper obtainFromSupplier(Supplier supplier, String beanName) { + Object instance = obtainInstanceFromSupplier(supplier, beanName); + if (instance == null) { + instance = new NullBean(); + } + BeanWrapper bw = new BeanWrapperImpl(instance); + initBeanWrapper(bw); + return bw; + } + private Object obtainInstanceFromSupplier(Supplier supplier, String beanName) { String outerBean = this.currentlyCreatedBean.get(); this.currentlyCreatedBean.set(beanName); try { - instance = instanceSupplier.get(); + if (supplier instanceof InstanceSupplier instanceSupplier) { + return instanceSupplier.get(RegisteredBean.of(this, beanName)); + } + if (supplier instanceof ThrowingSupplier throwableSupplier) { + return throwableSupplier.getWithException(); + } + return supplier.get(); + } + catch (Throwable ex) { + if (ex instanceof BeansException beansException) { + throw beansException; + } + throw new BeanCreationException(beanName, + "Instantiation of supplied bean failed", ex); } finally { if (outerBean != null) { @@ -1221,13 +1243,6 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac this.currentlyCreatedBean.remove(); } } - - if (instance == null) { - instance = new NullBean(); - } - BeanWrapper bw = new BeanWrapperImpl(instance); - initBeanWrapper(bw); - return bw; } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java new file mode 100644 index 00000000000..3eede1f0884 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java @@ -0,0 +1,97 @@ +/* + * 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. + * 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.beans.factory.support; + +import java.util.function.Supplier; + +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingBiFunction; +import org.springframework.util.function.ThrowingSupplier; + +/** + * Specialized {@link Supplier} that can be set on a + * {@link AbstractBeanDefinition#setInstanceSupplier(Supplier) BeanDefinition} + * when details about the {@link RegisteredBean registered bean} are needed to + * supply the instance. + * + * @author Phillip Webb + * @since 6.0 + * @param the type of instance supplied by this supplier + * @see RegisteredBean + */ +@FunctionalInterface +public interface InstanceSupplier extends ThrowingSupplier { + + @Override + default T getWithException() { + throw new IllegalStateException("No RegisteredBean parameter provided"); + } + + /** + * Gets the supplied instance. + * @param registeredBean the registered bean requesting the instance + * @return the supplied instance + * @throws Exception on error + */ + T get(RegisteredBean registeredBean) throws Exception; + + /** + * Return a composed instance supplier that first obtains the instance from + * this supplier, and then applied the {@code after} function to obtain the + * result. + * @param the type of output of the {@code after} function, and of the + * composed function + * @param after the function to apply after the instance is obtained + * @return a composed instance supplier + */ + default InstanceSupplier andThen( + ThrowingBiFunction after) { + Assert.notNull(after, "After must not be null"); + return registeredBean -> after.applyWithException(registeredBean, + get(registeredBean)); + } + + /** + * Factory method to create an {@link InstanceSupplier} from a + * {@link ThrowingSupplier}. + * @param the type of instance supplied by this supplier + * @param supplier the source supplier + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier using(ThrowingSupplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + if (supplier instanceof InstanceSupplier instanceSupplier) { + return instanceSupplier; + } + return registeredBean -> supplier.getWithException(); + } + + /** + * Lambda friendly method that can be used to create a + * {@link InstanceSupplier} and add post processors in a single call. For + * example: {@code + * InstanceSupplier.of(registeredBean -> ...).withPostProcessor(...)}. + * @param the type of instance supplied by this supplier + * @param instanceSupplier the source instance supplier + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier of(InstanceSupplier instanceSupplier) { + Assert.notNull(instanceSupplier, "InstanceSupplier must not be null"); + return instanceSupplier; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java new file mode 100644 index 00000000000..a8d3f0fa4f3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java @@ -0,0 +1,268 @@ +/* + * 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. + * 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.beans.factory.support; + +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * A {@code RegisteredBean} represents a bean that has been registered with a + * {@link BeanFactory}, but has not necessarily been instantiated. It provides + * access to the bean factory that contains the bean as well as the bean name. + * In the case of inner-beans, the bean name may have been generated. + * + * @author Phillip Webb + * @since 6.0 + */ +public final class RegisteredBean { + + private final ConfigurableBeanFactory beanFactory; + + private final Supplier beanName; + + private final boolean generatedBeanName; + + private final Supplier mergedBeanDefinition; + + @Nullable + private final RegisteredBean parent; + + + private RegisteredBean(ConfigurableBeanFactory beanFactory, Supplier beanName, + boolean generatedBeanName, Supplier mergedBeanDefinition, + @Nullable RegisteredBean parent) { + + this.beanFactory = beanFactory; + this.beanName = beanName; + this.generatedBeanName = generatedBeanName; + this.mergedBeanDefinition = mergedBeanDefinition; + this.parent = parent; + } + + + /** + * Create a new {@link RegisteredBean} instance for a regular bean. + * @param beanFactory the source bean factory + * @param beanName the bean name + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean of(ConfigurableBeanFactory beanFactory, + String beanName) { + + Assert.notNull(beanFactory, "BeanFactory must not be null"); + Assert.hasLength(beanName, "BeanName must not be empty"); + return new RegisteredBean(beanFactory, () -> beanName, false, + () -> (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName), + null); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBean a {@link BeanDefinitionHolder} for the inner bean + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, + BeanDefinitionHolder innerBean) { + + Assert.notNull(innerBean, "InnerBean must not be null"); + return ofInnerBean(parent, innerBean.getBeanName(), + innerBean.getBeanDefinition()); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBeanDefinition the inner-bean definition + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, + BeanDefinition innerBeanDefinition) { + + return ofInnerBean(parent, null, innerBeanDefinition); + } + + /** + * Create a new {@link RegisteredBean} instance for an inner-bean. + * @param parent the parent of the inner-bean + * @param innerBeanName the name of the inner bean or {@code null} to + * generate a name + * @param innerBeanDefinition the inner-bean definition + * @return a new {@link RegisteredBean} instance + */ + public static RegisteredBean ofInnerBean(RegisteredBean parent, + @Nullable String innerBeanName, BeanDefinition innerBeanDefinition) { + + Assert.notNull(parent, "Parent must not be null"); + Assert.notNull(innerBeanDefinition, "InnerBeanDefinition must not be null"); + InnerBeanResolver resolver = new InnerBeanResolver(parent, innerBeanName, + innerBeanDefinition); + Supplier beanName = StringUtils.hasLength(innerBeanName) + ? () -> innerBeanName : resolver::resolveBeanName; + return new RegisteredBean(parent.getBeanFactory(), beanName, + innerBeanName == null, resolver::resolveMergedBeanDefinition, parent); + } + + + /** + * Return the name of the bean. + * @return the beanName the bean name + */ + public String getBeanName() { + return this.beanName.get(); + } + + /** + * Return if the bean name is generated. + * @return {@code true} if the name was generated + */ + public boolean isGeneratedBeanName() { + return this.generatedBeanName; + } + + /** + * Return the bean factory containing the bean. + * @return the bean factory + */ + public ConfigurableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + /** + * Return the user-defined class of the bean. + * @return the bean class + */ + public Class getBeanClass() { + if (this.beanFactory.containsSingleton(getBeanName())) { + return this.beanFactory.getSingleton(getBeanName()).getClass(); + } + return ClassUtils.getUserClass(getBeanType().toClass()); + } + + /** + * Return the {@link ResolvableType} of the bean. + * @return the bean type + */ + public ResolvableType getBeanType() { + if (this.beanFactory.containsSingleton(getBeanName())) { + return ResolvableType + .forInstance(this.beanFactory.getSingleton(getBeanName())); + } + return getMergedBeanDefinition().getResolvableType(); + } + + /** + * Return the merged bean definition of the bean. + * @return the merged bean definition + * @see ConfigurableBeanFactory#getMergedBeanDefinition(String) + */ + public RootBeanDefinition getMergedBeanDefinition() { + return this.mergedBeanDefinition.get(); + } + + /** + * Return if this instance is for an inner-bean. + * @return if an inner-bean + */ + public boolean isInnerBean() { + return this.parent != null; + } + + /** + * Return the parent of this instance or {@code null} if not an inner-bean. + * @return the parent + */ + @Nullable + public RegisteredBean getParent() { + return this.parent; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("beanName", getBeanName()) + .append("mergedBeanDefinition", getMergedBeanDefinition()).toString(); + } + + + /** + * Resolver used to obtain inner-bean details. + */ + private static class InnerBeanResolver { + + private final RegisteredBean parent; + + @Nullable + private final String innerBeanName; + + private final BeanDefinition innerBeanDefinition; + + @Nullable + private volatile String resolvedBeanName; + + + InnerBeanResolver(RegisteredBean parent, @Nullable String innerBeanName, + BeanDefinition innerBeanDefinition) { + + Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, + parent.getBeanFactory()); + this.parent = parent; + this.innerBeanName = innerBeanName; + this.innerBeanDefinition = innerBeanDefinition; + } + + + String resolveBeanName() { + String resolvedBeanName = this.resolvedBeanName; + if (resolvedBeanName != null) { + return resolvedBeanName; + } + resolvedBeanName = resolveInnerBean( + (beanName, mergedBeanDefinition) -> beanName); + this.resolvedBeanName = resolvedBeanName; + return resolvedBeanName; + } + + RootBeanDefinition resolveMergedBeanDefinition() { + return resolveInnerBean( + (beanName, mergedBeanDefinition) -> mergedBeanDefinition); + } + + private T resolveInnerBean( + BiFunction resolver) { + + // Always use a fresh BeanDefinitionValueResolver in case the parent merged bean definition has changed. + BeanDefinitionValueResolver beanDefinitionValueResolver = new BeanDefinitionValueResolver( + (AbstractAutowireCapableBeanFactory) this.parent.getBeanFactory(), + this.parent.getBeanName(), this.parent.getMergedBeanDefinition()); + return beanDefinitionValueResolver.resolveInnerBean(this.innerBeanName, + this.innerBeanDefinition, resolver); + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java new file mode 100644 index 00000000000..d07cd08abda --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactorySupplierTests.java @@ -0,0 +1,91 @@ +/* + * 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. + * 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.beans.factory.support; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.util.function.ThrowingSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractAutowireCapableBeanFactory} instance supplier + * support. + * + * @author Phillip Webb + */ +public class BeanFactorySupplierTests { + + @Test + void getBeanWhenUsingRegularSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(() -> "I am supplied"); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am supplied"); + } + + @Test + void getBeanWhenUsingInstanceSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(InstanceSupplier + .of(registeredBean -> "I am bean " + registeredBean.getBeanName())); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am bean test"); + } + + @Test + void getBeanWhenUsingThrowableSupplier() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> "I am supplied")); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThat(beanFactory.getBean("test")).isEqualTo("I am supplied"); + } + + @Test + void getBeanWhenUsingThrowableSupplierThatThrowsCheckedException() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> { + throw new IOException("fail"); + })); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean("test")) + .withCauseInstanceOf(IOException.class); + } + + @Test + void getBeanWhenUsingThrowableSupplierThatThrowsRuntimeException() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setInstanceSupplier(ThrowingSupplier.of(() -> { + throw new IllegalStateException("fail"); + })); + beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean("test")) + .withCauseInstanceOf(IllegalStateException.class); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java new file mode 100644 index 00000000000..a995f1b4607 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/InstanceSupplierTests.java @@ -0,0 +1,92 @@ +/* + * 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. + * 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.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.function.ThrowingBiFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link InstanceSupplier}. + * + * @author Phillip Webb + */ +class InstanceSupplierTests { + + private final RegisteredBean registeredBean = RegisteredBean + .of(new DefaultListableBeanFactory(), "test"); + + @Test + void getWithoutRegisteredBeanThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThatIllegalStateException().isThrownBy(() -> supplier.get()) + .withMessage("No RegisteredBean parameter provided"); + } + + @Test + void getWithExceptionWithoutRegisteredBeanThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThatIllegalStateException().isThrownBy(() -> supplier.getWithException()) + .withMessage("No RegisteredBean parameter provided"); + } + + @Test + void getReturnsResult() throws Exception { + InstanceSupplier supplier = registeredBean -> "test"; + assertThat(supplier.get(this.registeredBean)).isEqualTo("test"); + } + + @Test + void andThenWithBiFunctionWhenFunctionIsNullThrowsException() { + InstanceSupplier supplier = registeredBean -> "test"; + ThrowingBiFunction after = null; + assertThatIllegalArgumentException().isThrownBy(() -> supplier.andThen(after)) + .withMessage("After must not be null"); + } + + @Test + void andThenWithBiFunctionAppliesFunctionToObtainResult() throws Exception { + InstanceSupplier supplier = registeredBean -> "bean"; + supplier = supplier.andThen( + (registeredBean, string) -> registeredBean.getBeanName() + "-" + string); + assertThat(supplier.get(this.registeredBean)).isEqualTo("test-bean"); + } + + @Test + void ofSupplierWhenInstanceSupplierReturnsSameInstance() { + InstanceSupplier supplier = registeredBean -> "test"; + assertThat(InstanceSupplier.of(supplier)).isSameAs(supplier); + } + + @Test + void usingSupplierAdaptsToInstanceSupplier() throws Exception { + InstanceSupplier instanceSupplier = InstanceSupplier.using(() -> "test"); + assertThat(instanceSupplier.get(this.registeredBean)).isEqualTo("test"); + } + + @Test + void ofInstanceSupplierAdaptsToInstanceSupplier() throws Exception { + InstanceSupplier instanceSupplier = InstanceSupplier + .of(registeredBean -> "test"); + assertThat(instanceSupplier.get(this.registeredBean)).isEqualTo("test"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java new file mode 100644 index 00000000000..19f9e0bc44e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/RegisteredBeanTests.java @@ -0,0 +1,215 @@ +/* + * 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. + * 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.beans.factory.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinitionHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link RegisteredBean}. + * + * @author Phillip Webb + * @since 6.0 + */ +class RegisteredBeanTests { + + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + this.beanFactory.registerBeanDefinition("bd", + new RootBeanDefinition(TestBean.class)); + this.beanFactory.registerSingleton("sb", new TestBean()); + } + + @Test + void ofWhenBeanFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.of(null, "bd")) + .withMessage("BeanFactory must not be null"); + } + + @Test + void ofWhenBeanNameIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.of(this.beanFactory, null)) + .withMessage("BeanName must not be empty"); + } + + @Test + void ofInnerBeanWhenInnerBeanIsNullThrowsException() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + assertThatIllegalArgumentException().isThrownBy( + () -> RegisteredBean.ofInnerBean(parent, (BeanDefinitionHolder) null)) + .withMessage("InnerBean must not be null"); + } + + @Test + void ofInnerBeanWhenParentIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.ofInnerBean(null, + new RootBeanDefinition(TestInnerBean.class))) + .withMessage("Parent must not be null"); + } + + @Test + void ofInnerBeanWhenInnerBeanDefinitionIsNullThrowsException() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + assertThatIllegalArgumentException() + .isThrownBy(() -> RegisteredBean.ofInnerBean(parent, "ib", null)) + .withMessage("InnerBeanDefinition must not be null"); + } + + @Test + void getBeanNameReturnsBeanName() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanName()).isEqualTo("bd"); + } + + @Test + void getBeanNameWhenNamedInnerBeanReturnsBeanName() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, "ib", + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getBeanName()).isEqualTo("ib"); + } + + @Test + void getBeanNameWhenUnnamedInnerBeanReturnsBeanName() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getBeanName()).startsWith("(inner bean)#"); + } + + @Test + void getBeanClassReturnsBeanClass() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanClass()).isEqualTo(TestBean.class); + } + + @Test + void getBeanClassWhenSingletonReturnsBeanClass() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "sb"); + assertThat(registeredBean.getBeanClass()).isEqualTo(TestBean.class); + } + + @Test + void getBeanTypeReturnsBeanType() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getBeanType().toClass()).isEqualTo(TestBean.class); + } + + @Test + void getBeanTypeWhenSingletonReturnsBeanType() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "sb"); + assertThat(registeredBean.getBeanType().toClass()).isEqualTo(TestBean.class); + } + + @Test + void getMergedBeanDefinitionReturnsMergedBeanDefinition() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getMergedBeanDefinition().getBeanClass()) + .isEqualTo(TestBean.class); + } + + @Test + void getMergedBeanDefinitionWhenSingletonThrowsException() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "sb"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> registeredBean.getMergedBeanDefinition()); + } + + @Test + void getMergedBeanDefinitionWhenInnerBeanReturnsMergedBeanDefinition() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getMergedBeanDefinition().getBeanClass()) + .isEqualTo(TestInnerBean.class); + } + + @Test + void isInnerBeanWhenInnerBeanReturnsTrue() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.isInnerBean()).isTrue(); + } + + @Test + void isInnerBeanWhenNotInnerBeanReturnsTrue() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.isInnerBean()).isFalse(); + } + + @Test + void getParentWhenInnerBeanReturnsParent() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.getParent()).isSameAs(parent); + } + + @Test + void getParentWhenNotInnerBeanReturnsNull() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.getParent()).isNull(); + } + + @Test + void isGeneratedBeanNameWhenInnerBeanWithoutNameReturnsTrue() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new RootBeanDefinition(TestInnerBean.class)); + assertThat(registeredBean.isGeneratedBeanName()).isTrue(); + } + + @Test + void isGeneratedBeanNameWhenInnerBeanWithNameReturnsFalse() { + RegisteredBean parent = RegisteredBean.of(this.beanFactory, "bd"); + RegisteredBean registeredBean = RegisteredBean.ofInnerBean(parent, + new BeanDefinitionHolder(new RootBeanDefinition(TestInnerBean.class), + "test")); + assertThat(registeredBean.isGeneratedBeanName()).isFalse(); + } + + @Test + void isGeneratedBeanNameWhenNotInnerBeanReturnsFalse() { + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "bd"); + assertThat(registeredBean.isGeneratedBeanName()).isFalse(); + } + + static class TestBean { + + } + + static class TestInnerBean { + + } + +}