Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jxblum/42173fc08f9da60ebb3158db7d4380f7 to your computer and use it in GitHub Desktop.
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.
/*
* 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