From 1047e1f7225c91a9a0fb4441957895f7b67299f4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Jun 2024 16:53:10 +0200 Subject: [PATCH] Declare complete set of default methods on ObjectProvider Closes gh-33070 --- .../beans/factory/ObjectProvider.java | 61 ++++++++- .../support/StaticListableBeanFactory.java | 7 +- .../factory/CustomObjectProviderTests.java | 126 ++++++++++++++++++ 3 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/CustomObjectProviderTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java index a9dc61eea42..465162e0ba2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -22,16 +22,27 @@ import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.beans.BeansException; +import org.springframework.core.OrderComparator; import org.springframework.lang.Nullable; /** * A variant of {@link ObjectFactory} designed specifically for injection points, * allowing for programmatic optionality and lenient not-unique handling. * + *

In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained + * from the factory will be bound to its {@code BeanFactory} for a specific bean + * type, matching all provider calls against factory-registered bean definitions. + * *

As of 5.1, this interface extends {@link Iterable} and provides {@link Stream} * support. It can be therefore be used in {@code for} loops, provides {@link #forEach} * iteration and allows for collection-style {@link #stream} access. * + *

As of 6.2, this interface declares default implementations for all methods. + * This makes it easier to implement in a custom fashion, e.g. for unit tests. + * For typical purposes, implement {@link #stream()} to enable all other methods. + * Alternatively, you may implement the specific methods that your callers expect, + * e.g. just {@link #getObject()} or {@link #getIfAvailable()}. + * * @author Juergen Hoeller * @since 4.3 * @param the object type @@ -40,6 +51,19 @@ import org.springframework.lang.Nullable; */ public interface ObjectProvider extends ObjectFactory, Iterable { + @Override + default T getObject() throws BeansException { + Iterator it = iterator(); + if (!it.hasNext()) { + throw new NoSuchBeanDefinitionException(Object.class); + } + T result = it.next(); + if (it.hasNext()) { + throw new NoUniqueBeanDefinitionException(Object.class, 2, "more than 1 matching bean"); + } + return result; + } + /** * Return an instance (possibly shared or independent) of the object * managed by this factory. @@ -50,7 +74,10 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @throws BeansException in case of creation errors * @see #getObject() */ - T getObject(Object... args) throws BeansException; + default T getObject(Object... args) throws BeansException { + throw new UnsupportedOperationException("Retrieval with arguments not supported -" + + "for custom ObjectProvider classes, implement getObject(Object...) for your purposes"); + } /** * Return an instance (possibly shared or independent) of the object @@ -60,7 +87,17 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @see #getObject() */ @Nullable - T getIfAvailable() throws BeansException; + default T getIfAvailable() throws BeansException { + try { + return getObject(); + } + catch (NoUniqueBeanDefinitionException ex) { + throw ex; + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } /** * Return an instance (possibly shared or independent) of the object @@ -103,7 +140,14 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @see #getObject() */ @Nullable - T getIfUnique() throws BeansException; + default T getIfUnique() throws BeansException { + try { + return getObject(); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } /** * Return an instance (possibly shared or independent) of the object @@ -157,7 +201,8 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * @see #orderedStream() */ default Stream stream() { - throw new UnsupportedOperationException("Multi element access not supported"); + throw new UnsupportedOperationException("Element access not supported - " + + "for custom ObjectProvider classes, implement stream() to enable all other methods"); } /** @@ -168,12 +213,16 @@ public interface ObjectProvider extends ObjectFactory, Iterable { * and in case of annotation-based configuration also considering the * {@link org.springframework.core.annotation.Order} annotation, * analogous to multi-element injection points of list/array type. + *

The default method applies an {@link OrderComparator} to the + * {@link #stream()} method. You may override this to apply an + * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * if necessary. * @since 5.1 * @see #stream() * @see org.springframework.core.OrderComparator */ default Stream orderedStream() { - throw new UnsupportedOperationException("Ordered element access not supported"); + return stream().sorted(OrderComparator.INSTANCE); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java index e93b7da2e35..c9bfc61de58 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -37,7 +37,6 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.core.OrderComparator; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.lang.Nullable; @@ -344,10 +343,6 @@ public class StaticListableBeanFactory implements ListableBeanFactory { public Stream stream() { return Arrays.stream(getBeanNamesForType(requiredType)).map(name -> (T) getBean(name)); } - @Override - public Stream orderedStream() { - return stream().sorted(OrderComparator.INSTANCE); - } }; } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/CustomObjectProviderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/CustomObjectProviderTests.java new file mode 100644 index 00000000000..278753c0bdb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/CustomObjectProviderTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2024 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; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @since 6.2 + */ +public class CustomObjectProviderTests { + + @Test + void getObject() { + TestBean tb1 = new TestBean("tb1"); + + ObjectProvider provider = new ObjectProvider<>() { + @Override + public TestBean getObject() throws BeansException { + return tb1; + } + }; + + assertThat(provider.getObject()).isSameAs(tb1); + assertThat(provider.getIfAvailable()).isSameAs(tb1); + assertThat(provider.getIfUnique()).isSameAs(tb1); + } + + @Test + void noObject() { + ObjectProvider provider = new ObjectProvider<>() { + @Override + public TestBean getObject() throws BeansException { + throw new NoSuchBeanDefinitionException(Object.class); + } + }; + + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(provider::getObject); + assertThat(provider.getIfAvailable()).isNull(); + assertThat(provider.getIfUnique()).isNull(); + } + + @Test + void noUniqueObject() { + ObjectProvider provider = new ObjectProvider<>() { + @Override + public TestBean getObject() throws BeansException { + throw new NoUniqueBeanDefinitionException(Object.class); + } + }; + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(provider::getObject); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(provider::getIfAvailable); + assertThat(provider.getIfUnique()).isNull(); + } + + @Test + void emptyStream() { + ObjectProvider provider = new ObjectProvider<>() { + @Override + public Stream stream() { + return Stream.empty(); + } + }; + + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(provider::getObject); + assertThat(provider.getIfAvailable()).isNull(); + assertThat(provider.getIfUnique()).isNull(); + } + + @Test + void streamWithOneObject() { + TestBean tb1 = new TestBean("tb1"); + + ObjectProvider provider = new ObjectProvider<>() { + @Override + public Stream stream() { + return Stream.of(tb1); + } + }; + + assertThat(provider.getObject()).isSameAs(tb1); + assertThat(provider.getIfAvailable()).isSameAs(tb1); + assertThat(provider.getIfUnique()).isSameAs(tb1); + } + + @Test + void streamWithTwoObjects() { + TestBean tb1 = new TestBean("tb1"); + TestBean tb2 = new TestBean("tb2"); + + ObjectProvider provider = new ObjectProvider<>() { + @Override + public Stream stream() { + return Stream.of(tb1, tb2); + } + }; + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(provider::getObject); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(provider::getIfAvailable); + assertThat(provider.getIfUnique()).isNull(); + } + +}