Created
April 4, 2020 19:59
-
-
Save jxblum/42173fc08f9da60ebb3158db7d4380f7 to your computer and use it in GitHub Desktop.
Bean and Bean Name ordering following the @order annotation and Ordered Interface test class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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.ListableBeanFactory; | |
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; | |
import org.springframework.beans.factory.annotation.Autowired; | |
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.AnnotationAttributes; | |
import org.springframework.core.annotation.AnnotationUtils; | |
import org.springframework.core.annotation.Order; | |
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} | |
* when using the {@link Order} annotation. | |
* | |
* @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 5.2.5.RELEASE | |
*/ | |
@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<String> beanNames = Arrays.stream(this.namedBeans) | |
.map(Object::toString) | |
.collect(Collectors.toList()); | |
assertThat(beanNames) | |
.describedAs("Expected [Y, X, B, C, A, D]; but was %s", beanNames) | |
.containsExactly("Y", "X", "B", "C", "A", "D"); | |
} | |
/** | |
* 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 <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/ListableBeanFactory.html#getBeansOfType-java.lang.Class-">ListableBeanFactory.getBeansOfType(:Class)</a> | |
*/ | |
@Test | |
public void beansAreOrderedByBeanDefinitionDeclarationOrder() { | |
List<String> beanNames = this.applicationContext.getBeansOfType(NamedBean.class).values().stream() | |
.map(Object::toString) | |
.collect(Collectors.toList()); | |
assertThat(beanNames) | |
.describedAs("Expected [A, B, C, D, X, Y]; but was %s", beanNames) | |
.containsExactly("A", "B", "C", "D", "X", "Y"); | |
} | |
/** | |
* 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 <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/ListableBeanFactory.html#getBeanNamesForType-java.lang.Class-">ListableBeanFactory.getBeanNamesForType(:Class)</a> | |
*/ | |
@Test | |
public void beanNamesAreOrderedByBeanDefinitionDeclarationOrder() { | |
List<String> beanNames = Arrays.asList(this.applicationContext.getBeanNamesForType(NamedBean.class)); | |
assertThat(beanNames) | |
.describedAs("Expected [A, B, C, D, X, Y]; but was %s", beanNames) | |
.containsExactly("A", "B", "C", "D", "X", "Y"); | |
} | |
/** | |
* 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<String> beanNames = this.applicationContext.getBeansOfType(NamedBean.class).values().stream() | |
.map(Object::toString) | |
.collect(Collectors.toList()); | |
assertThat(beanNames) | |
.describedAs("Expected [Y, X, B, C, A, D]; but was %s", beanNames) | |
.containsExactly("Y", "X", "B", "C", "A", "D"); | |
} | |
/** | |
* 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<String> beanNames = Arrays.asList(this.applicationContext.getBeanNamesForType(NamedBean.class)); | |
assertThat(beanNames) | |
.describedAs("Expected [Y, X, B, C, A, D]; but was %s", beanNames) | |
.containsExactly("Y", "X", "B", "C", "A", "D"); | |
} | |
/** | |
* 6. Test almost passes... my hacky attempt 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)} to avoid eager bean initialization | |
* as far as possible. | |
* | |
* @see #autoWiredBeansAreOrderedByOrderAnnotationAndOrderedInterface() | |
*/ | |
@Test | |
public void expectBeanNamesToBeOrderedByOrderAnnotationFromBeanDefinitionMetadata() { | |
List<String> beanNames = Arrays.stream(this.applicationContext.getBeanNamesForType(NamedBean.class)) | |
.map(this::toBeanDefinitionHolder) | |
.filter(Objects::nonNull) | |
.sorted(OrderAnnotatedBeanDefinitionComparator.INSTANCE) | |
.map(BeanDefinitionHolder::getBeanName) | |
.collect(Collectors.toList()); | |
assertThat(beanNames) | |
.describedAs("Expected [Y, X, B, C, A, D]; but was %s", beanNames) | |
.containsExactly("Y", "X", "B", "C", "A", "D"); | |
} | |
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); | |
} | |
@Configuration | |
static class TestConfiguration { | |
@Bean("A") | |
@Order(3) | |
NamedBean a() { | |
return new NamedBean("A"); | |
} | |
@Bean("B") | |
@Order(1) | |
NamedBean b() { | |
return new NamedBean("B"); | |
} | |
@Bean("C") | |
@Order(2) | |
NamedBean c() { | |
return new NamedBean("C"); | |
} | |
@Bean("D") | |
@Order(4) | |
NamedBean d() { | |
return new NamedBean("D"); | |
} | |
@Bean("X") | |
X x() { | |
return new X(); | |
} | |
@Bean("Y") | |
Y y() { | |
return new Y(); | |
} | |
} | |
static class NamedBean { | |
private final String name; | |
public NamedBean(String name) { | |
this.name = name; | |
} | |
@Override | |
public String toString() { | |
return this.name; | |
} | |
} | |
static class X extends NamedBean implements Ordered { | |
public X() { | |
super("X"); | |
} | |
@Override | |
public int getOrder() { | |
return -1; | |
} | |
} | |
@Order(-2) | |
static class Y extends NamedBean { | |
public Y() { | |
super("Y"); | |
} | |
} | |
static class OrderAnnotatedBeanDefinitionComparator implements Comparator<BeanDefinitionHolder> { | |
static final OrderAnnotatedBeanDefinitionComparator INSTANCE = new OrderAnnotatedBeanDefinitionComparator(); | |
private final Map<String, Integer> beanNameToOrder = new ConcurrentHashMap<>(); | |
@Override | |
public int compare(BeanDefinitionHolder beanOne, BeanDefinitionHolder beanTwo) { | |
return getOrder(beanOne).compareTo(getOrder(beanTwo)); | |
} | |
private Integer getOrder(BeanDefinitionHolder bean) { | |
return this.beanNameToOrder.computeIfAbsent(bean.getBeanName(), beanName -> | |
Optional.ofNullable(bean) | |
.map(BeanDefinitionHolder::getBeanDefinition) | |
.filter(AnnotatedBeanDefinition.class::isInstance) | |
.map(AnnotatedBeanDefinition.class::cast) | |
.map(this::getOrderAnnotationAttributesFromFactoryMethod) | |
.map(this::getOrder) | |
.orElse(Ordered.LOWEST_PRECEDENCE)); | |
} | |
private Integer getOrder(AnnotationAttributes annotationAttributes) { | |
return Optional.ofNullable(annotationAttributes) | |
.map(it -> it.getOrDefault("value", Ordered.LOWEST_PRECEDENCE)) | |
.map(Integer.class::cast) | |
.orElse(Ordered.LOWEST_PRECEDENCE); | |
} | |
private AnnotationAttributes getOrderAnnotationAttributesFromFactoryMethod(AnnotatedBeanDefinition beanDefinition) { | |
return Optional.of(beanDefinition) | |
.map(AnnotatedBeanDefinition::getFactoryMethodMetadata) | |
.filter(methodMetadata -> methodMetadata.isAnnotated(Order.class.getName())) | |
.map(methodMetadata -> methodMetadata.getAnnotationAttributes(Order.class.getName())) | |
.map(AnnotationAttributes::fromMap) | |
.orElseGet(() -> getOrderAnnotationAttributesFromBeanClass(beanDefinition)); | |
} | |
private AnnotationAttributes getOrderAnnotationAttributesFromBeanClass(AnnotatedBeanDefinition beanDefinition) { | |
return Optional.of(beanDefinition) | |
.map(AnnotatedBeanDefinition::getResolvableType) | |
.map(ResolvableType::resolve) | |
.filter(beanType -> beanType.isAnnotationPresent(Order.class)) | |
.map(beanType -> beanType.getAnnotation(Order.class)) | |
.map(AnnotationUtils::getAnnotationAttributes) | |
.map(AnnotationAttributes::fromMap) | |
.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. | |
/* | |
return Optional.of(beanDefinition) | |
.map(AnnotatedBeanDefinition::getMetadata) | |
.filter(annotationMetadata -> annotationMetadata.hasAnnotation(Order.class.getName())) | |
.map(annotationMetadata -> annotationMetadata.getAnnotationAttributes(Order.class.getName())) | |
.map(AnnotationAttributes::fromMap) | |
.orElse(null); | |
*/ | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment