Use query-less datasource validation by default

This commit changes DataSourceHealthIndicator to validate the connection
rather than issuing a query to the database. If a custom validation
query is specified, it uses that as before.

Closes gh-17582
This commit is contained in:
Stephane Nicoll 2020-02-18 14:59:06 +01:00
parent c53d4f2bf1
commit 16111f126e
2 changed files with 48 additions and 30 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -28,7 +28,7 @@ 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.jdbc.DatabaseDriver;
import org.springframework.boot.actuate.health.Status;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.IncorrectResultSetColumnCountException;
import org.springframework.jdbc.core.ConnectionCallback;
@ -51,8 +51,6 @@ import org.springframework.util.StringUtils;
*/
public class DataSourceHealthIndicator extends AbstractHealthIndicator implements InitializingBean {
private static final String DEFAULT_QUERY = "SELECT 1";
private DataSource dataSource;
private String query;
@ -104,17 +102,27 @@ public class DataSourceHealthIndicator extends AbstractHealthIndicator implement
}
private void doDataSourceHealthCheck(Health.Builder builder) throws Exception {
String product = getProduct();
builder.up().withDetail("database", product);
String validationQuery = getValidationQuery(product);
try {
// Avoid calling getObject as it breaks MySQL on Java 7
List<Object> results = this.jdbcTemplate.query(validationQuery, new SingleColumnRowMapper());
Object result = DataAccessUtils.requiredSingleResult(results);
builder.withDetail("result", result);
builder.up().withDetail("database", getProduct());
String validationQuery = this.query;
if (StringUtils.hasText(validationQuery)) {
try {
// Avoid calling getObject as it breaks MySQL on Java 7
List<Object> results = this.jdbcTemplate.query(validationQuery, new SingleColumnRowMapper());
Object result = DataAccessUtils.requiredSingleResult(results);
builder.withDetail("result", result);
}
finally {
builder.withDetail("validationQuery", validationQuery);
}
}
finally {
builder.withDetail("validationQuery", validationQuery);
else {
try {
boolean valid = isConnectionValid();
builder.status((valid) ? Status.UP : Status.DOWN);
}
finally {
builder.withDetail("validationQuery", "isValid()");
}
}
}
@ -126,16 +134,12 @@ public class DataSourceHealthIndicator extends AbstractHealthIndicator implement
return connection.getMetaData().getDatabaseProductName();
}
protected String getValidationQuery(String product) {
String query = this.query;
if (!StringUtils.hasText(query)) {
DatabaseDriver specific = DatabaseDriver.fromProductName(product);
query = specific.getValidationQuery();
}
if (!StringUtils.hasText(query)) {
query = DEFAULT_QUERY;
}
return query;
private Boolean isConnectionValid() {
return this.jdbcTemplate.execute((ConnectionCallback<Boolean>) this::isConnectionValid);
}
private Boolean isConnectionValid(Connection connection) throws SQLException {
return connection.isValid(0);
}
/**
@ -149,8 +153,8 @@ public class DataSourceHealthIndicator extends AbstractHealthIndicator implement
/**
* Set a specific validation query to use to validate a connection. If none is set, a
* default validation query is used.
* @param query the query
* validation based on {@link Connection#isValid(int)} is used.
* @param query the validation query to use
*/
public void setQuery(String query) {
this.query = query;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -17,6 +17,7 @@
package org.springframework.boot.actuate.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
@ -26,7 +27,6 @@ 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.DatabaseDriver;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
@ -69,8 +69,8 @@ class DataSourceHealthIndicatorTests {
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("result", 1L),
entry("validationQuery", DatabaseDriver.HSQLDB.getValidationQuery()));
assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"),
entry("validationQuery", "isValid()"));
}
@Test
@ -109,4 +109,18 @@ class DataSourceHealthIndicatorTests {
verify(connection, 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()"));
}
}