Commit 9f858e75 authored by Phillip Webb's avatar Phillip Webb

Support parameterizedContainer in bean conditions

Add a `parameterizedContainer` attribute to `ConditionalOnBean` and
`ConditionalOnMissingBean` which can be used to support generic types
when checking for the presence of beans.

Closes gh-14940
parent 4d3d711e
...@@ -97,4 +97,14 @@ public @interface ConditionalOnBean { ...@@ -97,4 +97,14 @@ public @interface ConditionalOnBean {
*/ */
SearchStrategy search() default SearchStrategy.ALL; SearchStrategy search() default SearchStrategy.ALL;
/**
* Additional classes that may contain the specified bean types within their generic
* parameters. For example, an annotation declaring {@code value=Name.class} and
* {@code parameterizedContainer=NameRegistration.class} would detect both
* {@code Name} and {@code NameRegistration<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
} }
...@@ -113,4 +113,14 @@ public @interface ConditionalOnMissingBean { ...@@ -113,4 +113,14 @@ public @interface ConditionalOnMissingBean {
*/ */
SearchStrategy search() default SearchStrategy.ALL; SearchStrategy search() default SearchStrategy.ALL;
/**
* Additional classes that may contain the specified bean types within their generic
* parameters. For example, an annotation declaring {@code value=Name.class} and
* {@code parameterizedContainer=NameRegistration.class} would detect both
* {@code Name} and {@code NameRegistration<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
} }
...@@ -22,6 +22,7 @@ import java.lang.annotation.Retention; ...@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Date; import java.util.Date;
import java.util.function.Consumer;
import org.junit.Test; import org.junit.Test;
...@@ -30,6 +31,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; ...@@ -30,6 +31,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
...@@ -38,6 +40,7 @@ import org.springframework.context.annotation.ImportResource; ...@@ -38,6 +40,7 @@ import org.springframework.context.annotation.ImportResource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -149,6 +152,89 @@ public class ConditionalOnBeanTests { ...@@ -149,6 +152,89 @@ public class ConditionalOnBeanTests {
}); });
} }
@Test
public void parameterizedContainerWhenValueIsOfMissingBeanDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithoutCustomConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("otherExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfExistingBeanMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfMissingBeanRegistrationDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithoutCustomContainerConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("otherExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfExistingBeanRegistrationMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnTypeIsOfExistingBeanMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithReturnTypeConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnTypeIsOfExistingBeanRegistrationMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithReturnTypeConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithReturnRegistrationTypeConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanRegistrationMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithReturnRegistrationTypeConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"customExampleBean", "conditionalCustomExampleBean")));
}
private Consumer<ConfigurableApplicationContext> exampleBeanRequirement(
String... names) {
return (context) -> {
String[] beans = context.getBeanNamesForType(ExampleBean.class);
String[] containers = context
.getBeanNamesForType(TestParameterizedContainer.class);
assertThat(StringUtils.concatenateStringArrays(beans, containers))
.containsOnly(names);
};
}
@Configuration @Configuration
@ConditionalOnBean(name = "foo") @ConditionalOnBean(name = "foo")
protected static class OnBeanNameConfiguration { protected static class OnBeanNameConfiguration {
...@@ -298,57 +384,146 @@ public class ConditionalOnBeanTests { ...@@ -298,57 +384,146 @@ public class ConditionalOnBeanTests {
} }
@TestAnnotation @Configuration
public static class ExampleBean { public static class OriginalDefinition {
private String value; @Bean
public String testBean() {
return "test";
}
public ExampleBean(String value) { }
this.value = value;
@Configuration
@ConditionalOnBean(String.class)
public static class OverridingDefinition {
@Bean
public Integer testBean() {
return 1;
} }
@Override }
public String toString() {
return this.value; @Configuration
@ConditionalOnBean(String.class)
public static class ConsumingConfiguration {
ConsumingConfiguration(String testBean) {
} }
} }
@Target(ElementType.TYPE) @Configuration
@Retention(RetentionPolicy.RUNTIME) static class ParmeterizedWithCustomConfig {
@Documented
public @interface TestAnnotation { @Bean
public CustomExampleBean customExampleBean() {
return new CustomExampleBean();
}
} }
@Configuration @Configuration
public static class OriginalDefinition { static class ParmeterizedWithoutCustomConfig {
@Bean @Bean
public String testBean() { public OtherExampleBean otherExampleBean() {
return "test"; return new OtherExampleBean();
} }
} }
@Configuration @Configuration
@ConditionalOnBean(String.class) static class ParmeterizedWithoutCustomContainerConfig {
public static class OverridingDefinition {
@Bean @Bean
public Integer testBean() { public TestParameterizedContainer<OtherExampleBean> otherExampleBean() {
return 1; return new TestParameterizedContainer<OtherExampleBean>();
} }
} }
@Configuration @Configuration
@ConditionalOnBean(String.class) static class ParmeterizedWithCustomContainerConfig {
public static class ConsumingConfiguration {
ConsumingConfiguration(String testBean) { @Bean
public TestParameterizedContainer<CustomExampleBean> customExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@Configuration
static class ParmeterizedConditionWithValueConfig {
@Bean
@ConditionalOnBean(value = CustomExampleBean.class, parameterizedContainer = TestParameterizedContainer.class)
public CustomExampleBean conditionalCustomExampleBean() {
return new CustomExampleBean();
}
}
@Configuration
static class ParmeterizedConditionWithReturnTypeConfig {
@Bean
@ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class)
public CustomExampleBean conditionalCustomExampleBean() {
return new CustomExampleBean();
}
}
@Configuration
static class ParmeterizedConditionWithReturnRegistrationTypeConfig {
@Bean
@ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class)
public TestParameterizedContainer<CustomExampleBean> conditionalCustomExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@TestAnnotation
public static class ExampleBean {
private String value;
public ExampleBean(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
} }
} }
public static class CustomExampleBean extends ExampleBean {
public CustomExampleBean() {
super("custom subclass");
}
}
public static class OtherExampleBean extends ExampleBean {
public OtherExampleBean() {
super("other subclass");
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestAnnotation {
}
} }
...@@ -22,6 +22,7 @@ import java.lang.annotation.Retention; ...@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Date; import java.util.Date;
import java.util.function.Consumer;
import org.junit.Test; import org.junit.Test;
...@@ -33,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanC ...@@ -33,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanC
import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration; import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
...@@ -44,6 +46,7 @@ import org.springframework.context.annotation.ImportResource; ...@@ -44,6 +46,7 @@ import org.springframework.context.annotation.ImportResource;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -304,6 +307,89 @@ public class ConditionalOnMissingBeanTests { ...@@ -304,6 +307,89 @@ public class ConditionalOnMissingBeanTests {
}); });
} }
@Test
public void parameterizedContainerWhenValueIsOfMissingBeanMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithoutCustomConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"otherExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfExistingBeanDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfMissingBeanRegistrationMatches() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithoutCustomContainerConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context).satisfies(exampleBeanRequirement(
"otherExampleBean", "conditionalCustomExampleBean")));
}
@Test
public void parameterizedContainerWhenValueIsOfExistingBeanRegistrationDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithValueConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnTypeIsOfExistingBeanDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithReturnTypeConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnTypeIsOfExistingBeanRegistrationDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithReturnTypeConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomConfig.class,
ParmeterizedConditionWithReturnRegistrationTypeConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
@Test
public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanRegistrationDoesNotMatch() {
this.contextRunner
.withUserConfiguration(ParmeterizedWithCustomContainerConfig.class,
ParmeterizedConditionWithReturnRegistrationTypeConfig.class)
.run((context) -> assertThat(context)
.satisfies(exampleBeanRequirement("customExampleBean")));
}
private Consumer<ConfigurableApplicationContext> exampleBeanRequirement(
String... names) {
return (context) -> {
String[] beans = context.getBeanNamesForType(ExampleBean.class);
String[] containers = context
.getBeanNamesForType(TestParameterizedContainer.class);
assertThat(StringUtils.concatenateStringArrays(beans, containers))
.containsOnly(names);
};
}
@Configuration @Configuration
protected static class OnBeanInAncestorsConfiguration { protected static class OnBeanInAncestorsConfiguration {
...@@ -584,30 +670,6 @@ public class ConditionalOnMissingBeanTests { ...@@ -584,30 +670,6 @@ public class ConditionalOnMissingBeanTests {
} }
@TestAnnotation
public static class ExampleBean {
private String value;
public ExampleBean(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
}
}
public static class CustomExampleBean extends ExampleBean {
public CustomExampleBean() {
super("custom subclass");
}
}
public static class ExampleFactoryBean implements FactoryBean<ExampleBean> { public static class ExampleFactoryBean implements FactoryBean<ExampleBean> {
public ExampleFactoryBean(String value) { public ExampleFactoryBean(String value) {
...@@ -654,6 +716,111 @@ public class ConditionalOnMissingBeanTests { ...@@ -654,6 +716,111 @@ public class ConditionalOnMissingBeanTests {
} }
@Configuration
static class ParmeterizedWithCustomConfig {
@Bean
public CustomExampleBean customExampleBean() {
return new CustomExampleBean();
}
}
@Configuration
static class ParmeterizedWithoutCustomConfig {
@Bean
public OtherExampleBean otherExampleBean() {
return new OtherExampleBean();
}
}
@Configuration
static class ParmeterizedWithoutCustomContainerConfig {
@Bean
public TestParameterizedContainer<OtherExampleBean> otherExampleBean() {
return new TestParameterizedContainer<OtherExampleBean>();
}
}
@Configuration
static class ParmeterizedWithCustomContainerConfig {
@Bean
public TestParameterizedContainer<CustomExampleBean> customExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@Configuration
static class ParmeterizedConditionWithValueConfig {
@Bean
@ConditionalOnMissingBean(value = CustomExampleBean.class, parameterizedContainer = TestParameterizedContainer.class)
public CustomExampleBean conditionalCustomExampleBean() {
return new CustomExampleBean();
}
}
@Configuration
static class ParmeterizedConditionWithReturnTypeConfig {
@Bean
@ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class)
public CustomExampleBean conditionalCustomExampleBean() {
return new CustomExampleBean();
}
}
@Configuration
static class ParmeterizedConditionWithReturnRegistrationTypeConfig {
@Bean
@ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class)
public TestParameterizedContainer<CustomExampleBean> conditionalCustomExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@TestAnnotation
public static class ExampleBean {
private String value;
public ExampleBean(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
}
}
public static class CustomExampleBean extends ExampleBean {
public CustomExampleBean() {
super("custom subclass");
}
}
public static class OtherExampleBean extends ExampleBean {
public OtherExampleBean() {
super("other subclass");
}
}
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
......
/*
* Copyright 2012-2018 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
*
* http://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.boot.autoconfigure.condition;
/**
* Simple parameterized container for testing {@link ConditionalOnBean} and
* {@link ConditionalOnMissingBean}.
*
* @param <T> The bean type
* @author Phillip Webb
*/
public class TestParameterizedContainer<T> {
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment