Commit fa6a1385 authored by Phillip Webb's avatar Phillip Webb

Refine ImportsContextCustomizer cache logic

Update `ImportsContextCustomizer` so that whenever possible a more
specific cache key is used.

Prior to this commit the customizer would generate a key based on *all*
annotations on the test class. This has repeatedly caused issues where
test classes that should have the same cache key did not due to
unrelated annotations.

A new `DeterminableImports` interface has been added that can be
implemented by `ImportSelector` and `ImportBeanDefinitionRegistrar`
implementations that are able to determine their imports early. The
existing `ImportAutoConfigurationImportSelector` and
`AutoConfigurationPackages` classes have been retrofitted with
this interface.

Fixes gh-7953
parent aaf118c5
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure; ...@@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
...@@ -31,6 +32,7 @@ import org.springframework.beans.factory.config.BeanDefinition; ...@@ -31,6 +32,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.annotation.DeterminableImports;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
...@@ -122,12 +124,52 @@ public abstract class AutoConfigurationPackages { ...@@ -122,12 +124,52 @@ public abstract class AutoConfigurationPackages {
* configuration. * configuration.
*/ */
@Order(Ordered.HIGHEST_PRECEDENCE) @Order(Ordered.HIGHEST_PRECEDENCE)
static class Registrar implements ImportBeanDefinitionRegistrar { static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override @Override
public void registerBeanDefinitions(AnnotationMetadata metadata, public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) { BeanDefinitionRegistry registry) {
register(registry, ClassUtils.getPackageName(metadata.getClassName())); register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.<Object>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;
} }
} }
......
...@@ -27,6 +27,7 @@ import java.util.List; ...@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.boot.context.annotation.DeterminableImports;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
...@@ -44,7 +45,8 @@ import org.springframework.util.ObjectUtils; ...@@ -44,7 +45,8 @@ import org.springframework.util.ObjectUtils;
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector { class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector
implements DeterminableImports {
private static final Set<String> ANNOTATION_NAMES; private static final Set<String> ANNOTATION_NAMES;
...@@ -55,6 +57,14 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec ...@@ -55,6 +57,14 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec
ANNOTATION_NAMES = Collections.unmodifiableSet(names); ANNOTATION_NAMES = Collections.unmodifiableSet(names);
} }
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
Set<String> result = new LinkedHashSet<String>();
result.addAll(getCandidateConfigurations(metadata, null));
result.removeAll(getExclusions(metadata, null));
return Collections.<Object>unmodifiableSet(result);
}
@Override @Override
protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) { protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
return null; return null;
...@@ -85,6 +95,10 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec ...@@ -85,6 +95,10 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec
if (classes.length > 0) { if (classes.length > 0) {
return Arrays.asList(classes); return Arrays.asList(classes);
} }
return loadFactoryNames(source);
}
protected Collection<String> loadFactoryNames(Class<?> source) {
return SpringFactoriesLoader.loadFactoryNames(source, return SpringFactoriesLoader.loadFactoryNames(source,
getClass().getClassLoader()); getClass().getClassLoader());
} }
...@@ -117,7 +131,8 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec ...@@ -117,7 +131,8 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec
return exclusions; return exclusions;
} }
private Map<Class<?>, List<Annotation>> getAnnotations(AnnotationMetadata metadata) { protected final Map<Class<?>, List<Annotation>> getAnnotations(
AnnotationMetadata metadata) {
MultiValueMap<Class<?>, Annotation> annotations = new LinkedMultiValueMap<Class<?>, Annotation>(); MultiValueMap<Class<?>, Annotation> annotations = new LinkedMultiValueMap<Class<?>, Annotation>();
Class<?> source = ClassUtils.resolveClassName(metadata.getClassName(), null); Class<?> source = ClassUtils.resolveClassName(metadata.getClassName(), null);
collectAnnotations(source, annotations, new HashSet<Class<?>>()); collectAnnotations(source, annotations, new HashSet<Class<?>>());
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,6 +19,9 @@ package org.springframework.boot.autoconfigure; ...@@ -19,6 +19,9 @@ package org.springframework.boot.autoconfigure;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -46,7 +49,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; ...@@ -46,7 +49,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
*/ */
public class ImportAutoConfigurationImportSelectorTests { public class ImportAutoConfigurationImportSelectorTests {
private final ImportAutoConfigurationImportSelector importSelector = new ImportAutoConfigurationImportSelector(); private final ImportAutoConfigurationImportSelector importSelector = new TestImportAutoConfigurationImportSelector();
private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory();
...@@ -63,36 +66,32 @@ public class ImportAutoConfigurationImportSelectorTests { ...@@ -63,36 +66,32 @@ public class ImportAutoConfigurationImportSelectorTests {
@Test @Test
public void importsAreSelected() throws Exception { public void importsAreSelected() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(ImportFreeMarker.class.getName()) ImportFreeMarker.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName()); assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName());
} }
@Test @Test
public void importsAreSelectedUsingClassesAttribute() throws Exception { public void importsAreSelectedUsingClassesAttribute() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(ImportFreeMarkerUsingClassesAttribute.class.getName()) ImportFreeMarkerUsingClassesAttribute.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName()); assertThat(imports).containsExactly(FreeMarkerAutoConfiguration.class.getName());
} }
@Test @Test
public void propertyExclusionsAreNotApplied() throws Exception { public void propertyExclusionsAreNotApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(ImportFreeMarker.class.getName()) ImportFreeMarker.class);
.getAnnotationMetadata();
this.importSelector.selectImports(annotationMetadata); this.importSelector.selectImports(annotationMetadata);
verifyZeroInteractions(this.environment); verifyZeroInteractions(this.environment);
} }
@Test @Test
public void multipleImportsAreFound() throws Exception { public void multipleImportsAreFound() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(MultipleImports.class.getName()) MultipleImports.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName(), assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName(),
ThymeleafAutoConfiguration.class.getName()); ThymeleafAutoConfiguration.class.getName());
...@@ -100,41 +99,94 @@ public class ImportAutoConfigurationImportSelectorTests { ...@@ -100,41 +99,94 @@ public class ImportAutoConfigurationImportSelectorTests {
@Test @Test
public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException { public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(ImportWithSelfAnnotatingAnnotation.class.getName()) ImportWithSelfAnnotatingAnnotation.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName()); assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName());
} }
@Test @Test
public void exclusionsAreApplied() throws Exception { public void exclusionsAreApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(MultipleImportsWithExclusion.class.getName()) MultipleImportsWithExclusion.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName()); assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName());
} }
@Test @Test
public void exclusionsWithoutImport() throws Exception { public void exclusionsWithoutImport() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader(ExclusionWithoutImport.class.getName()) ExclusionWithoutImport.class);
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName()); assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName());
} }
@Test @Test
public void exclusionsAliasesAreApplied() throws Exception { public void exclusionsAliasesAreApplied() throws Exception {
AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() AnnotationMetadata annotationMetadata = getAnnotationMetadata(
.getMetadataReader( ImportWithSelfAnnotatingAnnotationExclude.class);
ImportWithSelfAnnotatingAnnotationExclude.class.getName())
.getAnnotationMetadata();
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).isEmpty(); assertThat(imports).isEmpty();
} }
@Test
public void determineImportsWhenUsingMetaWithoutClassesShouldBeEqual()
throws Exception {
Set<Object> set1 = this.importSelector.determineImports(
getAnnotationMetadata(ImportMetaAutoConfigurationWithUnrelatedOne.class));
Set<Object> set2 = this.importSelector.determineImports(
getAnnotationMetadata(ImportMetaAutoConfigurationWithUnrelatedTwo.class));
assertThat(set1).isEqualTo(set2);
assertThat(set1.hashCode()).isEqualTo(set2.hashCode());
}
@Test
public void determineImportsWhenUsingNonMetaWithoutClassesShouldBeSame()
throws Exception {
Set<Object> set1 = this.importSelector.determineImports(
getAnnotationMetadata(ImportAutoConfigurationWithUnrelatedOne.class));
Set<Object> set2 = this.importSelector.determineImports(
getAnnotationMetadata(ImportAutoConfigurationWithUnrelatedTwo.class));
assertThat(set1).isEqualTo(set2);
}
@Test
public void determineImportsWhenUsingNonMetaWithClassesShouldBeSame()
throws Exception {
Set<Object> set1 = this.importSelector.determineImports(
getAnnotationMetadata(ImportAutoConfigurationWithItemsOne.class));
Set<Object> set2 = this.importSelector.determineImports(
getAnnotationMetadata(ImportAutoConfigurationWithItemsOne.class));
assertThat(set1).isEqualTo(set2);
}
@Test
public void determineImportsWhenUsingMetaExcludeWithoutClassesShouldBeEqual()
throws Exception {
Set<Object> set1 = this.importSelector.determineImports(getAnnotationMetadata(
ImportMetaAutoConfigurationExcludeWithUnrelatedOne.class));
Set<Object> set2 = this.importSelector.determineImports(getAnnotationMetadata(
ImportMetaAutoConfigurationExcludeWithUnrelatedTwo.class));
assertThat(set1).isEqualTo(set2);
assertThat(set1.hashCode()).isEqualTo(set2.hashCode());
}
@Test
public void determineImportsWhenUsingMetaDifferentExcludeWithoutClassesShouldBeDifferent()
throws Exception {
Set<Object> set1 = this.importSelector.determineImports(getAnnotationMetadata(
ImportMetaAutoConfigurationExcludeWithUnrelatedOne.class));
Set<Object> 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) @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
static class ImportFreeMarker { static class ImportFreeMarker {
...@@ -186,6 +238,73 @@ public class ImportAutoConfigurationImportSelectorTests { ...@@ -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) @Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(ThymeleafAutoConfiguration.class)
@SelfAnnotating @SelfAnnotating
...@@ -196,4 +315,18 @@ public class ImportAutoConfigurationImportSelectorTests { ...@@ -196,4 +315,18 @@ public class ImportAutoConfigurationImportSelectorTests {
} }
private static class TestImportAutoConfigurationImportSelector
extends ImportAutoConfigurationImportSelector {
@Override
protected Collection<String> loadFactoryNames(Class<?> source) {
if (source == MetaImportAutoConfiguration.class) {
return Arrays.asList(ThymeleafAutoConfiguration.class.getName(),
FreeMarkerAutoConfiguration.class.getName());
}
return super.loadFactoryNames(source);
}
}
} }
/*
* 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 {
}
}
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,8 +18,10 @@ package org.springframework.boot.test.context; ...@@ -18,8 +18,10 @@ package org.springframework.boot.test.context;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
...@@ -30,19 +32,24 @@ import org.springframework.beans.factory.config.BeanDefinition; ...@@ -30,19 +32,24 @@ import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.context.annotation.DeterminableImports;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.ImportSelector; import org.springframework.context.annotation.ImportSelector;
import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.style.ToStringCreator;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.StandardAnnotationMetadata;
import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.ReflectionUtils;
/** /**
* {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on
...@@ -127,6 +134,11 @@ class ImportsContextCustomizer implements ContextCustomizer { ...@@ -127,6 +134,11 @@ class ImportsContextCustomizer implements ContextCustomizer {
return this.key.equals(other.key); 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}. * {@link Configuration} registered to trigger the {@link ImportsSelector}.
*/ */
...@@ -214,6 +226,8 @@ class ImportsContextCustomizer implements ContextCustomizer { ...@@ -214,6 +226,8 @@ class ImportsContextCustomizer implements ContextCustomizer {
*/ */
static class ContextCustomizerKey { static class ContextCustomizerKey {
private static final Class<?>[] NO_IMPORTS = {};
private static final Set<AnnotationFilter> ANNOTATION_FILTERS; private static final Set<AnnotationFilter> ANNOTATION_FILTERS;
static { static {
...@@ -224,13 +238,15 @@ class ImportsContextCustomizer implements ContextCustomizer { ...@@ -224,13 +238,15 @@ class ImportsContextCustomizer implements ContextCustomizer {
ANNOTATION_FILTERS = Collections.unmodifiableSet(filters); ANNOTATION_FILTERS = Collections.unmodifiableSet(filters);
} }
private final Set<Annotation> annotations; private final Set<Object> key;
ContextCustomizerKey(Class<?> testClass) { ContextCustomizerKey(Class<?> testClass) {
Set<Annotation> annotations = new HashSet<Annotation>(); Set<Annotation> annotations = new HashSet<Annotation>();
Set<Class<?>> seen = new HashSet<Class<?>>(); Set<Class<?>> seen = new HashSet<Class<?>>();
collectClassAnnotations(testClass, annotations, seen); collectClassAnnotations(testClass, annotations, seen);
this.annotations = Collections.unmodifiableSet(annotations); Set<Object> determinedImports = determineImports(annotations, testClass);
this.key = Collections.<Object>unmodifiableSet(
determinedImports != null ? determinedImports : annotations);
} }
private void collectClassAnnotations(Class<?> classType, private void collectClassAnnotations(Class<?> classType,
...@@ -266,17 +282,78 @@ class ImportsContextCustomizer implements ContextCustomizer { ...@@ -266,17 +282,78 @@ class ImportsContextCustomizer implements ContextCustomizer {
return false; return false;
} }
private Set<Object> determineImports(Set<Annotation> annotations,
Class<?> testClass) {
Set<Object> determinedImports = new LinkedHashSet<Object>();
AnnotationMetadata testClassMetadata = new StandardAnnotationMetadata(
testClass);
for (Annotation annotation : annotations) {
for (Class<?> source : getImports(annotation)) {
Set<Object> 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<Object> 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.<Object>singleton(source.getName());
}
@SuppressWarnings("unchecked")
private <T> T instantiate(Class<T> 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 @Override
public int hashCode() { public int hashCode() {
return this.annotations.hashCode(); return this.key.hashCode();
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return (obj != null && getClass().equals(obj.getClass()) 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();
}
} }
/** /**
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,10 +16,21 @@ ...@@ -16,10 +16,21 @@
package org.springframework.boot.test.context; 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 kotlin.Metadata;
import org.junit.Test; import org.junit.Test;
import org.spockframework.runtime.model.SpecMetadata; 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; import static org.assertj.core.api.Assertions.assertThat;
/** /**
...@@ -29,6 +40,26 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -29,6 +40,26 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class ImportsContextCustomizerTests { 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 @Test
public void customizersForTestClassesWithDifferentKotlinMetadataAreEqual() { public void customizersForTestClassesWithDifferentKotlinMetadataAreEqual() {
assertThat(new ImportsContextCustomizer(FirstKotlinAnnotatedTestClass.class)) assertThat(new ImportsContextCustomizer(FirstKotlinAnnotatedTestClass.class))
...@@ -43,6 +74,30 @@ public class ImportsContextCustomizerTests { ...@@ -43,6 +74,30 @@ public class ImportsContextCustomizerTests {
SecondSpockAnnotatedTestClass.class)); 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") @Metadata(d2 = "foo")
static class FirstKotlinAnnotatedTestClass { static class FirstKotlinAnnotatedTestClass {
...@@ -63,4 +118,43 @@ public class ImportsContextCustomizerTests { ...@@ -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<Object> determineImports(AnnotationMetadata metadata) {
return Collections.<Object>singleton(TestConfig.class.getName());
}
}
@Configuration
static class TestConfig {
}
} }
/*
* 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.
* <p>
* 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}.
* <p>
* Imports from multiple {@link DeterminableImports} instances may be combined by the
* caller to create a complete set.
* <p>
* 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<Object> determineImports(AnnotationMetadata metadata);
}
/*
* 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;
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