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 @@ @@ -1,4 +1,4 @@
/*
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -24,6 +24,12 @@ import java.util.Set; @@ -24,6 +24,12 @@ import java.util.Set;
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;
@ -63,6 +69,8 @@ final class ConfigurationClass { @@ -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 =
@ -73,6 +81,8 @@ final class ConfigurationClass { @@ -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<>();
@ -200,6 +210,20 @@ final class ConfigurationClass { @@ -200,6 +210,20 @@ final class ConfigurationClass {
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);
}
@ -241,6 +265,44 @@ final class ConfigurationClass { @@ -241,6 +265,44 @@ final class ConfigurationClass {
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());

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

@ -113,17 +113,22 @@ class ConfigurationClassBeanDefinitionReader { @@ -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
@ -220,6 +225,7 @@ class ConfigurationClassBeanDefinitionReader { @@ -220,6 +225,7 @@ class ConfigurationClassBeanDefinitionReader {
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)) {

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

@ -631,7 +631,9 @@ class ConfigurationClassParser { @@ -631,7 +631,9 @@ class ConfigurationClassParser {
// 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);
}
}
}

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

@ -0,0 +1,47 @@ @@ -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