diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 104e2fdd2102..b8086aca92fa 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -98,6 +98,7 @@ * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll + * @author Daeho Kwon * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -549,6 +550,9 @@ private Set getImports(SourceClass sourceClass) throws IOException *

For example, it is common for a {@code @Configuration} class to declare direct * {@code @Import}s in addition to meta-imports originating from an {@code @Enable} * annotation. + *

As of Spring Framework 7.0, {@code @Import} annotations declared on interfaces implemented by + * the configuration class are also considered. This allows imports to be triggered + * indirectly via marker interfaces or shared base interfaces. * @param sourceClass the class to search * @param imports the imports collected so far * @param visited used to track visited classes to prevent infinite recursion @@ -558,6 +562,9 @@ private void collectImports(SourceClass sourceClass, Set imports, S throws IOException { if (visited.add(sourceClass)) { + for (SourceClass ifc : sourceClass.getInterfaces()) { + collectImports(ifc, imports, visited); + } for (SourceClass annotation : sourceClass.getAnnotations()) { String annName = annotation.getMetadata().getClassName(); if (!annName.equals(Import.class.getName())) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java index 2014c12000b2..974100791179 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -62,6 +62,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Daeho Kwon */ @SuppressWarnings("resource") public class ImportSelectorTests { @@ -203,6 +204,71 @@ void invokeAwareMethodsInImportGroup() { assertThat(TestImportGroup.environment).isEqualTo(context.getEnvironment()); } + @Test + void importAnnotationOnImplementedInterfaceIsRespected() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + InterfaceBasedConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class).name()).isEqualTo("imported"); + } + + @Test + void localImportShouldOverrideInterfaceImport() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + OverridingConfig.class); + + assertThat(context.getBean(ImportedConfig.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class)).isNotNull(); + assertThat(context.getBean(ImportedBean.class).name()).isEqualTo("from class"); + } + + @Import(ImportedConfig.class) + interface ConfigImportMarker { + } + + @Configuration + static class InterfaceBasedConfig implements ConfigImportMarker { + } + + @Configuration + @Import(OverridingImportedConfig.class) + static class OverridingConfig implements ConfigImportMarker { + } + + @Configuration + static class OverridingImportedConfig { + @Bean + ImportedBean importedBean() { + return new ImportedBean("from class"); + } + } + + static class ImportedBean { + + private final String name; + + ImportedBean() { + this.name = "imported"; + } + + ImportedBean(String name) { + this.name = name; + } + + String name() { + return name; + } + } + + @Configuration + static class ImportedConfig { + @Bean + ImportedBean importedBean() { + return new ImportedBean(); + } + } @Configuration @Import(SampleImportSelector.class)