From cf201ffd80c65f473a5ef0cf716bbf26c0de46b0 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 30 May 2013 14:15:06 +0100 Subject: [PATCH] [bs-142] Add @AssertMissingBean Example usage is to fail fast if trying to provide a @ConfigurationProperties bean and it was already defined [Fixes #50812235] --- .../annotation/AbstractOnBeanCondition.java | 34 +++++---- .../context/annotation/AssertMissingBean.java | 55 +++++++++++++++ .../AssertMissingBeanCondition.java | 47 +++++++++++++ .../EnableConfigurationPropertiesTests.java | 70 +++++++++++++++++++ .../context/annotation/testProperties.xml | 12 ++++ 5 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBean.java create mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBeanCondition.java create mode 100644 spring-bootstrap/src/test/resources/org/springframework/bootstrap/context/annotation/testProperties.xml diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java index cd217a6e8d..449f2c9ca3 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AbstractOnBeanCondition.java @@ -38,9 +38,19 @@ import org.springframework.util.MultiValueMap; abstract class AbstractOnBeanCondition implements Condition { protected Log logger = LogFactory.getLog(getClass()); + private List beanClasses; + private List beanNames; protected abstract Class annotationClass(); + protected List getBeanClasses() { + return this.beanClasses; + } + + protected List getBeanNames() { + return this.beanNames; + } + @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { @@ -48,16 +58,16 @@ abstract class AbstractOnBeanCondition implements Condition { MultiValueMap attributes = metadata.getAllAnnotationAttributes( annotationClass().getName(), true); - List beanClasses = collect(attributes, "value"); - List 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 beanClassesFound = new ArrayList(); List beanNamesFound = new ArrayList(); - 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 { diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBean.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBean.java new file mode 100644 index 0000000000..895249be3e --- /dev/null +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBean.java @@ -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 {}; + +} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBeanCondition.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBeanCondition.java new file mode 100644 index 0000000000..978ea302a8 --- /dev/null +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/AssertMissingBeanCondition.java @@ -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; + } + +} diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java index 3d5e3eba19..759484f35f 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java @@ -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 diff --git a/spring-bootstrap/src/test/resources/org/springframework/bootstrap/context/annotation/testProperties.xml b/spring-bootstrap/src/test/resources/org/springframework/bootstrap/context/annotation/testProperties.xml new file mode 100644 index 0000000000..c04d85080c --- /dev/null +++ b/spring-bootstrap/src/test/resources/org/springframework/bootstrap/context/annotation/testProperties.xml @@ -0,0 +1,12 @@ + + + + + + + +