From c6741114ae6801ba162f66e7116ee60f26bee889 Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 6 Apr 2020 09:28:15 -0700 Subject: [PATCH] DATAGEODE-326 - Add utility method in SpringUtils to get beans by type ordered by the @Order annotation or by the Ordered interface. --- .../data/gemfire/util/SpringUtils.java | 147 +++++++ ...onContextBeanOrderingIntegrationTests.java | 395 ++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 spring-data-geode/src/test/java/org/springframework/context/ApplicationContextBeanOrderingIntegrationTests.java diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java index 0b79c2ef..82558b42 100644 --- a/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/util/SpringUtils.java @@ -21,17 +21,30 @@ import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.core.annotation.OrderUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -77,6 +90,90 @@ public abstract class SpringUtils { return beanDefinition; } + @NonNull + public static List getBeansOfTypeOrdered(@NonNull ConfigurableListableBeanFactory beanFactory, + @NonNull Class beanType) { + + return getBeansOfTypeOrdered(beanFactory, beanType, true, true); + } + + @NonNull + public static List getBeansOfTypeOrdered(@NonNull ConfigurableListableBeanFactory beanFactory, + @NonNull Class beanType, boolean includeNonSingletons, boolean allowEagerInit) { + + Assert.notNull(beanFactory, "BeanFactory must not be null"); + Assert.notNull(beanType, "Bean type must not be null"); + + Map beansOfType = beanFactory.getBeansOfType(beanType, includeNonSingletons, allowEagerInit); + + Set beanNamesOfType = new HashSet<>(beansOfType.keySet()); + + // Handles @Order annotated beans and beans implementing the Ordered interface + List> orderedBeansOfType = + CollectionUtils.nullSafeMap(beansOfType).entrySet().stream() + .map(SpringUtils::toOrderedBeanWrapper) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + Set orderedBeanNamesOfType = orderedBeansOfType.stream() + .map(OrderedBeanWrapper::getBeanName) + .collect(Collectors.toSet()); + + Set unorderedBeanNamesOfType = new HashSet<>(beanNamesOfType); + + // Set Difference + unorderedBeanNamesOfType.removeAll(orderedBeanNamesOfType); + + orderedBeansOfType.addAll(orderUnorderedBeans(beanFactory, beansOfType, unorderedBeanNamesOfType)); + orderedBeansOfType.sort(AnnotationAwareOrderComparator.INSTANCE); + + return orderedBeansOfType.stream() + .map(OrderedBeanWrapper::getBean) + .collect(Collectors.toList()); + } + + private static List> orderUnorderedBeans(@NonNull ConfigurableListableBeanFactory beanFactory, + @NonNull Map beansOfType, @NonNull Set unorderedBeanNames) { + + List> orderedBeanWrappers = new ArrayList<>(unorderedBeanNames.size()); + + for (String beanName : unorderedBeanNames) { + + Integer order = Optional.ofNullable(beanName) + .filter(StringUtils::hasText) + .map(beanFactory::getBeanDefinition) + .filter(AnnotatedBeanDefinition.class::isInstance) + .map(AnnotatedBeanDefinition.class::cast) + .map(AnnotatedBeanDefinition::getFactoryMethodMetadata) + .filter(methodMetadata -> methodMetadata.isAnnotated(Order.class.getName())) + .map(methodMetadata -> methodMetadata.getAnnotationAttributes(Order.class.getName())) + .map(annotationAttributes -> annotationAttributes.getOrDefault("value", Ordered.LOWEST_PRECEDENCE)) + .map(Integer.class::cast) + .orElse(Ordered.LOWEST_PRECEDENCE); + + orderedBeanWrappers.add(DefaultOrderedBeanWrapper.from(beanName, beansOfType.get(beanName), order)); + } + + return orderedBeanWrappers; + } + + @Nullable + private static OrderedBeanWrapper toOrderedBeanWrapper(@NonNull Map.Entry beanEntry) { + + T bean = beanEntry.getValue(); + + Integer order = bean instanceof Ordered + ? ((Ordered) bean).getOrder() + : Optional.ofNullable(bean) + .map(Object::getClass) + .map(OrderUtils::getOrder) + .orElse(null); + + return order != null + ? DefaultOrderedBeanWrapper.from(beanEntry.getKey(), bean, order) + : null; + } + public static Optional getPropertyValue(BeanDefinition beanDefinition, String propertyName) { return Optional.ofNullable(beanDefinition) @@ -190,6 +287,56 @@ public abstract class SpringUtils { } } + private static class DefaultOrderedBeanWrapper implements OrderedBeanWrapper { + + private static OrderedBeanWrapper from(String beanName, T bean) { + return from(beanName, bean, Ordered.LOWEST_PRECEDENCE); + } + + private static OrderedBeanWrapper from(String beanName, T bean, int order) { + return new DefaultOrderedBeanWrapper<>(beanName, bean, order); + } + + private final int order; + + private final T bean; + + private final String beanName; + + private DefaultOrderedBeanWrapper(String beanName, T bean, int order) { + + Assert.notNull(bean, "Bean must not be null"); + Assert.hasText(beanName, "Bean name is required"); + + this.order = order; + this.bean = bean; + this.beanName = beanName; + } + + @Override + public T getBean() { + return this.bean; + } + + @Override + public String getBeanName() { + return this.beanName; + } + + @Override + public int getOrder() { + return this.order; + } + } + + public interface OrderedBeanWrapper extends Ordered { + + T getBean(); + + String getBeanName(); + + } + @FunctionalInterface public interface ValueReturningThrowableOperation { T get() throws Throwable; diff --git a/spring-data-geode/src/test/java/org/springframework/context/ApplicationContextBeanOrderingIntegrationTests.java b/spring-data-geode/src/test/java/org/springframework/context/ApplicationContextBeanOrderingIntegrationTests.java new file mode 100644 index 00000000..0fe23d94 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/context/ApplicationContextBeanOrderingIntegrationTests.java @@ -0,0 +1,395 @@ +/* + * Copyright 2020 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.Order; +import org.springframework.core.annotation.OrderUtils; +import org.springframework.data.gemfire.util.SpringUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration Tests testing the bean ordering applied by the Spring {@link ApplicationContext} + * or Spring {@link BeanFactory} when using the {@link Order} annotation or implementing the {@link Ordered} interface. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.beans.factory.ListableBeanFactory + * @see org.springframework.core.Ordered + * @see org.springframework.core.annotation.Order + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 2.3.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class ApplicationContextBeanOrderingIntegrationTests { + + @Autowired + private ConfigurableApplicationContext applicationContext; + + @Autowired + private NamedBean[] namedBeans; + + /** + * 1. Auto-wiring/Dependency Injection (DI) does exactly what I'd like to do programmatically using some API + * on a {@link BeanFactory} or an {@link ApplicationContext}. + */ + @Test + public void autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface() { + + List beanNames = Arrays.stream(this.namedBeans) + .map(Object::toString) + .collect(Collectors.toList()); + + assertThat(beanNames) + .describedAs("Expected [Y, X, Z, B, C, A, D, U]; but was %s", beanNames) + .containsExactly("Y", "X", "Z", "B", "C", "A", "D", "U"); + } + + /** + * 2. The Javadoc is pretty precise about bean ordering... + * + * {@literal The Map returned by this method should always return bean names and corresponding bean instances + * in the order of definition in the backend configuration, as far as possible.} + * + * @see ListableBeanFactory#getBeansOfType(Class) + * @see ListableBeanFactory.getBeansOfType(:Class) + */ + @Test + public void beansAreOrderedByBeanDefinitionDeclarationOrder() { + + List beanNames = this.applicationContext.getBeansOfType(NamedBean.class).values().stream() + .map(Object::toString) + .collect(Collectors.toList()); + + assertThat(beanNames) + .describedAs("Expected [A, B, C, D, U, X, Y, Z]; but was %s", beanNames) + .containsExactly("A", "B", "C", "D", "U", "X", "Y", "Z"); + } + + /** + * 3. The Javadoc is pretty precise about bean name ordering... + * + * {@literal Bean names returned by this method should always return bean names in the order of definition + * in the backend configuration, as far as possible.} + * + * @see ListableBeanFactory#getBeanNamesForType(Class) + * @see ListableBeanFactory.getBeanNamesForType(:Class) + */ + @Test + public void beanNamesAreOrderedByBeanDefinitionDeclarationOrder() { + + List beanNames = Arrays.asList(this.applicationContext.getBeanNamesForType(NamedBean.class)); + + assertThat(beanNames) + .describedAs("Expected [A, B, C, D, U, X, Y, Z]; but was %s", beanNames) + .containsExactly("A", "B", "C", "D", "U", "X", "Y", "Z"); + } + + /** + * 4. Test Fails (of course)! + * + * Like the bean name ordering specification called out in the Javadoc, {@link ListableBeanFactory#getBeansOfType(Class)} + * is exactly like {@link ListableBeanFactory#getBeanNamesForType(Class)}. That is... + * + * {@literal The Map returned by this method should always return bean names and corresponding bean instances + * in the order of definition in the backend configuration, as far as possible.} + * + * However, is there a programmatical means (i.e. API) to do what Auto-wiring/Dependency Injection (DI) + * (i.e. using {@link Autowired}) does as tested in the + * {@link #autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface()} test case? + * + * This test case demonstrates what I'd like to happen (using a different API call given the contract + * of the existing method). + * + * @see #autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface() + */ + //@Test + public void expectBeansToBeOrderedByOrderAnnotationAndOrderedInterface() { + + List beanNames = this.applicationContext.getBeansOfType(NamedBean.class).values().stream() + .map(Object::toString) + .collect(Collectors.toList()); + + assertThat(beanNames) + .describedAs("Expected [Y, X, Z, B, C, A, D, U]; but was %s", beanNames) + .containsExactly("Y", "X", "Z", "B", "C", "A", "D", "U"); + } + + /** + * 5. Test Fails (of course)! + * + * This test case demonstrates what I'd like to happen (using a different API call given the contract + * of the existing method). + * + * @see #autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface() + */ + //@Test + public void expectBeanNamesToBeOrderedByOrderAnnotationAndOrderedInterface() { + + List beanNames = Arrays.asList(this.applicationContext.getBeanNamesForType(NamedBean.class)); + + assertThat(beanNames) + .describedAs("Expected [Y, X, Z, B, C, A, D, U]; but was %s", beanNames) + .containsExactly("Y", "X", "Z", "B", "C", "A", "D", "U"); + } + + /** + * 6. Test almost passes... my hack to simulate what I want. + * + * The hack does not handle {@link Ordered} interface implementations, though. That would require an instantiation + * unless the bean defined some conventions, such as a {@code public static final int} {@literal ORDER} field + * that could be introspected reflectively. :-P + * + * I used {@link ListableBeanFactory#getBeanNamesForType(Class, boolean, boolean)} to avoid + * eager bean initialization as far as possible. + * + * @see #autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface() + */ + //@Test + public void expectBeanNamesToBeOrderedByOrderAnnotationFromBeanDefinitionMetadata() { + + List beanNames = Arrays.stream(this.applicationContext.getBeanNamesForType(NamedBean.class, true, false)) + .map(this::toBeanDefinitionHolder) + .filter(Objects::nonNull) + .sorted(OrderAnnotatedBeanDefinitionComparator.INSTANCE) + .map(BeanDefinitionHolder::getBeanName) + .collect(Collectors.toList()); + + assertThat(beanNames) + .describedAs("Expected [Y, X, Z, B, C, A, D, U]; but was %s", beanNames) + .containsExactly("Y", "X", "Z", "B", "C", "A", "D", "U"); + } + + private BeanDefinitionHolder toBeanDefinitionHolder(String beanName) { + + return Optional.ofNullable(this.applicationContext) + .map(ConfigurableApplicationContext::getBeanFactory) + .map(beanFactory -> beanFactory.getBeanDefinition(beanName)) + .map(beanDefinition -> new BeanDefinitionHolder(beanDefinition, beanName)) + .orElse(null); + } + + @Test + public void expectBeansToBeOrderedByOrderAnnotationAndOrderedInterfaceUsingSpringUtils() { + + List beanNames = + SpringUtils.getBeansOfTypeOrdered(this.applicationContext.getBeanFactory(), NamedBean.class).stream() + .map(Object::toString) + .collect(Collectors.toList()); + + assertThat(beanNames) + .describedAs("Expected [Y, X, Z, B, C, A, D, U]; but was %s", beanNames) + .containsExactly("Y", "X", "Z", "B", "C", "A", "D", "U"); + } + + @Configuration + static class TestConfiguration { + + @Bean("A") + @Order(3) + NamedBean a() { + return new NamedBean(); + } + + @Bean("B") + @Order(1) + NamedBean b() { + return new NamedBean(); + } + + @Bean("C") + @Order(2) + NamedBean c() { + return new NamedBean(); + } + + @Bean("D") + @Order(4) + NamedBean d() { + return new NamedBean(); + } + + @Bean("U") + NamedBean unorderedNamedBean() { + return new NamedBean(); + } + + @Bean("X") + X x() { + return new X(); + } + + @Bean("Y") + Y y() { + return new Y(); + } + + @Bean("Z") + Z z() { + return new Z(); + } + } + + static class NamedBean implements BeanNameAware { + + private String name; + + public NamedBean() { } + + public NamedBean(String name) { + this.name = name; + } + + @Override + public void setBeanName(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + } + + @Order(0) + static abstract class AbstractZeroOrderedNamedBean extends NamedBean { + + AbstractZeroOrderedNamedBean() { } + + AbstractZeroOrderedNamedBean(String name) { + super(name); + } + } + + static class X extends NamedBean implements Ordered { + + X() { + super(X.class.getSimpleName()); + } + + @Override + public int getOrder() { + return -1; + } + } + + @Order(-2) + static class Y extends NamedBean { + + Y() { + super(Y.class.getSimpleName()); + } + } + + static class Z extends AbstractZeroOrderedNamedBean { + + Z() { + super(Z.class.getSimpleName()); + } + } + + static class OrderAnnotatedBeanDefinitionComparator implements Comparator { + + static final OrderAnnotatedBeanDefinitionComparator INSTANCE = new OrderAnnotatedBeanDefinitionComparator(); + + private final Map beanNameToOrder = new ConcurrentHashMap<>(); + + @Override + public int compare(BeanDefinitionHolder beanOne, BeanDefinitionHolder beanTwo) { + return getOrder(beanOne).compareTo(getOrder(beanTwo)); + } + + private Integer getOrder(@Nullable BeanDefinitionHolder bean) { + + return this.beanNameToOrder.computeIfAbsent(bean.getBeanName(), beanName -> { + + Integer order = getOrderFromBeanType(bean); + + return order != null ? order : getOrderFromFactoryMethod(bean); + }); + } + + private @Nullable Integer getOrderFromBeanType(@Nullable BeanDefinitionHolder bean) { + + return Optional.ofNullable(bean) + .map(BeanDefinitionHolder::getBeanDefinition) + .map(BeanDefinition::getResolvableType) + .map(ResolvableType::resolve) + .map(beanType -> OrderUtils.getOrder(beanType, Ordered.LOWEST_PRECEDENCE)) + .orElse(null); + + // Why does the following not work given the Javadoc for AnnotatedBeanDefinition.getMetadata() reads... + // "Obtain the annotation metadata (as well as basic class metadata) for this BEAN DEFINITION's 'BEAN CLASS'" + // See: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.html#getMetadata-- + // This does not work for BeanDefinitions from @Bean methods on @Configuration classes and seems like it should! + /* + return Optional.of(bean) + .map(BeanDefinitionHolder::getBeanDefinition) + .filter(AnnotatedBeanDefinition.class::isInstance) + .map(AnnotatedBeanDefinition.class::cast) + .map(AnnotatedBeanDefinition::getMetadata) + .filter(annotationMetadata -> annotationMetadata.hasAnnotation(Order.class.getName())) + .map(annotationMetadata -> annotationMetadata.getAnnotationAttributes(Order.class.getName())) + .map(AnnotationAttributes::fromMap) + .orElse(null); + */ + } + + private @NonNull Integer getOrderFromFactoryMethod(@NonNull BeanDefinitionHolder bean) { + + return Optional.of(bean) + .map(BeanDefinitionHolder::getBeanDefinition) + .filter(AnnotatedBeanDefinition.class::isInstance) + .map(AnnotatedBeanDefinition.class::cast) + .map(AnnotatedBeanDefinition::getFactoryMethodMetadata) + .filter(methodMetadata -> methodMetadata.isAnnotated(Order.class.getName())) + .map(methodMetadata -> methodMetadata.getAnnotationAttributes(Order.class.getName())) + .map(annotationAttributes -> annotationAttributes.getOrDefault("value", Ordered.LOWEST_PRECEDENCE)) + .map(Integer.class::cast) + .orElse(Ordered.LOWEST_PRECEDENCE); + } + } +}