From a4d5800a6cb1c57c1113af0ba563b5c4f0c0005a Mon Sep 17 00:00:00 2001 From: Daeho Kwon Date: Fri, 25 Apr 2025 03:29:00 +0900 Subject: [PATCH] =?UTF-8?q?Support=20@=E2=81=A0Import=20on=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-34805 Closes gh-34820 Signed-off-by: Daeho Kwon --- .../annotation/ConfigurationClassParser.java | 7 ++ .../annotation/ImportSelectorTests.java | 68 ++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) 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 104e2fdd210..b8086aca92f 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 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll + * @author Daeho Kwon * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -549,6 +550,9 @@ class ConfigurationClassParser { *

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 @@ class ConfigurationClassParser { 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 2014c12000b..97410079117 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 @@ import static org.mockito.Mockito.spy; * * @author Phillip Webb * @author Stephane Nicoll + * @author Daeho Kwon */ @SuppressWarnings("resource") public class ImportSelectorTests { @@ -203,6 +204,71 @@ public class ImportSelectorTests { 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)