Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -24,6 +24,12 @@

import org.jspecify.annotations.Nullable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.parsing.Location;
import org.springframework.beans.factory.parsing.Problem;
Expand Down Expand Up @@ -63,6 +69,8 @@ final class ConfigurationClass {

private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1);

private final Set<ConfigurationClass> directImports = new LinkedHashSet<>();

private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();

private final Map<String, Class<? extends BeanDefinitionReader>> importedResources =
Expand All @@ -73,6 +81,8 @@ final class ConfigurationClass {
private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars =
new LinkedHashMap<>();

private static final Log logger = LogFactory.getLog(ConfigurationClass.class);

final Set<String> skippedBeanMethods = new HashSet<>();


Expand Down Expand Up @@ -200,6 +210,20 @@ Set<ConfigurationClass> getImportedBy() {
return this.importedBy;
}

/**
* Record a configuration class that was explicitly imported by this one.
*/
void addDirectImport(ConfigurationClass importedClass) {
this.directImports.add(importedClass);
}

/**
* Return the configuration classes explicitly imported by this one.
*/
Set<ConfigurationClass> getDirectImports() {
return this.directImports;
}

void addBeanMethod(BeanMethod method) {
this.beanMethods.add(method);
}
Expand Down Expand Up @@ -241,6 +265,44 @@ Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> getImportBeanDefinitionRe
return this.importBeanDefinitionRegistrars;
}

void detectTransitiveImports(BeanDefinitionRegistry registry) {
if (!Boolean.getBoolean("spring.strict.imports")) {
return;
}

if (!(registry instanceof ListableBeanFactory lbf)) {
return;
}

for (BeanMethod method : this.beanMethods) {
// Look at the parameters of the @Bean method
MethodMetadata metadata = method.getMetadata();

// We iterate through all registered beans to find who provides the dependencies
for (String targetBeanName : lbf.getBeanDefinitionNames()) {
BeanDefinition bd = registry.getBeanDefinition(targetBeanName);
String origin = (String) bd.getAttribute("org.springframework.config.origin");

if (origin != null && !isAllowed(origin)) {
// Check if this bean is actually used by our current config class
// For this proof of concept, we'll trigger if ANY transitive bean
// exists in the context that isn't explicitly imported.
throw new org.springframework.beans.factory.BeanDefinitionStoreException(
String.format("Strict import violation: @Configuration [%s] detected transitive bean [%s] from source [%s].",
this.metadata.getClassName(), targetBeanName, origin));
}
}
}
}

private boolean isAllowed(String origin) {
if (origin.equals(this.metadata.getClassName())) return true;
for (ConfigurationClass dc : this.directImports) {
if (dc.getMetadata().getClassName().equals(origin)) return true;
}
return false;
}

@SuppressWarnings("NullAway") // Reflection
void validate(ProblemReporter problemReporter) {
Map<String, @Nullable Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,22 @@ class ConfigurationClassBeanDefinitionReader {
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}


/**
* Read {@code configurationModel}, registering bean definitions
* with the registry based on its contents.
*/
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
* Read {@code configurationModel}, registering bean definitions
* with the registry based on its contents.
*/
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}

// --- ADD THIS TRIGGER BLOCK ---
for (ConfigurationClass configClass : configurationModel) {
configClass.detectTransitiveImports(this.registry);
}
// ------------------------------
}

/**
* Read a particular {@link ConfigurationClass}, registering bean definitions
Expand Down Expand Up @@ -220,6 +225,7 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClassBeanDefinition beanDef =
new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName);
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
beanDef.setAttribute("org.springframework.config.origin", configClass.getMetadata().getClassName());

// Has this effectively been overridden before (for example, via XML)?
if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,9 @@ else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), filter);
ConfigurationClass importedConfigClass = candidate.asConfigClass(configClass);
configClass.addDirectImport(importedConfigClass);
processConfigurationClass(importedConfigClass, filter);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.springframework.context.annotation;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import static org.junit.jupiter.api.Assertions.assertThrows;

class TransitiveConfigurationTests {

@Test
void transitiveBeanUsageShouldFailInStrictMode() {
System.setProperty("spring.strict.imports", "true");
try {
assertThrows(BeanDefinitionStoreException.class, () -> {
new AnnotationConfigApplicationContext(ConfigA.class);
});
} finally {
System.clearProperty("spring.strict.imports");
}
}

@Configuration
@Import(ConfigB.class)
static class ConfigA {
@Bean
public String beanA(Integer beanC) {
return "A depends on " + beanC;
}
}

@Configuration
@Import(ConfigC.class)
static class ConfigB {
}

@Configuration
static class ConfigC {
@Bean
public Integer beanC() {
return 42;
}
}
}