Pranav Manglik 1 week ago committed by GitHub
parent
commit
46b8cf1334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 64
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java
  2. 26
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java
  3. 4
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java
  4. 47
      spring-context/src/test/java/org/springframework/context/annotation/TransitiveConfigurationTests.java

64
spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java

@ -1,4 +1,4 @@
/* /*
* Copyright 2002-present the original author or authors. * Copyright 2002-present 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");
@ -24,6 +24,12 @@ import java.util.Set;
import org.jspecify.annotations.Nullable; 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.BeanRegistrar;
import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Location;
import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.Problem;
@ -63,6 +69,8 @@ final class ConfigurationClass {
private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1); private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1);
private final Set<ConfigurationClass> directImports = new LinkedHashSet<>();
private final Set<BeanMethod> beanMethods = new LinkedHashSet<>(); private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
private final Map<String, Class<? extends BeanDefinitionReader>> importedResources = private final Map<String, Class<? extends BeanDefinitionReader>> importedResources =
@ -73,6 +81,8 @@ final class ConfigurationClass {
private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars = private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars =
new LinkedHashMap<>(); new LinkedHashMap<>();
private static final Log logger = LogFactory.getLog(ConfigurationClass.class);
final Set<String> skippedBeanMethods = new HashSet<>(); final Set<String> skippedBeanMethods = new HashSet<>();
@ -200,6 +210,20 @@ final class ConfigurationClass {
return this.importedBy; 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) { void addBeanMethod(BeanMethod method) {
this.beanMethods.add(method); this.beanMethods.add(method);
} }
@ -241,6 +265,44 @@ final class ConfigurationClass {
return this.importBeanDefinitionRegistrars; 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 @SuppressWarnings("NullAway") // Reflection
void validate(ProblemReporter problemReporter) { void validate(ProblemReporter problemReporter) {
Map<String, @Nullable Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); Map<String, @Nullable Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());

26
spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java

@ -113,17 +113,22 @@ class ConfigurationClassBeanDefinitionReader {
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader); this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
} }
/** /**
* Read {@code configurationModel}, registering bean definitions * Read {@code configurationModel}, registering bean definitions
* with the registry based on its contents. * with the registry based on its contents.
*/ */
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator(); TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) { for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
} }
}
// --- ADD THIS TRIGGER BLOCK ---
for (ConfigurationClass configClass : configurationModel) {
configClass.detectTransitiveImports(this.registry);
}
// ------------------------------
}
/** /**
* Read a particular {@link ConfigurationClass}, registering bean definitions * Read a particular {@link ConfigurationClass}, registering bean definitions
@ -220,6 +225,7 @@ class ConfigurationClassBeanDefinitionReader {
ConfigurationClassBeanDefinition beanDef = ConfigurationClassBeanDefinition beanDef =
new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName); new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName);
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource())); 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)? // Has this effectively been overridden before (for example, via XML)?
if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) { if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) {

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

@ -631,7 +631,9 @@ class ConfigurationClassParser {
// process it as an @Configuration class // process it as an @Configuration class
this.importStack.registerImport( this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), filter); ConfigurationClass importedConfigClass = candidate.asConfigClass(configClass);
configClass.addDirectImport(importedConfigClass);
processConfigurationClass(importedConfigClass, filter);
} }
} }
} }

47
spring-context/src/test/java/org/springframework/context/annotation/TransitiveConfigurationTests.java

@ -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;
}
}
}
Loading…
Cancel
Save