Move code from spring-boot-actuator to spring-boot-jdbc

This commit is contained in:
Andy Wilkinson
2025-05-12 09:00:55 +01:00
committed by Phillip Webb
parent d6a7aeb298
commit 29b745215e
13 changed files with 25 additions and 22 deletions

View File

@@ -13,14 +13,18 @@ dependencies {
api(project(":spring-boot-project:spring-boot-sql"))
api("org.springframework:spring-jdbc")
compileOnly("com.fasterxml.jackson.core:jackson-annotations")
implementation(project(":spring-boot-project:spring-boot-tx"))
optional(project(":spring-boot-project:spring-boot-actuator"))
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
optional("com.h2database:h2")
optional("com.mchange:c3p0")
optional("com.oracle.database.jdbc:ojdbc11")
optional("com.oracle.database.jdbc:ucp11")
optional("com.zaxxer:HikariCP")
optional("io.micrometer:micrometer-core")
optional("org.apache.commons:commons-dbcp2") {
exclude group: "commons-logging", module: "commons-logging"
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jdbc.actuate.health;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.IncorrectResultSetColumnCountException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link HealthIndicator} that tests the status of a {@link DataSource} and optionally
* runs a test query.
*
* @author Dave Syer
* @author Christian Dupuis
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Arthur Kalimullin
* @since 4.0.0
*/
public class DataSourceHealthIndicator extends AbstractHealthIndicator implements InitializingBean {
private DataSource dataSource;
private String query;
private JdbcTemplate jdbcTemplate;
/**
* Create a new {@link DataSourceHealthIndicator} instance.
*/
public DataSourceHealthIndicator() {
this(null, null);
}
/**
* Create a new {@link DataSourceHealthIndicator} using the specified
* {@link DataSource}.
* @param dataSource the data source
*/
public DataSourceHealthIndicator(DataSource dataSource) {
this(dataSource, null);
}
/**
* Create a new {@link DataSourceHealthIndicator} using the specified
* {@link DataSource} and validation query.
* @param dataSource the data source
* @param query the validation query to use (can be {@code null})
*/
public DataSourceHealthIndicator(DataSource dataSource, String query) {
super("DataSource health check failed");
this.dataSource = dataSource;
this.query = query;
this.jdbcTemplate = (dataSource != null) ? new JdbcTemplate(dataSource) : null;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(this.dataSource != null, "DataSource for DataSourceHealthIndicator must be specified");
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (this.dataSource == null) {
builder.up().withDetail("database", "unknown");
}
else {
doDataSourceHealthCheck(builder);
}
}
private void doDataSourceHealthCheck(Health.Builder builder) {
builder.up().withDetail("database", getProduct());
String validationQuery = this.query;
if (StringUtils.hasText(validationQuery)) {
builder.withDetail("validationQuery", validationQuery);
// Avoid calling getObject as it breaks MySQL on Java 7 and later
List<Object> results = this.jdbcTemplate.query(validationQuery, new SingleColumnRowMapper());
Object result = DataAccessUtils.requiredSingleResult(results);
builder.withDetail("result", result);
}
else {
builder.withDetail("validationQuery", "isValid()");
boolean valid = isConnectionValid();
builder.status((valid) ? Status.UP : Status.DOWN);
}
}
private String getProduct() {
return this.jdbcTemplate.execute((ConnectionCallback<String>) this::getProduct);
}
private String getProduct(Connection connection) throws SQLException {
return connection.getMetaData().getDatabaseProductName();
}
private Boolean isConnectionValid() {
return this.jdbcTemplate.execute((ConnectionCallback<Boolean>) this::isConnectionValid);
}
private Boolean isConnectionValid(Connection connection) throws SQLException {
return connection.isValid(0);
}
/**
* Set the {@link DataSource} to use.
* @param dataSource the data source
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
/**
* Set a specific validation query to use to validate a connection. If none is set, a
* validation based on {@link Connection#isValid(int)} is used.
* @param query the validation query to use
*/
public void setQuery(String query) {
this.query = query;
}
/**
* Return the validation query or {@code null}.
* @return the query
*/
public String getQuery() {
return this.query;
}
/**
* {@link RowMapper} that expects and returns results from a single column.
*/
private static final class SingleColumnRowMapper implements RowMapper<Object> {
@Override
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
ResultSetMetaData metaData = rs.getMetaData();
int columns = metaData.getColumnCount();
if (columns != 1) {
throw new IncorrectResultSetColumnCountException(1, columns);
}
return JdbcUtils.getResultSetValue(rs, 1);
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Health integration for JDBC.
*/
package org.springframework.boot.jdbc.actuate.health;

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jdbc.actuate.metrics;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import javax.sql.DataSource;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.boot.jdbc.metadata.CompositeDataSourcePoolMetadataProvider;
import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadata;
import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
* A {@link MeterBinder} for a {@link DataSource}.
*
* @author Jon Schneider
* @author Phillip Webb
* @since 4.0.0
*/
public class DataSourcePoolMetrics implements MeterBinder {
private final DataSource dataSource;
private final CachingDataSourcePoolMetadataProvider metadataProvider;
private final Iterable<Tag> tags;
public DataSourcePoolMetrics(DataSource dataSource, Collection<DataSourcePoolMetadataProvider> metadataProviders,
String dataSourceName, Iterable<Tag> tags) {
this(dataSource, new CompositeDataSourcePoolMetadataProvider(metadataProviders), dataSourceName, tags);
}
public DataSourcePoolMetrics(DataSource dataSource, DataSourcePoolMetadataProvider metadataProvider, String name,
Iterable<Tag> tags) {
Assert.notNull(dataSource, "'dataSource' must not be null");
Assert.notNull(metadataProvider, "'metadataProvider' must not be null");
this.dataSource = dataSource;
this.metadataProvider = new CachingDataSourcePoolMetadataProvider(metadataProvider);
this.tags = Tags.concat(tags, "name", name);
}
@Override
public void bindTo(MeterRegistry registry) {
if (this.metadataProvider.getDataSourcePoolMetadata(this.dataSource) != null) {
bindPoolMetadata(registry, "active",
"Current number of active connections that have been allocated from the data source.",
DataSourcePoolMetadata::getActive);
bindPoolMetadata(registry, "idle", "Number of established but idle connections.",
DataSourcePoolMetadata::getIdle);
bindPoolMetadata(registry, "max",
"Maximum number of active connections that can be allocated at the same time.",
DataSourcePoolMetadata::getMax);
bindPoolMetadata(registry, "min", "Minimum number of idle connections in the pool.",
DataSourcePoolMetadata::getMin);
}
}
private <N extends Number> void bindPoolMetadata(MeterRegistry registry, String metricName, String description,
Function<DataSourcePoolMetadata, N> function) {
bindDataSource(registry, metricName, description, this.metadataProvider.getValueFunction(function));
}
private <N extends Number> void bindDataSource(MeterRegistry registry, String metricName, String description,
Function<DataSource, N> function) {
if (function.apply(this.dataSource) != null) {
Gauge.builder("jdbc.connections." + metricName, this.dataSource, (m) -> function.apply(m).doubleValue())
.tags(this.tags)
.description(description)
.register(registry);
}
}
private static class CachingDataSourcePoolMetadataProvider implements DataSourcePoolMetadataProvider {
private static final Map<DataSource, DataSourcePoolMetadata> cache = new ConcurrentReferenceHashMap<>();
private final DataSourcePoolMetadataProvider metadataProvider;
CachingDataSourcePoolMetadataProvider(DataSourcePoolMetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
}
<N extends Number> Function<DataSource, N> getValueFunction(Function<DataSourcePoolMetadata, N> function) {
return (dataSource) -> function.apply(getDataSourcePoolMetadata(dataSource));
}
@Override
public DataSourcePoolMetadata getDataSourcePoolMetadata(DataSource dataSource) {
return cache.computeIfAbsent(dataSource,
(key) -> this.metadataProvider.getDataSourcePoolMetadata(dataSource));
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Metrics for JDBC.
*/
package org.springframework.boot.jdbc.actuate.metrics;

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jdbc.actuate.health;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* Tests for {@link DataSourceHealthIndicator}.
*
* @author Dave Syer
* @author Stephane Nicoll
*/
class DataSourceHealthIndicatorTests {
private final DataSourceHealthIndicator indicator = new DataSourceHealthIndicator();
private SingleConnectionDataSource dataSource;
@BeforeEach
void init() {
EmbeddedDatabaseConnection db = EmbeddedDatabaseConnection.HSQLDB;
this.dataSource = new SingleConnectionDataSource(db.getUrl("testdb") + ";shutdown=true", "sa", "", false);
this.dataSource.setDriverClassName(db.getDriverClassName());
}
@AfterEach
void close() {
if (this.dataSource != null) {
this.dataSource.destroy();
}
}
@Test
void healthIndicatorWithDefaultSettings() {
this.indicator.setDataSource(this.dataSource);
Health health = this.indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"),
entry("validationQuery", "isValid()"));
}
@Test
void healthIndicatorWithCustomValidationQuery() {
String customValidationQuery = "SELECT COUNT(*) from FOO";
new JdbcTemplate(this.dataSource).execute("CREATE TABLE FOO (id INTEGER IDENTITY PRIMARY KEY)");
this.indicator.setDataSource(this.dataSource);
this.indicator.setQuery(customValidationQuery);
Health health = this.indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"), entry("result", 0L),
entry("validationQuery", customValidationQuery));
}
@Test
void healthIndicatorWithInvalidValidationQuery() {
String invalidValidationQuery = "SELECT COUNT(*) from BAR";
this.indicator.setDataSource(this.dataSource);
this.indicator.setQuery(invalidValidationQuery);
Health health = this.indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).contains(entry("database", "HSQL Database Engine"),
entry("validationQuery", invalidValidationQuery));
assertThat(health.getDetails()).containsOnlyKeys("database", "error", "validationQuery");
}
@Test
void healthIndicatorCloseConnection() throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
given(connection.getMetaData()).willReturn(this.dataSource.getConnection().getMetaData());
given(dataSource.getConnection()).willReturn(connection);
this.indicator.setDataSource(dataSource);
Health health = this.indicator.health();
assertThat(health.getDetails()).containsKey("database");
then(connection).should(times(2)).close();
}
@Test
void healthIndicatorWithConnectionValidationFailure() throws SQLException {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
given(connection.isValid(0)).willReturn(false);
given(connection.getMetaData()).willReturn(this.dataSource.getConnection().getMetaData());
given(dataSource.getConnection()).willReturn(connection);
this.indicator.setDataSource(dataSource);
Health health = this.indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"),
entry("validationQuery", "isValid()"));
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.jdbc.actuate.metrics;
import java.util.Collection;
import java.util.Collections;
import javax.sql.DataSource;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
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;
/**
* Tests for {@link DataSourcePoolMetrics}.
*
* @author Jon Schneider
* @author Andy Wilkinson
*/
class DataSourcePoolMetricsTests {
@Test
void dataSourceIsInstrumented() {
new ApplicationContextRunner().withUserConfiguration(DataSourceConfig.class, MetricsApp.class)
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class))
.withPropertyValues("spring.datasource.generate-unique-name=true", "metrics.use-global-registry=false")
.run((context) -> {
context.getBean(DataSource.class).getConnection().getMetaData();
context.getBean(MeterRegistry.class).get("jdbc.connections.max").meter();
});
}
@Configuration(proxyBeanMethods = false)
static class MetricsApp {
@Bean
MeterRegistry registry() {
return new SimpleMeterRegistry();
}
}
@Configuration(proxyBeanMethods = false)
static class DataSourceConfig {
DataSourceConfig(DataSource dataSource, Collection<DataSourcePoolMetadataProvider> metadataProviders,
MeterRegistry registry) {
new DataSourcePoolMetrics(dataSource, metadataProviders, "data.source", Collections.emptyList())
.bindTo(registry);
}
}
}