[bs-142] Add @AssertMissingBean

Example usage is to fail fast if trying to provide
a @ConfigurationProperties bean and it was already
defined

[Fixes #50812235]
This commit is contained in:
Dave Syer 2013-05-30 14:15:06 +01:00
parent 8023a862c3
commit cf201ffd80
5 changed files with 206 additions and 12 deletions

View File

@ -38,9 +38,19 @@ import org.springframework.util.MultiValueMap;
abstract class AbstractOnBeanCondition implements Condition {
protected Log logger = LogFactory.getLog(getClass());
private List<String> beanClasses;
private List<String> beanNames;
protected abstract Class<?> annotationClass();
protected List<String> getBeanClasses() {
return this.beanClasses;
}
protected List<String> getBeanNames() {
return this.beanNames;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
@ -48,16 +58,16 @@ abstract class AbstractOnBeanCondition implements Condition {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
annotationClass().getName(), true);
List<String> beanClasses = collect(attributes, "value");
List<String> beanNames = collect(attributes, "name");
Assert.isTrue(beanClasses.size() > 0 || beanNames.size() > 0,
"@" + ClassUtils.getShortName(annotationClass())
+ " annotations must specify at least one bean");
this.beanClasses = collect(attributes, "value");
this.beanNames = collect(attributes, "name");
Assert.isTrue(this.beanClasses.size() > 0 || this.beanNames.size() > 0, "@"
+ ClassUtils.getShortName(annotationClass())
+ " annotations must specify at least one bean");
List<String> beanClassesFound = new ArrayList<String>();
List<String> beanNamesFound = new ArrayList<String>();
for (String beanClass : beanClasses) {
for (String beanClass : this.beanClasses) {
try {
// eagerInit set to false to prevent early instantiation (some
// factory beans will not be able to determine their object type at this
@ -72,7 +82,7 @@ abstract class AbstractOnBeanCondition implements Condition {
} catch (ClassNotFoundException ex) {
}
}
for (String beanName : beanNames) {
for (String beanName : this.beanNames) {
if (context.getBeanFactory().containsBeanDefinition(beanName)) {
beanNamesFound.add(beanName);
}
@ -80,9 +90,9 @@ abstract class AbstractOnBeanCondition implements Condition {
boolean result = evaluate(beanClassesFound, beanNamesFound);
if (this.logger.isDebugEnabled()) {
if (!beanClasses.isEmpty()) {
if (!this.beanClasses.isEmpty()) {
this.logger.debug(checking + "Looking for beans with class: "
+ beanClasses);
+ this.beanClasses);
if (beanClassesFound.isEmpty()) {
this.logger.debug(checking + "Found no beans");
} else {
@ -91,9 +101,9 @@ abstract class AbstractOnBeanCondition implements Condition {
}
}
if (!beanNames.isEmpty()) {
this.logger
.debug(checking + "Looking for beans with names: " + beanNames);
if (!this.beanNames.isEmpty()) {
this.logger.debug(checking + "Looking for beans with names: "
+ this.beanNames);
if (beanNamesFound.isEmpty()) {
this.logger.debug(checking + "Found no beans");
} else {

View File

@ -0,0 +1,55 @@
/*
* Copyright 2012-2013 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.bootstrap.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional} that only matches when the specified bean classes and/or names are
* not already contained in the {@link BeanFactory}, and throws an exception otherwise.
*
* @author Dave Syer
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(AssertMissingBeanCondition.class)
public @interface AssertMissingBean {
/**
* The class type of bean that should be checked. The condition matches when each
* class specified is missing in the {@link ApplicationContext}.
* @return the class types of beans to check
*/
Class<?>[] value() default {};
/**
* The names of beans to check. The condition matches when each bean name specified is
* missing in the {@link ApplicationContext}.
* @return the name of beans to check
*/
String[] name() default {};
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2012-2013 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.bootstrap.context.annotation;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks that specific beans are missing.
*
* @author Dave Syer
* @see AssertMissingBean
*/
class AssertMissingBeanCondition extends OnMissingBeanCondition {
@Override
protected Class<?> annotationClass() {
return AssertMissingBean.class;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean result = super.matches(context, metadata);
if (!result) {
throw new BeanCreationException("Found existing bean for classes="
+ getBeanClasses() + " and names=" + getBeanNames());
}
return result;
}
}

View File

@ -15,13 +15,18 @@
*/
package org.springframework.bootstrap.context.annotation;
import java.util.Arrays;
import javax.annotation.PostConstruct;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.bootstrap.TestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.stereotype.Component;
import static org.junit.Assert.assertEquals;
@ -53,6 +58,45 @@ public class EnableConfigurationPropertiesTests {
assertEquals("foo", this.context.getBean(MoreProperties.class).getName());
}
@Test
public void testPropertiesBindingWithDefaultsInXml() {
this.context.register(TestConfiguration.class, DefaultXmlConfiguration.class);
this.context.refresh();
String[] beanNames = this.context.getBeanNamesForType(TestProperties.class);
assertEquals("Wrong beans: " + Arrays.asList(beanNames), 1, beanNames.length);
assertEquals("bar", this.context.getBean(TestProperties.class).getName());
}
@Test(expected = BeanCreationException.class)
public void testPropertiesBindingWithDefaultsInBeanMethodReverseOrder() {
this.context.register(TestBeanConfiguration.class, DefaultConfiguration.class);
this.context.refresh();
String[] beanNames = this.context.getBeanNamesForType(TestProperties.class);
assertEquals("Wrong beans: " + Arrays.asList(beanNames), 1, beanNames.length);
assertEquals("bar", this.context.getBean(TestProperties.class).getName());
}
@Test
public void testPropertiesBindingWithDefaultsInBeanMethod() {
this.context.register(DefaultConfiguration.class, TestBeanConfiguration.class);
this.context.refresh();
String[] beanNames = this.context.getBeanNamesForType(TestProperties.class);
assertEquals("Wrong beans: " + Arrays.asList(beanNames), 1, beanNames.length);
assertEquals("bar", this.context.getBean(TestProperties.class).getName());
}
// Maybe we could relax the condition that causes this exception but Spring makes it
// difficult because it is impossible for DefaultConfiguration to override a bean
// definition created with a direct regsistration (as opposed to a @Bean)
@Test(expected = BeanCreationException.class)
public void testPropertiesBindingWithDefaults() {
this.context.register(DefaultConfiguration.class, TestConfiguration.class);
this.context.refresh();
String[] beanNames = this.context.getBeanNamesForType(TestProperties.class);
assertEquals("Wrong beans: " + Arrays.asList(beanNames), 1, beanNames.length);
assertEquals("bar", this.context.getBean(TestProperties.class).getName());
}
@Test
public void testBindingWithTwoBeans() {
this.context.register(MoreConfiguration.class, TestConfiguration.class);
@ -94,6 +138,32 @@ public class EnableConfigurationPropertiesTests {
protected static class TestConfiguration {
}
@Configuration
@EnableConfigurationProperties
protected static class TestBeanConfiguration {
@ConditionalOnMissingBean(TestProperties.class)
@Bean(name = "org.springframework.bootstrap.context.annotation.EnableConfigurationPropertiesTests$TestProperties")
public TestProperties testProperties() {
return new TestProperties();
}
}
@Configuration
protected static class DefaultConfiguration {
@Bean
@AssertMissingBean(TestProperties.class)
public TestProperties testProperties() {
TestProperties test = new TestProperties();
test.setName("bar");
return test;
}
}
@Configuration
@ImportResource("org/springframework/bootstrap/context/annotation/testProperties.xml")
protected static class DefaultXmlConfiguration {
}
@Component
protected static class TestConsumer {
@Autowired

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean
id="org.springframework.bootstrap.context.annotation.EnableConfigurationPropertiesTests$TestProperties"
class="org.springframework.bootstrap.context.annotation.EnableConfigurationPropertiesTests$TestProperties">
<property name="name" value="bar"/>
</bean>
</beans>