Skip to content

Commit 985cb9d

Browse files
committed
Merge pull request #176 from philwebb/SPR-9925
# By Phillip Webb * SPR-9925: Prevent duplicate @import processing Polish Javadoc for @import Improve #toString for AnnotationAttributes
2 parents 2081521 + 6d8b37d commit 985cb9d

File tree

4 files changed

+113
-45
lines changed

4 files changed

+113
-45
lines changed

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717
package org.springframework.context.annotation;
1818

1919
import java.io.IOException;
20-
import java.lang.annotation.Annotation;
21-
import java.util.ArrayList;
20+
import java.util.Arrays;
2221
import java.util.Collections;
2322
import java.util.Comparator;
2423
import java.util.HashMap;
24+
import java.util.HashSet;
2525
import java.util.Iterator;
2626
import java.util.LinkedHashSet;
27-
import java.util.List;
2827
import java.util.Map;
2928
import java.util.Set;
3029
import java.util.Stack;
@@ -222,10 +221,9 @@ protected AnnotationMetadata doProcessConfigurationClass(
222221
}
223222

224223
// process any @Import annotations
225-
List<AnnotationAttributes> imports =
226-
findAllAnnotationAttributes(Import.class, metadata.getClassName(), true);
227-
for (AnnotationAttributes importAnno : imports) {
228-
processImport(configClass, importAnno.getStringArray("value"), true);
224+
Set<String> imports = getImports(metadata.getClassName(), null, new HashSet<String>());
225+
if (imports != null && !imports.isEmpty()) {
226+
processImport(configClass, imports.toArray(new String[imports.size()]), true);
229227
}
230228

231229
// process any @ImportResource annotations
@@ -265,45 +263,36 @@ protected AnnotationMetadata doProcessConfigurationClass(
265263
}
266264

267265
/**
268-
* Return a list of attribute maps for all declarations of the given annotation
269-
* on the given annotated class using the given MetadataReaderFactory to introspect
270-
* annotation metadata. Meta-annotations are ordered first in the list, and if the
271-
* target annotation is declared directly on the class, its map of attributes will be
272-
* ordered last in the list.
273-
* @param targetAnnotation the annotation to search for, both locally and as a meta-annotation
274-
* @param annotatedClassName the class to inspect
275-
* @param classValuesAsString whether class attributes should be returned as strings
266+
* Recursively collect all declared {@code @Import} values. Unlike most
267+
* meta-annotations it is valid to have several {@code @Import}s declared with
268+
* different values, the usual process or returning values from the first
269+
* meta-annotation on a class is not sufficient.
270+
* <p>For example, it is common for a {@code @Configuration} class to declare direct
271+
* {@code @Import}s in addition to meta-imports originating from an {@code @Enable}
272+
* annotation.
273+
* @param className the class name to search
274+
* @param imports the imports collected so far or {@code null}
275+
* @param visited used to track visited classes to prevent infinite recursion (must not be null)
276+
* @return a set of all {@link Import#value() import values} or {@code null}
277+
* @throws IOException if there is any problem reading metadata from the named class
276278
*/
277-
private List<AnnotationAttributes> findAllAnnotationAttributes(
278-
Class<? extends Annotation> targetAnnotation, String annotatedClassName,
279-
boolean classValuesAsString) throws IOException {
280-
281-
List<AnnotationAttributes> allAttribs = new ArrayList<AnnotationAttributes>();
282-
283-
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(annotatedClassName);
284-
AnnotationMetadata metadata = reader.getAnnotationMetadata();
285-
String targetAnnotationType = targetAnnotation.getName();
286-
287-
for (String annotationType : metadata.getAnnotationTypes()) {
288-
if (annotationType.equals(targetAnnotationType)) {
289-
continue;
279+
private Set<String> getImports(String className, Set<String> imports,
280+
Set<String> visited) throws IOException {
281+
if (visited.add(className)) {
282+
AnnotationMetadata metadata = metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata();
283+
Map<String, Object> attributes = metadata.getAnnotationAttributes(Import.class.getName(), true);
284+
if (attributes != null) {
285+
String[] value = (String[]) attributes.get("value");
286+
if (value != null && value.length > 0) {
287+
imports = (imports == null ? new LinkedHashSet<String>() : imports);
288+
imports.addAll(Arrays.asList(value));
289+
}
290290
}
291-
AnnotationMetadata metaAnnotations =
292-
this.metadataReaderFactory.getMetadataReader(annotationType).getAnnotationMetadata();
293-
AnnotationAttributes targetAttribs =
294-
AnnotationAttributes.fromMap(metaAnnotations.getAnnotationAttributes(targetAnnotationType, classValuesAsString));
295-
if (targetAttribs != null) {
296-
allAttribs.add(targetAttribs);
291+
for (String annotationType : metadata.getAnnotationTypes()) {
292+
getImports(annotationType, imports, visited);
297293
}
298294
}
299-
300-
AnnotationAttributes localAttribs =
301-
AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(targetAnnotationType, classValuesAsString));
302-
if (localAttribs != null) {
303-
allAttribs.add(localAttribs);
304-
}
305-
306-
return allAttribs;
295+
return imports;
307296
}
308297

309298
private void processImport(ConfigurationClass configClass, String[] classesToImport, boolean checkForCircularImports) throws IOException {
@@ -440,5 +429,4 @@ public CircularImportProblem(ConfigurationClass attemptedImport, Stack<Configura
440429
new Location(importStack.peek().getResource(), metadata));
441430
}
442431
}
443-
444432
}

spring-context/src/main/java/org/springframework/context/annotation/Import.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
public @interface Import {
5454

5555
/**
56-
* The @{@link Configuration} and/or {@link ImportSelector} classes to import.
56+
* The @{@link Configuration}, {@link ImportSelector} and/or
57+
* {@link ImportBeanDefinitionRegistrar} classes to import.
5758
*/
5859
Class<?>[] value();
5960
}

spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
import org.springframework.beans.BeansException;
2626
import org.springframework.beans.factory.BeanFactory;
2727
import org.springframework.beans.factory.BeanFactoryAware;
28+
import org.springframework.beans.factory.config.BeanDefinition;
2829
import org.springframework.beans.factory.config.BeanPostProcessor;
30+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
31+
import org.springframework.beans.factory.support.GenericBeanDefinition;
2932
import org.springframework.core.annotation.AnnotationAttributes;
3033
import org.springframework.core.type.AnnotationMetadata;
3134
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
35+
import org.springframework.util.Assert;
3236

3337
import static org.hamcrest.CoreMatchers.*;
3438
import static org.junit.Assert.*;
@@ -77,6 +81,24 @@ public void indirectlyAnnotatedWithImport() {
7781
assertThat(foo, is("xyz"));
7882
}
7983

84+
@Test
85+
public void importRegistrar() throws Exception {
86+
ImportedRegistrar.called = false;
87+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
88+
ctx.register(ImportingRegistrarConfig.class);
89+
ctx.refresh();
90+
assertNotNull(ctx.getBean("registrarImportedBean"));
91+
}
92+
93+
@Test
94+
public void importRegistrarWithImport() throws Exception {
95+
ImportedRegistrar.called = false;
96+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
97+
ctx.register(ImportingRegistrarConfigWithImport.class);
98+
ctx.refresh();
99+
assertNotNull(ctx.getBean("registrarImportedBean"));
100+
assertNotNull(ctx.getBean(ImportedConfig.class));
101+
}
80102

81103
@Configuration
82104
@Import(ImportedConfig.class)
@@ -131,4 +153,35 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw
131153
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
132154
}
133155
}
156+
157+
@Configuration
158+
@EnableImportRegistrar
159+
static class ImportingRegistrarConfig {
160+
}
161+
162+
@Configuration
163+
@EnableImportRegistrar
164+
@Import(ImportedConfig.class)
165+
static class ImportingRegistrarConfigWithImport {
166+
}
167+
168+
@Target(ElementType.TYPE)
169+
@Retention(RetentionPolicy.RUNTIME)
170+
@Import(ImportedRegistrar.class)
171+
public @interface EnableImportRegistrar {
172+
}
173+
174+
static class ImportedRegistrar implements ImportBeanDefinitionRegistrar {
175+
176+
static boolean called;
177+
178+
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
179+
BeanDefinitionRegistry registry) {
180+
BeanDefinition beanDefinition = new GenericBeanDefinition();
181+
beanDefinition.setBeanClassName(String.class.getName());
182+
registry.registerBeanDefinition("registrarImportedBean", beanDefinition );
183+
Assert.state(called == false, "ImportedRegistrar called twice");
184+
called = true;
185+
}
186+
}
134187
}

spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import static java.lang.String.format;
2020

21+
import java.util.Iterator;
2122
import java.util.LinkedHashMap;
2223
import java.util.Map;
2324

2425
import org.springframework.util.Assert;
26+
import org.springframework.util.StringUtils;
2527

2628
/**
2729
* {@link LinkedHashMap} subclass representing annotation attribute key/value pairs
@@ -129,4 +131,28 @@ private <T> T doGet(String attributeName, Class<T> expectedType) {
129131
attributeName, value.getClass().getSimpleName(), expectedType.getSimpleName()));
130132
return (T) value;
131133
}
132-
}
134+
135+
public String toString() {
136+
Iterator<Map.Entry<String, Object>> entries = entrySet().iterator();
137+
StringBuilder sb = new StringBuilder("{");
138+
while (entries.hasNext()) {
139+
Map.Entry<String, Object> entry = entries.next();
140+
sb.append(entry.getKey());
141+
sb.append('=');
142+
sb.append(valueToString(entry.getValue()));
143+
sb.append(entries.hasNext() ? ", " : "");
144+
}
145+
sb.append("}");
146+
return sb.toString();
147+
}
148+
149+
private String valueToString(Object value) {
150+
if (value == this) {
151+
return "(this Map)";
152+
}
153+
if (value instanceof Object[]) {
154+
return "[" + StringUtils.arrayToCommaDelimitedString((Object[]) value) + "]";
155+
}
156+
return String.valueOf(value);
157+
}
158+
}

0 commit comments

Comments
 (0)