diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java index 53732979a..ed0176c1f 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/named/NamedContextFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -16,6 +16,8 @@ package org.springframework.cloud.context.named; +import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -46,9 +48,8 @@ /** * Creates a set of child contexts that allows a set of Specifications to define the beans - * in each child context. - * - * Ported from spring-cloud-netflix FeignClientFactory and SpringClientFactory + * in each child context. Ported from spring-cloud-netflix FeignClientFactory and + * SpringClientFactory * * @param specification * @author Spencer Gibb @@ -230,6 +231,23 @@ public T getInstance(String name, ResolvableType type) { return null; } + @SuppressWarnings("unchecked") + public T getAnnotatedInstance(String name, ResolvableType type, Class annotationType) { + GenericApplicationContext context = getContext(name); + String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(context, annotationType); + + List beans = new ArrayList<>(); + for (String beanName : beanNames) { + if (context.isTypeMatch(beanName, type)) { + beans.add((T) context.getBean(beanName)); + } + } + if (beans.size() > 1) { + throw new IllegalStateException("Only one annotated bean for type expected."); + } + return beans.isEmpty() ? null : beans.get(0); + } + public Map getInstances(String name, Class type) { GenericApplicationContext context = getContext(name); diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/named/NamedContextFactoryTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/named/NamedContextFactoryTests.java index f9accd988..e9ecd1750 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/named/NamedContextFactoryTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/named/NamedContextFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -16,7 +16,14 @@ package org.springframework.cloud.context.named; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -24,15 +31,17 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.util.ClassUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.BDDAssertions.then; /** @@ -50,6 +59,64 @@ public void testChildContexts() { testChildContexts(parent); } + @Test + void testBadThreadContextClassLoader() throws InterruptedException, ExecutionException, TimeoutException { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.setClassLoader(ClassUtils.getDefaultClassLoader()); + parent.register(BaseConfig.class); + parent.refresh(); + + ExecutorService es = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setContextClassLoader(new ThrowingClassLoader()); + return t; + }); + es.submit(() -> this.testChildContexts(parent)).get(5, TimeUnit.SECONDS); + + } + + @Test + void testGetAnnotatedBeanInstance() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.register(BaseConfig.class); + parent.refresh(); + TestClientFactory factory = new TestClientFactory(); + factory.setApplicationContext(parent); + factory.setConfigurations(List.of(getSpec("annotated", AnnotatedConfig.class))); + + TestType annotatedBean = factory.getAnnotatedInstance("annotated", ResolvableType.forType(TestType.class), + TestBean.class); + + assertThat(annotatedBean.value()).isEqualTo(2); + } + + @Test + void testNoAnnotatedBeanInstance() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.register(BaseConfig.class); + parent.refresh(); + TestClientFactory factory = new TestClientFactory(); + factory.setApplicationContext(parent); + factory.setConfigurations(List.of(getSpec("not-annotated", NotAnnotatedConfig.class))); + + TestType annotatedBean = factory.getAnnotatedInstance("not-annotated", ResolvableType.forType(TestType.class), + TestBean.class); + assertThat(annotatedBean).isNull(); + } + + @Test + void testMoreThanOneAnnotatedBeanInstance() { + AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); + parent.register(BaseConfig.class); + parent.refresh(); + TestClientFactory factory = new TestClientFactory(); + factory.setApplicationContext(parent); + factory.setConfigurations(List.of(getSpec("many-annotated", ManyAnnotatedConfig.class))); + + assertThatIllegalStateException().isThrownBy(() -> factory.getAnnotatedInstance("many-annotated", + ResolvableType.forType(TestType.class), TestBean.class)); + } + private void testChildContexts(GenericApplicationContext parent) { TestClientFactory factory = new TestClientFactory(); factory.setApplicationContext(parent); @@ -97,7 +164,7 @@ private void testChildContexts(GenericApplicationContext parent) { .as("foo context bean factory classloader does not match parent") .isSameAs(parent.getBeanFactory().getBeanClassLoader()); - Assertions.assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true); + assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true); factory.destroy(); @@ -106,22 +173,6 @@ private void testChildContexts(GenericApplicationContext parent) { then(barContext.isActive()).as("bar context wasn't closed").isFalse(); } - @Test - void testBadThreadContextClassLoader() throws InterruptedException, ExecutionException, TimeoutException { - AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); - parent.setClassLoader(ClassUtils.getDefaultClassLoader()); - parent.register(BaseConfig.class); - parent.refresh(); - - ExecutorService es = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r); - t.setContextClassLoader(new ThrowingClassLoader()); - return t; - }); - - es.submit(() -> this.testChildContexts(parent)).get(5, TimeUnit.SECONDS); - } - private TestSpec getSpec(String name, Class configClass) { return new TestSpec(name, new Class[] { configClass }); } @@ -236,18 +287,81 @@ static class Bar { } - static class Container { + record Container (T item) { + + } + + static class AnnotatedConfig { + + @Bean + TestType test1() { + return new TestType(1); + } - private final T item; + @TestBean + @Bean + TestType test2() { + return new TestType(2); + } - Container(T item) { - this.item = item; + @TestBean + @Bean + Bar bar() { + return new Bar(); } - public T getItem() { - return this.item; + } + + static class NotAnnotatedConfig { + + @Bean + TestType test1() { + return new TestType(1); } + @Bean + TestType test2() { + return new TestType(2); + } + + @TestBean + @Bean + Bar bar() { + return new Bar(); + } + + } + + static class ManyAnnotatedConfig { + + @TestBean + @Bean + TestType test1() { + return new TestType(1); + } + + @TestBean + @Bean + TestType test2() { + return new TestType(2); + } + + @Bean + Bar bar() { + return new Bar(); + } + + } + + record TestType(int value) { + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Inherited + @interface TestBean { + } } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 24afedc95..f275182c8 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -18,6 +18,7 @@ +