diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java index 3aeb097d8b..a2557aa823 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -31,6 +32,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -122,12 +124,52 @@ public abstract class AutoConfigurationPackages { * configuration. */ @Order(Ordered.HIGHEST_PRECEDENCE) - static class Registrar implements ImportBeanDefinitionRegistrar { + static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - register(registry, ClassUtils.getPackageName(metadata.getClassName())); + register(registry, new PackageImport(metadata).getPackageName()); + } + + @Override + public Set determineImports(AnnotationMetadata metadata) { + return Collections.singleton(new PackageImport(metadata)); + } + + } + + /** + * Wrapper for a package import. + */ + private final static class PackageImport { + + private final String packageName; + + PackageImport(AnnotationMetadata metadata) { + this.packageName = ClassUtils.getPackageName(metadata.getClassName()); + } + + @Override + public int hashCode() { + return this.packageName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.packageName.equals(((PackageImport) obj).packageName); + } + + public String getPackageName() { + return this.packageName; + } + + @Override + public String toString() { + return "Package Import " + this.packageName; } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java index 834e392cb1..5b9fcac212 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; @@ -44,7 +45,8 @@ import org.springframework.util.ObjectUtils; * @author Phillip Webb * @author Andy Wilkinson */ -class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector { +class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector + implements DeterminableImports { private static final Set ANNOTATION_NAMES; @@ -55,6 +57,14 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec ANNOTATION_NAMES = Collections.unmodifiableSet(names); } + @Override + public Set determineImports(AnnotationMetadata metadata) { + Set result = new LinkedHashSet(); + result.addAll(getCandidateConfigurations(metadata, null)); + result.removeAll(getExclusions(metadata, null)); + return Collections.unmodifiableSet(result); + } + @Override protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) { return null; @@ -85,6 +95,10 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec if (classes.length > 0) { return Arrays.asList(classes); } + return loadFactoryNames(source); + } + + protected Collection loadFactoryNames(Class source) { return SpringFactoriesLoader.loadFactoryNames(source, getClass().getClassLoader()); } @@ -117,7 +131,8 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec return exclusions; } - private Map, List> getAnnotations(AnnotationMetadata metadata) { + protected final Map, List> getAnnotations( + AnnotationMetadata metadata) { MultiValueMap, Annotation> annotations = new LinkedMultiValueMap, Annotation>(); Class source = ClassUtils.resolveClassName(metadata.getClassName(), null); collectAnnotations(source, annotations, new HashSet>()); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java index bb0587b01c..cbd254b1a5 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -19,6 +19,9 @@ package org.springframework.boot.autoconfigure; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -46,7 +49,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; */ public class ImportAutoConfigurationImportSelectorTests { - private final ImportAutoConfigurationImportSelector importSelector = new ImportAutoConfigurationImportSelector(); + private final ImportAutoConfigurationImportSelector importSelector = new TestImportAutoConfigurationImportSelector(); private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -63,36 +66,32 @@ public class ImportAutoConfigurationImportSelectorTests { @Test public void importsAreSelected() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(ImportFreeMarker.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ImportFreeMarker.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName()); } @Test public void importsAreSelectedUsingClassesAttribute() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(ImportFreeMarkerUsingClassesAttribute.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ImportFreeMarkerUsingClassesAttribute.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName()); } @Test public void propertyExclusionsAreNotApplied() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(ImportFreeMarker.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ImportFreeMarker.class); this.importSelector.selectImports(annotationMetadata); verifyZeroInteractions(this.environment); } @Test public void multipleImportsAreFound() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(MultipleImports.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + MultipleImports.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName(), ThymeleafAutoConfiguration.class.getName()); @@ -100,41 +99,94 @@ public class ImportAutoConfigurationImportSelectorTests { @Test public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(ImportWithSelfAnnotatingAnnotation.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ImportWithSelfAnnotatingAnnotation.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName()); } @Test public void exclusionsAreApplied() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(MultipleImportsWithExclusion.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + MultipleImportsWithExclusion.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName()); } @Test public void exclusionsWithoutImport() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader(ExclusionWithoutImport.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ExclusionWithoutImport.class); String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName()); } @Test public void exclusionsAliasesAreApplied() throws Exception { - AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() - .getMetadataReader( - ImportWithSelfAnnotatingAnnotationExclude.class.getName()) - .getAnnotationMetadata(); + AnnotationMetadata annotationMetadata = getAnnotationMetadata( + ImportWithSelfAnnotatingAnnotationExclude.class); + String[] imports = this.importSelector.selectImports(annotationMetadata); assertThat(imports).isEmpty(); } + @Test + public void determineImportsWhenUsingMetaWithoutClassesShouldBeEqual() + throws Exception { + Set set1 = this.importSelector.determineImports( + getAnnotationMetadata(ImportMetaAutoConfigurationWithUnrelatedOne.class)); + Set set2 = this.importSelector.determineImports( + getAnnotationMetadata(ImportMetaAutoConfigurationWithUnrelatedTwo.class)); + assertThat(set1).isEqualTo(set2); + assertThat(set1.hashCode()).isEqualTo(set2.hashCode()); + } + + @Test + public void determineImportsWhenUsingNonMetaWithoutClassesShouldBeSame() + throws Exception { + Set set1 = this.importSelector.determineImports( + getAnnotationMetadata(ImportAutoConfigurationWithUnrelatedOne.class)); + Set set2 = this.importSelector.determineImports( + getAnnotationMetadata(ImportAutoConfigurationWithUnrelatedTwo.class)); + assertThat(set1).isEqualTo(set2); + } + + @Test + public void determineImportsWhenUsingNonMetaWithClassesShouldBeSame() + throws Exception { + Set set1 = this.importSelector.determineImports( + getAnnotationMetadata(ImportAutoConfigurationWithItemsOne.class)); + Set set2 = this.importSelector.determineImports( + getAnnotationMetadata(ImportAutoConfigurationWithItemsOne.class)); + assertThat(set1).isEqualTo(set2); + } + + @Test + public void determineImportsWhenUsingMetaExcludeWithoutClassesShouldBeEqual() + throws Exception { + Set set1 = this.importSelector.determineImports(getAnnotationMetadata( + ImportMetaAutoConfigurationExcludeWithUnrelatedOne.class)); + Set set2 = this.importSelector.determineImports(getAnnotationMetadata( + ImportMetaAutoConfigurationExcludeWithUnrelatedTwo.class)); + assertThat(set1).isEqualTo(set2); + assertThat(set1.hashCode()).isEqualTo(set2.hashCode()); + } + + @Test + public void determineImportsWhenUsingMetaDifferentExcludeWithoutClassesShouldBeDifferent() + throws Exception { + Set set1 = this.importSelector.determineImports(getAnnotationMetadata( + ImportMetaAutoConfigurationExcludeWithUnrelatedOne.class)); + Set set2 = this.importSelector.determineImports( + getAnnotationMetadata(ImportMetaAutoConfigurationWithUnrelatedTwo.class)); + assertThat(set1).isNotEqualTo(set2); + } + + private AnnotationMetadata getAnnotationMetadata(Class source) throws IOException { + return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()) + .getAnnotationMetadata(); + } + @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class) static class ImportFreeMarker { @@ -186,6 +238,73 @@ public class ImportAutoConfigurationImportSelectorTests { } + @MetaImportAutoConfiguration + @UnrelatedOne + static class ImportMetaAutoConfigurationWithUnrelatedOne { + + } + + @MetaImportAutoConfiguration + @UnrelatedTwo + static class ImportMetaAutoConfigurationWithUnrelatedTwo { + + } + + @ImportAutoConfiguration + @UnrelatedOne + static class ImportAutoConfigurationWithUnrelatedOne { + + } + + @ImportAutoConfiguration + @UnrelatedTwo + static class ImportAutoConfigurationWithUnrelatedTwo { + + } + + @ImportAutoConfiguration(classes = ThymeleafAutoConfiguration.class) + @UnrelatedOne + static class ImportAutoConfigurationWithItemsOne { + + } + + @ImportAutoConfiguration(classes = ThymeleafAutoConfiguration.class) + @UnrelatedOne + static class ImportAutoConfigurationWithItemsTwo { + + } + + @MetaImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) + @UnrelatedOne + static class ImportMetaAutoConfigurationExcludeWithUnrelatedOne { + + } + + @MetaImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) + @UnrelatedTwo + static class ImportMetaAutoConfigurationExcludeWithUnrelatedTwo { + + } + + @ImportAutoConfiguration + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaImportAutoConfiguration { + + @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") + Class[] exclude() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface UnrelatedOne { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface UnrelatedTwo { + + } + @Retention(RetentionPolicy.RUNTIME) @ImportAutoConfiguration(ThymeleafAutoConfiguration.class) @SelfAnnotating @@ -196,4 +315,18 @@ public class ImportAutoConfigurationImportSelectorTests { } + private static class TestImportAutoConfigurationImportSelector + extends ImportAutoConfigurationImportSelector { + + @Override + protected Collection loadFactoryNames(Class source) { + if (source == MetaImportAutoConfiguration.class) { + return Arrays.asList(ThymeleafAutoConfiguration.class.getName(), + FreeMarkerAutoConfiguration.class.getName()); + } + return super.loadFactoryNames(source); + } + + } + } diff --git a/spring-boot-docs/src/main/asciidoc/deployment.adoc b/spring-boot-docs/src/main/asciidoc/deployment.adoc index 2884a5cd29..f09cff14f1 100644 --- a/spring-boot-docs/src/main/asciidoc/deployment.adoc +++ b/spring-boot-docs/src/main/asciidoc/deployment.adoc @@ -287,6 +287,88 @@ run the app. +[[cloud-deployment-aws]] +=== Amazon Web Services (AWS) +Amazon Web Services offers multiple ways to install Spring Boot based applications, either +as traditional web applications (war) or as executable jar files with an embedded web +server. Options include : + +* AWS Elastic Beanstalk +* AWS Code Deploy +* AWS OPS Works +* AWS Cloud Formation +* AWS Container Registry + +Each has different features and pricing model, here we will describe only the simplest +option : AWS Elastic Beanstalk. + + + +==== AWS Elastic Beanstalk +As described in the official http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_Java.html[Elastic +Beanstalk Java guide], there are two main options to deploy a Java application; You can +either us the "`Tomcat Platform`" or the "`Java SE platform`". + + + +===== Using the Tomcat platform +This option applies to Spring Boot projects producing a war file. There is no any special +configuration required, just follow the official guide. + + + +===== Using the Java SE platform +This option applies to Spring Boot projects producing a jar file and running an embedded +web container. Elastic Beanstalk environments run an nginx instance on port 80 to proxy +the actual application, running on port 5000. To configure it, add the following to your +`application.properties`: + +[indent=0] +---- + server.port=5000 +---- + + + +===== Best practices + +====== Uploading binaries instead of sources +By default Elastic Beanstalk uploads sources and compile them in AWS. To upload the +binaries instead, add the following to your `.elasticbeanstalk/config.yml` file: + + + +[source,xml,indent=0,subs="verbatim,quotes,attributes"] +---- + deploy: + artifact: target/demo-0.0.1-SNAPSHOT.jar +---- + + + +====== Reduce costs by setting the environment type +By default an Elastic Beanstalk environment is load balanced. The load balancer has a cost +perspective, to avoid it, set the environment type to "`Single instance`" as described +http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-create-wizard.html#environments-create-wizard-capacity[in the Amazon documentation]. +Single instance environments can be created using the CLI as well using the following +command: + +[indent=0] +---- + eb create -s +---- + + + +==== Summary +This is one of the easiest way to get to AWS, but there are more things +to cover, e.g.: how to integrate Elastic Beanstalk into any CI / CD tool, using the +Elastic Beanstalk maven plugin instead of the CLI, etc. There is a +https://exampledriven.wordpress.com/2017/01/09/spring-boot-aws-elastic-beanstalk-example/[blog] +covering these topics more in detail. + + + [[cloud-deployment-boxfuse]] === Boxfuse and Amazon Web Services https://boxfuse.com/[Boxfuse] works by turning your Spring Boot executable jar or war diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTypeExcludeFilter.java index e08d29d356..66adfc5039 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTypeExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTypeExcludeFilter.java @@ -16,7 +16,6 @@ package org.springframework.boot.test.autoconfigure.data.mongo; -import java.io.IOException; import java.util.Collections; import java.util.Set; @@ -24,8 +23,6 @@ import org.springframework.boot.context.TypeExcludeFilter; import org.springframework.boot.test.autoconfigure.filter.AnnotationCustomizableTypeExcludeFilter; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; /** * {@link TypeExcludeFilter} for {@link DataMongoTest @DataMongoTest}. @@ -63,15 +60,14 @@ class DataMongoTypeExcludeFilter extends AnnotationCustomizableTypeExcludeFilter return this.annotation.useDefaultFilters(); } - @Override - protected boolean defaultInclude(final MetadataReader metadataReader, - final MetadataReaderFactory metadataReaderFactory) throws IOException { - return false; - } - @Override protected Set> getDefaultIncludes() { return Collections.emptySet(); } + @Override + protected Set> getComponentIncludes() { + return Collections.emptySet(); + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java index b5737706f8..ceff15ab5c 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java @@ -27,6 +27,7 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.util.ObjectUtils; /** * Abstract base class for a {@link TypeExcludeFilter} that can be customized using an @@ -75,6 +76,11 @@ public abstract class AnnotationCustomizableTypeExcludeFilter extends TypeExclud return true; } } + for (Class component : getComponentIncludes()) { + if (isTypeOrAnnotated(metadataReader, metadataReaderFactory, component)) { + return true; + } + } return false; } @@ -103,10 +109,50 @@ public abstract class AnnotationCustomizableTypeExcludeFilter extends TypeExclud protected abstract Set> getDefaultIncludes(); + protected abstract Set> getComponentIncludes(); + protected enum FilterType { INCLUDE, EXCLUDE + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 0; + result = prime * result + ObjectUtils.hashCode(hasAnnotation()); + for (FilterType filterType : FilterType.values()) { + result = prime * result + + ObjectUtils.nullSafeHashCode(getFilters(filterType)); + } + result = prime * result + ObjectUtils.hashCode(isUseDefaultFilters()); + result = prime * result + ObjectUtils.nullSafeHashCode(getDefaultIncludes()); + result = prime * result + ObjectUtils.nullSafeHashCode(getComponentIncludes()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + AnnotationCustomizableTypeExcludeFilter other = (AnnotationCustomizableTypeExcludeFilter) obj; + boolean result = true; + result &= hasAnnotation() == other.hasAnnotation(); + for (FilterType filterType : FilterType.values()) { + result &= ObjectUtils.nullSafeEquals(getFilters(filterType), + other.getFilters(filterType)); + } + result &= isUseDefaultFilters() == other.isUseDefaultFilters(); + result &= ObjectUtils.nullSafeEquals(getDefaultIncludes(), + other.getDefaultIncludes()); + result &= ObjectUtils.nullSafeEquals(getComponentIncludes(), + other.getComponentIncludes()); + return result; }; } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java index c386a22a3b..b76c448fdc 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java @@ -18,6 +18,7 @@ package org.springframework.boot.test.autoconfigure.filter; import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -40,48 +41,65 @@ class TypeExcludeFiltersContextCustomizer implements ContextCustomizer { private static final String EXCLUDE_FILTER_BEAN_NAME = TypeExcludeFilters.class .getName(); - private final Class testClass; - - private final Set> filterClasses; + private final Set filters; TypeExcludeFiltersContextCustomizer(Class testClass, Set> filterClasses) { - this.testClass = testClass; - this.filterClasses = filterClasses; + this.filters = instantiateTypeExcludeFilters(testClass, filterClasses); + } + + private Set instantiateTypeExcludeFilters(Class testClass, + Set> filterClasses) { + Set filters = new LinkedHashSet(); + for (Class filterClass : filterClasses) { + filters.add(instantiateTypeExcludeFilter(testClass, filterClass)); + } + return Collections.unmodifiableSet(filters); + } + + private TypeExcludeFilter instantiateTypeExcludeFilter(Class testClass, + Class filterClass) { + try { + Constructor constructor = getTypeExcludeFilterConstructor(filterClass); + ReflectionUtils.makeAccessible(constructor); + if (constructor.getParameterTypes().length == 1) { + return (TypeExcludeFilter) constructor.newInstance(testClass); + } + return (TypeExcludeFilter) constructor.newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to create filter for " + filterClass, + ex); + } } @Override public int hashCode() { - return this.filterClasses.hashCode(); + return this.filters.hashCode(); } @Override public boolean equals(Object obj) { - return (obj != null && getClass().equals(obj.getClass()) && this.filterClasses - .equals(((TypeExcludeFiltersContextCustomizer) obj).filterClasses)); + return (obj != null && getClass().equals(obj.getClass()) && this.filters + .equals(((TypeExcludeFiltersContextCustomizer) obj).filters)); } @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedContextConfiguration) { - if (!this.filterClasses.isEmpty()) { + if (!this.filters.isEmpty()) { context.getBeanFactory().registerSingleton(EXCLUDE_FILTER_BEAN_NAME, createDelegatingTypeExcludeFilter()); } } private TypeExcludeFilter createDelegatingTypeExcludeFilter() { - final Set filters = new LinkedHashSet( - this.filterClasses.size()); - for (Class filterClass : this.filterClasses) { - filters.add(createTypeExcludeFilter(filterClass)); - } return new TypeExcludeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { - for (TypeExcludeFilter filter : filters) { + for (TypeExcludeFilter filter : TypeExcludeFiltersContextCustomizer.this.filters) { if (filter.match(metadataReader, metadataReaderFactory)) { return true; } @@ -92,20 +110,6 @@ class TypeExcludeFiltersContextCustomizer implements ContextCustomizer { }; } - private TypeExcludeFilter createTypeExcludeFilter(Class type) { - try { - Constructor constructor = getTypeExcludeFilterConstructor(type); - ReflectionUtils.makeAccessible(constructor); - if (constructor.getParameterTypes().length == 1) { - return (TypeExcludeFilter) constructor.newInstance(this.testClass); - } - return (TypeExcludeFilter) constructor.newInstance(); - } - catch (Exception ex) { - throw new IllegalStateException("Unable to create filter for " + type, ex); - } - } - private Constructor getTypeExcludeFilterConstructor(Class type) throws NoSuchMethodException { try { diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTypeExcludeFilter.java index 122b02712b..9d32f67e51 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTypeExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTypeExcludeFilter.java @@ -16,7 +16,6 @@ package org.springframework.boot.test.autoconfigure.jdbc; -import java.io.IOException; import java.util.Collections; import java.util.Set; @@ -24,8 +23,6 @@ import org.springframework.boot.context.TypeExcludeFilter; import org.springframework.boot.test.autoconfigure.filter.AnnotationCustomizableTypeExcludeFilter; import org.springframework.context.annotation.ComponentScan; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; /** * {@link TypeExcludeFilter} for {@link JdbcTest @JdbcTest}. @@ -62,15 +59,14 @@ class JdbcTypeExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { return this.annotation.useDefaultFilters(); } - @Override - protected boolean defaultInclude(MetadataReader metadataReader, - MetadataReaderFactory metadataReaderFactory) throws IOException { - return false; - } - @Override protected Set> getDefaultIncludes() { return Collections.emptySet(); } + @Override + protected Set> getComponentIncludes() { + return Collections.emptySet(); + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonExcludeFilter.java index cf070bad92..1cf49f948b 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonExcludeFilter.java @@ -77,4 +77,9 @@ class JsonExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { return DEFAULT_INCLUDES; } + @Override + protected Set> getComponentIncludes() { + return Collections.emptySet(); + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTypeExcludeFilter.java index 616182b021..cdf068086d 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTypeExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTypeExcludeFilter.java @@ -16,7 +16,6 @@ package org.springframework.boot.test.autoconfigure.orm.jpa; -import java.io.IOException; import java.util.Collections; import java.util.Set; @@ -24,8 +23,6 @@ import org.springframework.boot.context.TypeExcludeFilter; import org.springframework.boot.test.autoconfigure.filter.AnnotationCustomizableTypeExcludeFilter; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; /** * {@link TypeExcludeFilter} for {@link DataJpaTest @DataJpaTest}. @@ -62,15 +59,14 @@ class DataJpaTypeExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { return this.annotation.useDefaultFilters(); } - @Override - protected boolean defaultInclude(MetadataReader metadataReader, - MetadataReaderFactory metadataReaderFactory) throws IOException { - return false; - } - @Override protected Set> getDefaultIncludes() { return Collections.emptySet(); } + @Override + protected Set> getComponentIncludes() { + return Collections.emptySet(); + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientExcludeFilter.java index 6b6c305fab..400c1b0fc1 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientExcludeFilter.java @@ -16,7 +16,7 @@ package org.springframework.boot.test.autoconfigure.web.client; -import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -26,8 +26,6 @@ import org.springframework.boot.jackson.JsonComponent; import org.springframework.boot.test.autoconfigure.filter.AnnotationCustomizableTypeExcludeFilter; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.util.ClassUtils; /** @@ -65,20 +63,6 @@ class RestClientExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { RestClientTest.class); } - @Override - protected boolean defaultInclude(MetadataReader metadataReader, - MetadataReaderFactory metadataReaderFactory) throws IOException { - if (super.defaultInclude(metadataReader, metadataReaderFactory)) { - return true; - } - for (Class controller : this.annotation.components()) { - if (isTypeOrAnnotated(metadataReader, metadataReaderFactory, controller)) { - return true; - } - } - return false; - } - @Override protected boolean hasAnnotation() { return this.annotation != null; @@ -105,4 +89,9 @@ class RestClientExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { return DEFAULT_INCLUDES; } + @Override + protected Set> getComponentIncludes() { + return new LinkedHashSet>(Arrays.asList(this.annotation.components())); + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java index da5fd4aa93..4a29b5bc48 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java @@ -16,7 +16,7 @@ package org.springframework.boot.test.autoconfigure.web.servlet; -import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -29,8 +29,6 @@ import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBea import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.stereotype.Controller; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -74,20 +72,6 @@ class WebMvcTypeExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { WebMvcTest.class); } - @Override - protected boolean defaultInclude(MetadataReader metadataReader, - MetadataReaderFactory metadataReaderFactory) throws IOException { - if (super.defaultInclude(metadataReader, metadataReaderFactory)) { - return true; - } - for (Class controller : this.annotation.controllers()) { - if (isTypeOrAnnotated(metadataReader, metadataReaderFactory, controller)) { - return true; - } - } - return false; - } - @Override protected boolean hasAnnotation() { return this.annotation != null; @@ -117,4 +101,9 @@ class WebMvcTypeExcludeFilter extends AnnotationCustomizableTypeExcludeFilter { return DEFAULT_INCLUDES; } + @Override + protected Set> getComponentIncludes() { + return new LinkedHashSet>(Arrays.asList(this.annotation.controllers())); + } + } diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java new file mode 100644 index 0000000000..45d5dbbfdc --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-2017 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.test.autoconfigure.cache; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Test; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.InitializationError; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.ExampleEntity; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@code ImportsContextCustomizerFactory} when used with + * {@link ImportAutoConfiguration}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +public class ImportsContextCustomizerFactoryWithAutoConfigurationTests { + + static ApplicationContext contextFromTest; + + @Test + public void testClassesThatHaveSameAnnotationsShareAContext() + throws InitializationError { + RunNotifier notifier = new RunNotifier(); + new SpringJUnit4ClassRunner(DataJpaTest1.class).run(notifier); + ApplicationContext test1Context = contextFromTest; + new SpringJUnit4ClassRunner(DataJpaTest3.class).run(notifier); + ApplicationContext test2Context = contextFromTest; + assertThat(test1Context).isSameAs(test2Context); + } + + @Test + public void testClassesThatOnlyHaveDifferingUnrelatedAnnotationsShareAContext() + throws InitializationError { + RunNotifier notifier = new RunNotifier(); + new SpringJUnit4ClassRunner(DataJpaTest1.class).run(notifier); + ApplicationContext test1Context = contextFromTest; + new SpringJUnit4ClassRunner(DataJpaTest2.class).run(notifier); + ApplicationContext test2Context = contextFromTest; + assertThat(test1Context).isSameAs(test2Context); + } + + @Test + public void testClassesThatOnlyHaveDifferingPropertyMappedAnnotationAttributesDoNotShareAContext() + throws InitializationError { + RunNotifier notifier = new RunNotifier(); + new SpringJUnit4ClassRunner(DataJpaTest1.class).run(notifier); + ApplicationContext test1Context = contextFromTest; + new SpringJUnit4ClassRunner(DataJpaTest4.class).run(notifier); + ApplicationContext test2Context = contextFromTest; + assertThat(test1Context).isNotSameAs(test2Context); + } + + @DataJpaTest + @ContextConfiguration(classes = EmptyConfig.class) + @Unrelated1 + public static class DataJpaTest1 { + + @Autowired + private ApplicationContext context; + + @Test + public void test() { + contextFromTest = this.context; + } + + } + + @ContextConfiguration(classes = EmptyConfig.class) + @DataJpaTest + @Unrelated2 + public static class DataJpaTest2 { + + @Autowired + private ApplicationContext context; + + @Test + public void test() { + contextFromTest = this.context; + } + + } + + @ContextConfiguration(classes = EmptyConfig.class) + @DataJpaTest + @Unrelated1 + public static class DataJpaTest3 { + + @Autowired + private ApplicationContext context; + + @Test + public void test() { + contextFromTest = this.context; + } + + } + + @ContextConfiguration(classes = EmptyConfig.class) + @DataJpaTest(showSql = false) + @Unrelated1 + public static class DataJpaTest4 { + + @Autowired + private ApplicationContext context; + + @Test + public void test() { + contextFromTest = this.context; + } + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface Unrelated1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface Unrelated2 { + + } + + @Configuration + @EntityScan(basePackageClasses = ExampleEntity.class) + static class EmptyConfig { + + } + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java index c4358faad0..61f15a7186 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java @@ -122,12 +122,22 @@ public class TypeExcludeFiltersContextCustomizerFactoryTests { .equals(getClass().getName()); } + @Override + public int hashCode() { + return SimpleExclude.class.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj.getClass().equals(getClass()); + } + } static class TestClassAwareExclude extends SimpleExclude { TestClassAwareExclude(Class testClass) { - assertThat(testClass).isEqualTo(WithExcludeFilters.class); + assertThat(testClass).isNotNull(); } } diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 17eea896f9..2b32b96f05 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -18,8 +18,10 @@ package org.springframework.boot.test.context; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.springframework.beans.BeansException; @@ -30,19 +32,24 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportSelector; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; +import org.springframework.core.style.ToStringCreator; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ReflectionUtils; /** * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on @@ -127,6 +134,11 @@ class ImportsContextCustomizer implements ContextCustomizer { return this.key.equals(other.key); } + @Override + public String toString() { + return new ToStringCreator(this).append("key", this.key).toString(); + } + /** * {@link Configuration} registered to trigger the {@link ImportsSelector}. */ @@ -214,6 +226,8 @@ class ImportsContextCustomizer implements ContextCustomizer { */ static class ContextCustomizerKey { + private static final Class[] NO_IMPORTS = {}; + private static final Set ANNOTATION_FILTERS; static { @@ -224,13 +238,15 @@ class ImportsContextCustomizer implements ContextCustomizer { ANNOTATION_FILTERS = Collections.unmodifiableSet(filters); } - private final Set annotations; + private final Set key; ContextCustomizerKey(Class testClass) { Set annotations = new HashSet(); Set> seen = new HashSet>(); collectClassAnnotations(testClass, annotations, seen); - this.annotations = Collections.unmodifiableSet(annotations); + Set determinedImports = determineImports(annotations, testClass); + this.key = Collections.unmodifiableSet( + determinedImports != null ? determinedImports : annotations); } private void collectClassAnnotations(Class classType, @@ -266,17 +282,78 @@ class ImportsContextCustomizer implements ContextCustomizer { return false; } + private Set determineImports(Set annotations, + Class testClass) { + Set determinedImports = new LinkedHashSet(); + AnnotationMetadata testClassMetadata = new StandardAnnotationMetadata( + testClass); + for (Annotation annotation : annotations) { + for (Class source : getImports(annotation)) { + Set determinedSourceImports = determineImports(source, + testClassMetadata); + if (determinedSourceImports == null) { + return null; + } + determinedImports.addAll(determinedSourceImports); + } + } + return determinedImports; + } + + private Class[] getImports(Annotation annotation) { + if (annotation instanceof Import) { + return ((Import) annotation).value(); + } + return NO_IMPORTS; + } + + private Set determineImports(Class source, + AnnotationMetadata metadata) { + if (DeterminableImports.class.isAssignableFrom(source)) { + // We can determine the imports + return ((DeterminableImports) instantiate(source)) + .determineImports(metadata); + } + if (ImportSelector.class.isAssignableFrom(source) + || ImportBeanDefinitionRegistrar.class.isAssignableFrom(source)) { + // Standard ImportSelector and ImportBeanDefinitionRegistrar could + // use anything to determine the imports so we can't be sure + return null; + } + // The source itself is the import + return Collections.singleton(source.getName()); + } + + @SuppressWarnings("unchecked") + private T instantiate(Class source) { + try { + Constructor constructor = source.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(constructor); + return (T) constructor.newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Unable to instantiate DeterminableImportSelector " + + source.getName(), + ex); + } + } + @Override public int hashCode() { - return this.annotations.hashCode(); + return this.key.hashCode(); } @Override public boolean equals(Object obj) { return (obj != null && getClass().equals(obj.getClass()) - && this.annotations.equals(((ContextCustomizerKey) obj).annotations)); + && this.key.equals(((ContextCustomizerKey) obj).key)); } + @Override + public String toString() { + return this.key.toString(); + } } /** diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java index aeec7702f6..337eeb6ff2 100644 --- a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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,10 +16,21 @@ package org.springframework.boot.test.context; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.Set; + import kotlin.Metadata; import org.junit.Test; import org.spockframework.runtime.model.SpecMetadata; +import org.springframework.boot.context.annotation.DeterminableImports; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -29,6 +40,26 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class ImportsContextCustomizerTests { + @Test + public void importSelectorsCouldUseAnyAnnotations() throws Exception { + assertThat(new ImportsContextCustomizer(FirstImportSelectorAnnotatedClass.class)) + .isNotEqualTo(new ImportsContextCustomizer( + SecondImportSelectorAnnotatedClass.class)); + } + + @Test + public void determinableImportSelector() throws Exception { + assertThat(new ImportsContextCustomizer( + FirstDeterminableImportSelectorAnnotatedClass.class)) + .isEqualTo(new ImportsContextCustomizer( + SecondDeterminableImportSelectorAnnotatedClass.class)); + } + + @Test + public void importAutoConfigurationCanIgnoreAdditionalAnnotations() throws Exception { + + } + @Test public void customizersForTestClassesWithDifferentKotlinMetadataAreEqual() { assertThat(new ImportsContextCustomizer(FirstKotlinAnnotatedTestClass.class)) @@ -43,6 +74,30 @@ public class ImportsContextCustomizerTests { SecondSpockAnnotatedTestClass.class)); } + @Import(TestImportSelector.class) + @Indicator1 + static class FirstImportSelectorAnnotatedClass { + + } + + @Import(TestImportSelector.class) + @Indicator2 + static class SecondImportSelectorAnnotatedClass { + + } + + @Import(TestDeterminableImportSelector.class) + @Indicator1 + static class FirstDeterminableImportSelectorAnnotatedClass { + + } + + @Import(TestDeterminableImportSelector.class) + @Indicator2 + static class SecondDeterminableImportSelectorAnnotatedClass { + + } + @Metadata(d2 = "foo") static class FirstKotlinAnnotatedTestClass { @@ -63,4 +118,43 @@ public class ImportsContextCustomizerTests { } + @Retention(RetentionPolicy.RUNTIME) + @interface Indicator1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Indicator2 { + + } + + static class TestImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata arg0) { + return new String[] {}; + } + + } + + static class TestDeterminableImportSelector + implements ImportSelector, DeterminableImports { + + @Override + public String[] selectImports(AnnotationMetadata arg0) { + return new String[] { TestConfig.class.getName() }; + } + + @Override + public Set determineImports(AnnotationMetadata metadata) { + return Collections.singleton(TestConfig.class.getName()); + } + + } + + @Configuration + static class TestConfig { + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/TypeExcludeFilter.java b/spring-boot/src/main/java/org/springframework/boot/context/TypeExcludeFilter.java index 03e6402843..6738503ca0 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/TypeExcludeFilter.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/TypeExcludeFilter.java @@ -36,7 +36,9 @@ import org.springframework.core.type.filter.TypeFilter; * *

* Implementations should provide a subclass registered with {@link BeanFactory} and - * override the {@link #match(MetadataReader, MetadataReaderFactory)} method. + * override the {@link #match(MetadataReader, MetadataReaderFactory)} method. They should + * also implement a valid {@link #hashCode() hashCode} and {@link #equals(Object) equals} + * methods so that they can be used as part of Spring test's application context caches. *

* Note that {@code TypeExcludeFilters} are initialized very early in the application * lifecycle, they should generally not have dependencies on any other beans. They are @@ -70,4 +72,16 @@ public class TypeExcludeFilter implements TypeFilter, BeanFactoryAware { return false; } + @Override + public boolean equals(Object obj) { + throw new IllegalStateException( + "TypeExcludeFilter " + getClass() + " has not implemented equals"); + } + + @Override + public int hashCode() { + throw new IllegalStateException( + "TypeExcludeFilter " + getClass() + " has not implemented hashCode"); + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/annotation/DeterminableImports.java b/spring-boot/src/main/java/org/springframework/boot/context/annotation/DeterminableImports.java new file mode 100644 index 0000000000..0eb1c3ede8 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/annotation/DeterminableImports.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2017 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.context.annotation; + +import java.util.Set; + +import org.springframework.beans.factory.Aware; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Interface that can be implemented by {@link ImportSelector} and + * {@link ImportBeanDefinitionRegistrar} implementations when they can determine imports + * early. The {@link ImportSelector} and {@link ImportBeanDefinitionRegistrar} interfaces + * are quite flexible which can make it hard to tell exactly what bean definitions they + * will add. This interface should be used when an implementation consistently result in + * the same imports, given the same source. + *

+ * Using {@link DeterminableImports} is particularly useful when working with Spring's + * testing support. It allows for better generation of {@link ApplicationContext} cache + * keys. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.5.0 + */ +public interface DeterminableImports { + + /** + * Return a set of objects that represent the imports. Objects within the returned + * {@code Set} must implement a valid {@link Object#hashCode() hashCode} and + * {@link Object#equals(Object) equals}. + *

+ * Imports from multiple {@link DeterminableImports} instances may be combined by the + * caller to create a complete set. + *

+ * Unlike {@link ImportSelector} and {@link ImportBeanDefinitionRegistrar} any + * {@link Aware} callbacks will not be invoked before this method is called. + * @param metadata the source meta-data + * @return a key representing the annotations that actually drive the import + */ + Set determineImports(AnnotationMetadata metadata); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/annotation/package-info.java b/spring-boot/src/main/java/org/springframework/boot/context/annotation/package-info.java new file mode 100644 index 0000000000..614959bf31 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/annotation/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Classes related to Spring's {@link org.springframework.context.ApplicationContext} + * annotations. + */ +package org.springframework.boot.context.annotation;