Ignore prototype DataSource beans for metrics and health

Previously, if a prototype DataSource bean was defined, Actuator's
metrics and health would try to access an instance of it. At best
this was wasteful as the new instance would only be used for metrics
and health and would not be indicative of the app's DataSource usage.
At worst, it could cause a failure in the unusual case of the
prototype bean definition requiring arguments to be supplied using
ObjectProvider.getObject(Object...) or the like.

This commit address the problem by ignoring prototype DataSource
for metrics and health.

Other types of beans for which Actuator provides metrics and health
are similarly affected. They have not be fixed here as the situation
is so unusual. Should another problem arise in the future, it can be
addressed at that time when there's a clear need.

Closes gh-44706
This commit is contained in:
Andy Wilkinson 2025-03-18 18:04:45 +00:00
parent 4dea97141c
commit e237390e66
4 changed files with 77 additions and 5 deletions

View File

@ -89,7 +89,7 @@ public class DataSourceHealthContributorAutoConfiguration implements Initializin
public HealthContributor dbHealthContributor(ConfigurableListableBeanFactory beanFactory,
DataSourceHealthIndicatorProperties dataSourceHealthIndicatorProperties) {
Map<String, DataSource> dataSources = SimpleAutowireCandidateResolver.resolveAutowireCandidates(beanFactory,
DataSource.class);
DataSource.class, false, true);
if (dataSourceHealthIndicatorProperties.isIgnoreRoutingDataSources()) {
Map<String, DataSource> filteredDatasources = dataSources.entrySet()
.stream()

View File

@ -72,9 +72,8 @@ public class DataSourcePoolMetricsAutoConfiguration {
@Bean
DataSourcePoolMetadataMeterBinder dataSourcePoolMetadataMeterBinder(ConfigurableListableBeanFactory beanFactory,
ObjectProvider<DataSourcePoolMetadataProvider> metadataProviders) {
return new DataSourcePoolMetadataMeterBinder(
SimpleAutowireCandidateResolver.resolveAutowireCandidates(beanFactory, DataSource.class),
metadataProviders);
return new DataSourcePoolMetadataMeterBinder(SimpleAutowireCandidateResolver
.resolveAutowireCandidates(beanFactory, DataSource.class, false, true), metadataProviders);
}
static class DataSourcePoolMetadataMeterBinder implements MeterBinder {
@ -141,7 +140,7 @@ public class DataSourcePoolMetricsAutoConfiguration {
@Override
public void bindTo(MeterRegistry registry) {
this.dataSources.stream(ObjectProvider.UNFILTERED).forEach((dataSource) -> {
this.dataSources.stream(ObjectProvider.UNFILTERED, false).forEach((dataSource) -> {
HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class,
HikariDataSource.class);
if (hikariDataSource != null) {

View File

@ -19,12 +19,15 @@ package org.springframework.boot.actuate.autoconfigure.jdbc;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration.RoutingDataSourceHealthContributor;
@ -41,6 +44,7 @@ import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import static org.assertj.core.api.Assertions.assertThat;
@ -213,6 +217,16 @@ class DataSourceHealthContributorAutoConfigurationTests {
});
}
@Test
void prototypeDataSourceIsIgnored() {
this.contextRunner
.withUserConfiguration(EmbeddedDataSourceConfiguration.class, PrototypeDataSourceConfiguration.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(CompositeHealthContributor.class);
assertThat(context.getBeansOfType(DataSourceHealthIndicator.class)).hasSize(1);
});
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
static class DataSourceConfig {
@ -317,4 +331,26 @@ class DataSourceHealthContributorAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class PrototypeDataSourceConfiguration {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
DataSource dataSourcePrototype(String username, String password) {
return createHikariDataSource(username, password);
}
private HikariDataSource createHikariDataSource(String username, String password) {
String url = "jdbc:hsqldb:mem:test-" + UUID.randomUUID();
HikariDataSource hikariDataSource = DataSourceBuilder.create()
.url(url)
.type(HikariDataSource.class)
.username(username)
.password(password)
.build();
return hikariDataSource;
}
}
}

View File

@ -28,6 +28,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
@ -39,6 +40,7 @@ import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.jdbc.datasource.DelegatingDataSource;
@ -222,6 +224,19 @@ class DataSourcePoolMetricsAutoConfigurationTests {
});
}
@Test
void prototypeDataSourceIsIgnored() {
this.contextRunner
.withUserConfiguration(OneHikariDataSourceConfiguration.class, PrototypeDataSourceConfiguration.class)
.run((context) -> {
context.getBean("hikariDataSource", DataSource.class).getConnection();
((DataSource) context.getBean("prototypeDataSource", "", "")).getConnection();
MeterRegistry registry = context.getBean(MeterRegistry.class);
assertThat(registry.get("hikaricp.connections").meter().getId().getTags())
.containsExactly(Tag.of("pool", "hikariDataSource"));
});
}
private static HikariDataSource createHikariDataSource(String poolName) {
String url = "jdbc:hsqldb:mem:test-" + UUID.randomUUID();
HikariDataSource hikariDataSource = DataSourceBuilder.create().url(url).type(HikariDataSource.class).build();
@ -334,6 +349,28 @@ class DataSourcePoolMetricsAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class PrototypeDataSourceConfiguration {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
DataSource prototypeDataSource(String username, String password) {
return createHikariDataSource(username, password);
}
private HikariDataSource createHikariDataSource(String username, String password) {
String url = "jdbc:hsqldb:mem:test-" + UUID.randomUUID();
HikariDataSource hikariDataSource = DataSourceBuilder.create()
.url(url)
.type(HikariDataSource.class)
.username(username)
.password(password)
.build();
return hikariDataSource;
}
}
@Configuration(proxyBeanMethods = false)
static class HikariSealingConfiguration {