Polish "Add support for Neo4j Java Driver 6.0.0"

See gh-47381
This commit is contained in:
Stéphane Nicoll 2025-10-03 15:48:57 +02:00
parent 2542430e5b
commit 6f1bcc4bfa
10 changed files with 155 additions and 64 deletions

View File

@ -1042,6 +1042,13 @@ management:
[[actuator.metrics.supported.neo4j]]
=== Neo4j Metrics
Auto-configuration registers a javadoc:org.neo4j.driver.observation.micrometer.MicrometerObservationProvider[] for the auto-configured javadoc:org.neo4j.driver.Driver[].
[[actuator.metrics.supported.jetty]] [[actuator.metrics.supported.jetty]]
=== Jetty Metrics === Jetty Metrics

View File

@ -35,8 +35,9 @@ dependencies {
optional(project(":core:spring-boot-docker-compose")) optional(project(":core:spring-boot-docker-compose"))
optional(project(":core:spring-boot-testcontainers")) optional(project(":core:spring-boot-testcontainers"))
optional(project(":module:spring-boot-health")) optional(project(":module:spring-boot-health"))
optional("org.testcontainers:neo4j") optional(project(":module:spring-boot-micrometer-observation"))
optional("org.neo4j.driver:neo4j-java-driver-observation-micrometer") optional("org.neo4j.driver:neo4j-java-driver-observation-micrometer")
optional("org.testcontainers:neo4j")
dockerTestImplementation(project(":core:spring-boot-test")) dockerTestImplementation(project(":core:spring-boot-test"))
dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) dockerTestImplementation(project(":test-support:spring-boot-docker-test-support"))
@ -49,6 +50,7 @@ dependencies {
testImplementation(project(":core:spring-boot-test")) testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support")) testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation(testFixtures(project(":core:spring-boot-testcontainers"))) testImplementation(testFixtures(project(":core:spring-boot-testcontainers")))
testImplementation("io.micrometer:micrometer-observation-test")
testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor:reactor-test")
testRuntimeOnly("ch.qos.logback:logback-classic") testRuntimeOnly("ch.qos.logback:logback-classic")

View File

@ -21,12 +21,9 @@ import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.micrometer.observation.ObservationRegistry;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.neo4j.bolt.connection.BoltConnectionProviderFactory;
import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthToken;
import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.AuthTokenManager;
import org.neo4j.driver.AuthTokens; import org.neo4j.driver.AuthTokens;
@ -35,7 +32,6 @@ import org.neo4j.driver.Config.TrustStrategy;
import org.neo4j.driver.Driver; import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.internal.Scheme; import org.neo4j.driver.internal.Scheme;
import org.neo4j.driver.observation.micrometer.MicrometerObservationProvider;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
@ -48,7 +44,6 @@ import org.springframework.boot.neo4j.autoconfigure.Neo4jProperties.Authenticati
import org.springframework.boot.neo4j.autoconfigure.Neo4jProperties.Pool; import org.springframework.boot.neo4j.autoconfigure.Neo4jProperties.Pool;
import org.springframework.boot.neo4j.autoconfigure.Neo4jProperties.Security; import org.springframework.boot.neo4j.autoconfigure.Neo4jProperties.Security;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -67,20 +62,6 @@ import org.springframework.util.StringUtils;
@EnableConfigurationProperties(Neo4jProperties.class) @EnableConfigurationProperties(Neo4jProperties.class)
public final class Neo4jAutoConfiguration { public final class Neo4jAutoConfiguration {
private static final boolean HAS_DRIVER_METRICS;
static {
boolean metricsObservationProviderFound = true;
try {
Class.forName("org.neo4j.driver.observation.micrometer.MicrometerObservationProvider", false,
Neo4jAutoConfiguration.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
metricsObservationProviderFound = false;
}
HAS_DRIVER_METRICS = metricsObservationProviderFound;
}
@Bean @Bean
@ConditionalOnMissingBean(Neo4jConnectionDetails.class) @ConditionalOnMissingBean(Neo4jConnectionDetails.class)
PropertiesNeo4jConnectionDetails neo4jConnectionDetails(Neo4jProperties properties, PropertiesNeo4jConnectionDetails neo4jConnectionDetails(Neo4jProperties properties,
@ -90,12 +71,10 @@ public final class Neo4jAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
Driver neo4jDriver(Neo4jProperties properties, Environment environment, Neo4jConnectionDetails connectionDetails, Driver neo4jDriver(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails,
ObjectProvider<ConfigBuilderCustomizer> configBuilderCustomizers, ObjectProvider<ConfigBuilderCustomizer> configBuilderCustomizers) {
ObjectProvider<ObservationRegistry> observationRegistryProvider) {
Config config = mapDriverConfig(properties, connectionDetails, Config config = mapDriverConfig(properties, connectionDetails,
configBuilderCustomizers.orderedStream().toList(), observationRegistryProvider); configBuilderCustomizers.orderedStream().toList());
AuthTokenManager authTokenManager = connectionDetails.getAuthTokenManager(); AuthTokenManager authTokenManager = connectionDetails.getAuthTokenManager();
if (authTokenManager != null) { if (authTokenManager != null) {
return GraphDatabase.driver(connectionDetails.getUri(), authTokenManager, config); return GraphDatabase.driver(connectionDetails.getUri(), authTokenManager, config);
@ -105,10 +84,9 @@ public final class Neo4jAutoConfiguration {
} }
Config mapDriverConfig(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails, Config mapDriverConfig(Neo4jProperties properties, Neo4jConnectionDetails connectionDetails,
List<ConfigBuilderCustomizer> customizers, List<ConfigBuilderCustomizer> customizers) {
ObjectProvider<ObservationRegistry> observationRegistryProvider) {
Config.ConfigBuilder builder = Config.builder(); Config.ConfigBuilder builder = Config.builder();
configurePoolSettings(builder, properties.getPool(), observationRegistryProvider); configurePoolSettings(builder, properties.getPool());
URI uri = connectionDetails.getUri(); URI uri = connectionDetails.getUri();
String scheme = (uri != null) ? uri.getScheme() : "bolt"; String scheme = (uri != null) ? uri.getScheme() : "bolt";
configureDriverSettings(builder, properties, isSimpleScheme(scheme)); configureDriverSettings(builder, properties, isSimpleScheme(scheme));
@ -118,16 +96,10 @@ public final class Neo4jAutoConfiguration {
private boolean isSimpleScheme(String scheme) { private boolean isSimpleScheme(String scheme) {
String lowerCaseScheme = scheme.toLowerCase(Locale.ENGLISH); String lowerCaseScheme = scheme.toLowerCase(Locale.ENGLISH);
if (!ServiceLoader.load(BoltConnectionProviderFactory.class)
.stream()
.anyMatch((p) -> p.get().supports(lowerCaseScheme))) {
throw new IllegalArgumentException(String.format("'%s' is not a supported scheme.", scheme));
}
return !Scheme.isSecurityScheme(lowerCaseScheme); return !Scheme.isSecurityScheme(lowerCaseScheme);
} }
private void configurePoolSettings(Config.ConfigBuilder builder, Pool pool, private void configurePoolSettings(Config.ConfigBuilder builder, Pool pool) {
ObjectProvider<ObservationRegistry> observationRegistryProvider) {
if (pool.isLogLeakedSessions()) { if (pool.isLogLeakedSessions()) {
builder.withLeakedSessionsLogging(); builder.withLeakedSessionsLogging();
} }
@ -139,11 +111,6 @@ public final class Neo4jAutoConfiguration {
builder.withMaxConnectionLifetime(pool.getMaxConnectionLifetime().toMillis(), TimeUnit.MILLISECONDS); builder.withMaxConnectionLifetime(pool.getMaxConnectionLifetime().toMillis(), TimeUnit.MILLISECONDS);
builder.withConnectionAcquisitionTimeout(pool.getConnectionAcquisitionTimeout().toMillis(), builder.withConnectionAcquisitionTimeout(pool.getConnectionAcquisitionTimeout().toMillis(),
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
observationRegistryProvider.ifAvailable((orp) -> {
if (pool.isMetricsEnabled() && HAS_DRIVER_METRICS) {
builder.withObservationProvider(MicrometerObservationProvider.builder(orp).build());
}
});
} }
private void configureDriverSettings(Config.ConfigBuilder builder, Neo4jProperties properties, private void configureDriverSettings(Config.ConfigBuilder builder, Neo4jProperties properties,

View File

@ -150,11 +150,6 @@ public class Neo4jProperties {
public static class Pool { public static class Pool {
/**
* Whether to enable metrics.
*/
private boolean metricsEnabled = false;
/** /**
* Whether to log leaked sessions. * Whether to log leaked sessions.
*/ */
@ -223,14 +218,6 @@ public class Neo4jProperties {
this.connectionAcquisitionTimeout = connectionAcquisitionTimeout; this.connectionAcquisitionTimeout = connectionAcquisitionTimeout;
} }
public boolean isMetricsEnabled() {
return this.metricsEnabled;
}
public void setMetricsEnabled(boolean metricsEnabled) {
this.metricsEnabled = metricsEnabled;
}
} }
public static class Security { public static class Security {

View File

@ -0,0 +1,49 @@
/*
* Copyright 2012-present 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.neo4j.autoconfigure.observation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.neo4j.driver.Config.ConfigBuilder;
import org.neo4j.driver.observation.micrometer.MicrometerObservationProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.neo4j.autoconfigure.ConfigBuilderCustomizer;
import org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* Auto-configuration for Neo4j observability.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@AutoConfiguration(before = Neo4jAutoConfiguration.class,
afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration")
@ConditionalOnBean(ObservationRegistry.class)
@ConditionalOnClass({ ConfigBuilder.class, MicrometerObservationProvider.class, Observation.class })
public final class Neo4jObservationAutoConfiguration {
@Bean
@ConditionalOnBean(ObservationRegistry.class)
ConfigBuilderCustomizer neo4jObservationCustomizer(ObservationRegistry registry) {
return (builder) -> builder.withObservationProvider(MicrometerObservationProvider.builder(registry).build());
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2012-present 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.
*/
/**
* Auto-configuration for Neo4j observation.
*/
@NullMarked
package org.springframework.boot.neo4j.autoconfigure.observation;
import org.jspecify.annotations.NullMarked;

View File

@ -7,6 +7,14 @@
"description": "Whether to enable Neo4j health check.", "description": "Whether to enable Neo4j health check.",
"defaultValue": true "defaultValue": true
}, },
{
"name": "spring.neo4j.pool.metrics-enabled",
"type": "java.lang.Boolean",
"deprecation": {
"reason": "Use 'management.metrics.enable' to restrict certain metrics.",
"level": "error"
}
},
{ {
"name": "spring.neo4j.uri", "name": "spring.neo4j.uri",
"defaultValue": "bolt://localhost:7687" "defaultValue": "bolt://localhost:7687"

View File

@ -1,2 +1,3 @@
org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration
org.springframework.boot.neo4j.autoconfigure.health.Neo4jHealthContributorAutoConfiguration org.springframework.boot.neo4j.autoconfigure.health.Neo4jHealthContributorAutoConfiguration
org.springframework.boot.neo4j.autoconfigure.observation.Neo4jObservationAutoConfiguration

View File

@ -21,9 +21,7 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Stream;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -34,7 +32,6 @@ import org.neo4j.driver.Config;
import org.neo4j.driver.Config.ConfigBuilder; import org.neo4j.driver.Config.ConfigBuilder;
import org.neo4j.driver.Driver; import org.neo4j.driver.Driver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration.PropertiesNeo4jConnectionDetails; import org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration.PropertiesNeo4jConnectionDetails;
@ -105,7 +102,7 @@ class Neo4jAutoConfigurationTests {
this.contextRunner.withPropertyValues("spring.neo4j.uri=" + invalidScheme + "://localhost:4711") this.contextRunner.withPropertyValues("spring.neo4j.uri=" + invalidScheme + "://localhost:4711")
.run((ctx) -> assertThat(ctx).hasFailed() .run((ctx) -> assertThat(ctx).hasFailed()
.getFailure() .getFailure()
.hasMessageContaining("'%s' is not a supported scheme.", invalidScheme)); .hasMessageContaining("Unsupported scheme: %s", invalidScheme));
} }
@Test @Test
@ -320,13 +317,7 @@ class Neo4jAutoConfigurationTests {
private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) { private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) {
return new Neo4jAutoConfiguration().mapDriverConfig(properties, return new Neo4jAutoConfiguration().mapDriverConfig(properties,
new PropertiesNeo4jConnectionDetails(properties, null), Arrays.asList(customizers), new PropertiesNeo4jConnectionDetails(properties, null), Arrays.asList(customizers));
new ObjectProvider<>() {
@Override
public Stream<ObservationRegistry> stream() {
return Stream.empty();
}
});
} }
} }

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-present 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.neo4j.autoconfigure.observation;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.neo4j.driver.internal.observation.NoopObservationProvider;
import org.neo4j.driver.observation.micrometer.MicrometerObservationProvider;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.neo4j.autoconfigure.Neo4jAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Neo4jObservationAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class Neo4jObservationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(Neo4jObservationAutoConfiguration.class));
@Test
void whenThereIsAnObservationRegistryThenMicrometerObservationProviderIsAdded() {
this.contextRunner.withBean(TestObservationRegistry.class, TestObservationRegistry::create)
.withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class))
.run((context) -> assertThat(context.getBean(Driver.class)).extracting("observationProvider")
.isInstanceOf(MicrometerObservationProvider.class));
}
@Test
void whenThereIsNoObservationRegistryThenConfigBuilderCustomizationBacksOff() {
this.contextRunner.withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class))
.run((context) -> assertThat(context.getBean(Driver.class)).extracting("observationProvider")
.isInstanceOf(NoopObservationProvider.class));
}
}