Skip to content

Remove CachedContentServiceCondition to prevent early bean instantiation - #6725

Open
zxcv9203 wants to merge 1 commit into
spring-projects:mainfrom
zxcv9203:remove-cached-content-service-condition
Open

Remove CachedContentServiceCondition to prevent early bean instantiation#6725
zxcv9203 wants to merge 1 commit into
spring-projects:mainfrom
zxcv9203:remove-cached-content-service-condition

Conversation

@zxcv9203

@zxcv9203 zxcv9203 commented Aug 3, 2026

Copy link
Copy Markdown

Bug description

Defining a custom com.google.genai.Client bean whose @Bean method takes a @Value("${spring.ai.google.genai.api-key}") parameter injects the raw placeholder string instead of the property value.

Nothing throws and the application starts normally, so it only surfaces later when Gemini rejects the literal ${spring.ai.google.genai.api-key} as an invalid API key. Reading the same property through Environment in that method works, so the problem is placeholder resolution rather than the property itself.

It reproduces on 1.1.8, 2.0.0 and 2.0.1-SNAPSHOT.

@Configuration(proxyBeanMethods = false)
class GeminiClientConfig {

	@Bean
	Client geminiClient(@Value("${spring.ai.google.genai.api-key}") String apiKey) {
		// apiKey is "${spring.ai.google.genai.api-key}", not the configured key
		return Client.builder().apiKey(apiKey).build();
	}

}

Root cause

The cause is CachedContentServiceCondition#getMatchOutcome calling context.getBeanFactory().getBean(GoogleGenAiChatModel.class).

Bean-method conditions are evaluated in the REGISTER_BEAN phase inside ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry, which runs before PropertySourcesPlaceholderConfigurer has registered its embedded value resolver. That getBean call eagerly instantiates the chat model dependency chain, so the user's Client bean is created there, its @Value parameter goes through AbstractBeanFactory#resolveEmbeddedValue with no resolver present, and the placeholder comes back unchanged.

The eager chain then fails because a later constructor argument is not registered yet, ToolCallingManager on 2.0.x and RetryTemplate on 1.1.x, but the condition swallows that in catch (Exception) and reports a no-match. The poisoned singleton stays in the context, and googleGenAiCachedContentService is never registered in a real application, so the condition never achieves what it was written for.

Stack captured at the user's @Bean method
at com.example.gemini.GeminiClientConfig.geminiClient(GeminiClientConfig.java:17)
...
at o.s.ai.model.google.genai.autoconfigure.chat.CachedContentServiceCondition.getMatchOutcome(CachedContentServiceCondition.java:50)
at o.s.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
at o.s.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:100)
at o.s.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(...)
at o.s.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(...)
at o.s.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(...)

Fix

This PR removes it. What the condition tries to check, whether chatModel.getCachedContentService() is non-null, is runtime information derived from the injected Client instance, so no Condition implementation can answer it without instantiating beans.

The @Bean method already returns @Nullable and handles that case at the right time. Returning null registers a NullBean, which behaves as an absent bean wherever the value is consumed, so getBeansOfType filters it out, ObjectProvider#getIfAvailable() returns null and required injection still fails as before.

Two things change observably. The bean is now actually registered when the chat model exists and spring.ai.google.genai.chat.enable-cached-content is not set to false, which never happened before. The chat model itself is unaffected since it only exposes the service through a getter and never reads the field internally.

And when the service is null, as with a mocked Client without cache surfaces, containsBean and getBeanNamesForType now report the bean while by-name getBean returns null instead of throwing. Value-based lookups behave exactly as they did.

Nothing inside spring-ai injects that bean or references it in a @ConditionalOnBean, so no internal wiring is affected. The bean exists for applications to autowire, which is what the reference documentation shows for programmatic cache management, and that has silently failed until now because the condition meant the bean was never registered.

Testing

./mvnw -Dmaven.build.cache.enabled=false clean package passes with 5,555 tests and no failures, and the google-genai autoconfigure module runs 41 tests clean.

The regression test added here, userDefinedClientBeanReceivesResolvedValuePlaceholders, fails on main with expected: "test-key" but was: "${spring.ai.google.genai.api-key}" and passes with the change. Reverting only the source changes while keeping the tests also makes cachedContentServiceBeanIsCreatedWhenChatModelExists fail on the bean it now asserts, so both assertions do exercise this fix.

./mvnw process-sources leaves the tree clean.

The condition called getBean(GoogleGenAiChatModel.class) during
REGISTER_BEAN condition evaluation, eagerly instantiating the chat
model dependency chain inside the BeanFactoryPostProcessor phase.
A user-defined Client bean created that early receives unresolved
`@Value` placeholders because PropertySourcesPlaceholderConfigurer
has not registered its embedded value resolver yet.

The nullability check the condition attempted is runtime information
and is already handled by the `@Nullable` bean method returning null,
which registers a NullBean that behaves as an absent bean wherever
the value is consumed.

See spring-projects#4447

Signed-off-by: zxcv9203 <zxcv9203@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants