© 2020 Neo4j, Inc.,
License: Creative Commons 4.0
This is the manual for the Spring Boot Starter for Neo4j’s Java Driver (a.k.a. the Bolt Driver).
Who should read this?
This manual is written for:
-
engineers setting up a Neo4j connection to a single Neo4j instance or a Neo4j cluster over the wire, without the need for object mapping
-
engineers who want to use Neo4j-OGM inside Spring Boot but needs access to the configuration options of the underlying Bolt driver.
1. Intro
1.1. What’s in the box?
The Neo4j Java Driver Spring Boot starter provides both a Spring Boot autoconfigure
and a starter
module.
The autoconfigure
module adds a single instance of org.neo4j.driver.Driver
as a bean to the Spring Context if there is none
and registers health- and metric endpoints for all Driver-beans if the necessary Spring infrastructure is in place.
It does not bring any mapping facilities in addition the available functions of the driver nor Spring Data integration. Have a look at Spring Data Neo4j⚡️RX for reactive Spring Data Neo4j repositories.
The driver instance is a long living object that provides short living sessions as needed. The instance does not need to be closed manually, that is done automatically when the application shuts down. There is no need for external connection pooling. This is done already in the driver and can be configured via properties. Please have a look at the chapter {driver_manual_base}/sessions-transactions/[Sessions and transactions]. |
1.2. Do I need the Neo4j Java Driver Spring Boot starter?
We recommend this starter for all new Spring Boot applications that want to use our new generation of drivers ("4.0"). The next generation of drivers is compatible with both 4.0.x and 3.5.x community and enterprise databases. The starter takes away all the boilerplate code of configuring an instance of the driver and does support externalized configuration for all aspects of the driver.
-
You want to work directly with a 4.0 driver? The starter is for you.
-
You want Spring Data Neo4j⚡️RX repositories? The automatic configuration for SDN-RX is dependent on this starter, so it is already there and you would use exactly the same as described in this manual to configure your connection
-
You have the previous generation of Spring Data Neo4j or Neo4j-OGM? Use the 4.0.x line of this starter with Spring Boot 2.2.x, Spring Data Neo4j 5.2.x and Neo4j-OGM 3.2.x. Use the 1.7.5.x line of this starter with Spring Boot 2.1.x, Spring Data Neo4j 5.1.x and Neo4j-OGM 3.1.x. Spring Boot recognizes the driver bean created by this starter and configures Neo4j-OGM accordingly. We also have an example for that.. For reference see the Neo4j-OGM compatibility matrix.
1.3. Does it work with the 1.7 series of the driver?
The 1.7.x line of the starter is for the 1.7.x line of the driver, the 4.0.x line for the 4.0..x of the driver.
1.4. Does it work with the embedded database?
If you enable the Bolt connector on the embedded instance, it does.
However, it does not and will not startup an embedded instance for you.
There are two main reasons for this decisions:
First, we think that booting up a database from the connection layer (which the driver belongs to) or - even worse - from an object mapping layer - is wrong and violates the solid responsibility principle, thus, leading to all kinds of architectural problems. Furthermore, Neo4j embedded is a full database engine and comes with a lot of dependencies and also in several versions and editions. Catering for all combinations and configuration possible will lead up to hard to maintain dependency management issues and also, duplication of configuration APIs. |
There are two examples in which this starter works with an embedded database:
-
Section 3.6 shows a couple of ways to add a
@Bean
of typeorg.neo4j.harness.Neo4j
(Neo4j 4.0) ororg.neo4j.harness.ServerControls
(Neo4j 3.5 and prior) to the application context. Those types represent the Neo4j test harness and provide an embedded instance, accessible via Bolt. The Bolt connector is always enabled with the test harness.
1.5. What’s with the long name?
neo4j-java-driver-spring-boot-starter
is quite a long name for a module, we get that.
However, it follows the official Spring Boot convention described here.
As a rule of thumb, you should name a combined module after the starter. For example, assume that you are creating a starter for "acme" and that you name the auto-configure module acme-spring-boot-autoconfigure and the starter acme-spring-boot-starter. If you only have one module that combines the two, name it acme-spring-boot-starter.
Our "acme" module is the Neo4j Java Driver, project name neo4j-java-driver
and things add up from there, that’s all.
2. Getting started
As with any other Spring Boot starter, the only thing you have to do is to include the starter module via your dependency management.
If you don’t configure anything, than the starter assumes bolt://localhost:7687
as Neo4j URI and a server that has disabled authentication.
If only a single URI is provided, than the configuration tries to use that.
Otherwise, it passes all URIs to the Java driver which in turn uses the first one that is a reachable bolt+routing
instance.
The automatic configuration will fail fast if the driver cannot connect to a single Neo4j database or to a routing server.
The Neo4j driver supports three different programming models:
-
Blocking database access (much like standard JDBC)
-
Asynchronous programming based on JDKs completable futures and related infrastructure
-
Reactive programming based on Reactive Streams
Those are all included in the same binary. The reactive programming model however requires a 4.0 Neo4j server on the database side and reactive Spring on the other hand. To make the following intro as accessible as possible, we only display the blocking database access. Have a look at the examples directory for a reactive web application example.
2.1. Preparing the database
For this example, we stay within the movie graph, as it comes for free with every Neo4j instance.
If you don’t have a running database but Docker installed, please run:
docker run --publish=7474:7474 --publish=7687:7687 neo4j:4.3.6
You know can access http://localhost:7474.
At first visit, you have to change your password. We chose secret
in the examples.
Note the command ready to run in the prompt.
Execute it to fill your database with some test data.
2.2. Create a new Spring Boot project
The easiest way to setup a Spring Boot project is start.spring.io (which is integrated in the major IDEs as well, in case you don’t want to use the website).
Select the "Spring Web Starter" to get all the dependencies needed for creating a Spring based web application. The Spring Initializr will take care of creating a valid project structure for you, with all the files and settings in place for the selected build tool.
Don’t choose Spring Data Neo4j here, as it will get you the previous generation of Spring Data Neo4j including OGM and additional abstraction over the driver. |
2.2.1. Maven
You can issue a CURL request against the Spring Initializer to create a basic Maven project:
curl https://start.spring.io/starter.tgz \
-d dependencies=web,actuator \
-d bootVersion=2.3.12.RELEASE \
-d baseDir=Neo4jSpringBootExample \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -
This will create a new folder Neo4jSpringBootExample
.
As this starter is not yet on the initializer, you’ll have to add the following dependency manually to your pom.xml
:
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver-spring-boot-starter</artifactId>
<version>4.3.6.0</version>
</dependency>
You would also add the dependency manually in case of an existing project.
2.2.2. Gradle
The idea is the same, just generate a Gradle project:
curl https://start.spring.io/starter.tgz \
-d dependencies=web,actuator \
-d type=gradle-project \
-d bootVersion=2.3.12.RELEASE \
-d baseDir=Neo4jSpringBootExampleGradle \
-d name=Neo4j%20SpringBoot%20Example | tar -xzvf -
The dependency for Gradle looks like this and must be added to build.gradle
:
dependencies {
compile 'org.neo4j.driver:neo4j-java-driver-spring-boot-starter:4.3.6.0'
}
You would also add the dependency manually in case of an existing project.
2.2.3. Configuration
Now open any of those projects in your favorite IDE.
Find application.properties
and configure your Neo4j credentials:
org.neo4j.driver.uri=bolt://localhost:7687
org.neo4j.driver.authentication.username=neo4j
org.neo4j.driver.authentication.password=secret
The URI is required. Autoconfiguration will not be triggered without it. This is to avoid clashes with the Spring Boot SDN/OGM starter. The SDN/OGM starter defaults to localhost, but backs off when it detects a driver bean. If this starter here would default to localhost as well, a driver bean would always be provided and SDN/OGM would back off even when a user configured a different URI. |
This is the bare minimum of what you need to connect to a Neo4j instance.
Refer to the list of configuration properties for all options this driver supports.
While the above configuration is presented in the easiest format (an application.properties
file),
you are of course free to use any other declarative way to define properties in Spring Boot.
Please checkout the chapter Externalized Configuration.
It is not necessary to add any programmatically configuration of the driver when you use this starter. While it may work, we strongly discourage and don’t support additional, pragmatical configuration of the Neo4j driver when using this starter. |
2.2.4. Example CRUD Controller
Add the following @RESTController
to your application:
package com.example.demo;
import java.util.List;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MoviesController {
private final Driver driver; (1)
public MoviesController(Driver driver) { (2)
this.driver = driver;
}
@GetMapping(path = "/movies", produces = MediaType.APPLICATION_JSON_VALUE) (3)
public List<String> getMovieTitles() {
try (Session session = driver.session()) { (4)
return session.run("MATCH (m:Movie) RETURN m ORDER BY m.name ASC")
.list(r -> r.get("m").asNode().get("title").asString());
}
}
}
1 | An instance field to hold the driver |
2 | The driver is injected via constructor injection |
3 | A mapping to the url /movies |
4 | Using the driver to get a short lived session and issue a query on it |
If you generated your application via the commands given above, you can now run the class Neo4jSpringBootExampleApplication
and after a short while, you can access http://localhost:8080/movies.
2.3. Logging
The Neo4j Spring Boot starter uses a small shim to integrate the driver with Springs JCL abstraction.
Thus, all logging configuration can be done via Spring Boot’s application.properties
.
Important names used for logging are:
logging.level.org.neo4j.driver.GraphDatabase = debug
logging.level.org.neo4j.driver.Driver = debug
If you ever have the need to debug outgoing and incoming Bolt messages, use those two names:
logging.level.org.neo4j.driver.OutboundMessageHandler = debug
logging.level.org.neo4j.driver.InboundMessageDispatcher = debug
The prefix org.neo4j.driver is specific to the Drivers integration with Spring Boot.
|
2.4. Production-ready features
The Neo4j Spring Boot starter hooks into Spring Boot’s Production-ready features (or the so called Spring Boot Actuator). This happens automatically when you add the Spring Boot Actuator Starter like this:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
or with Gradle:
dependencies {
compile 'org.springframework.boot:spring-boot-starter-actuator'
}
We support both the health- and metrics-actuator.
2.5. Health information
Both reactive and imperative health checks are available, with the reactive health checks having precedence when Project Reactor is detected.
When calling the health endpoint /actuator/health
and the driver can reach a Neo4j instance, an unauthenticated user will see
{
"status": "UP"
}
and an authenticated user will see
{
"status": "UP",
"details": {
"neo4j": {
"status": "UP",
"details": {
"server": "Neo4j/4.0.0@localhost(127.0.0.1):7687",
"database": "neo4j"
}
}
}
}
In case no instance is reachable, the status will be DOWN
and the details carry the error message.
To disable the Neo4j health indicator, use the standard Spring Boot property management.health.neo4j.enabled
with a value of false
.
2.6. Driver metrics
neo4j-java-driver-spring-boot-starter
comes with support for Micrometer metrics out of the box.
It detects Micrometer on the classpath and binds the metrics of all instances of org.neo4j.driver.Driver
, that have enabled their metrics, to a micrometer registry.
To enable metrics for the driver instance provided by this starter, set org.neo4j.driver.config.metrics-enabled
to true.
The following metrics are exposes
-
neo4j.driver.connections.inUse
(Gauge) -
neo4j.driver.connections.timedOutToAcquire
(Counter) -
neo4j.driver.connections.closed
(Counter) -
neo4j.driver.connections.failedToCreate
(Counter) -
neo4j.driver.connections.created
(Counter) -
neo4j.driver.connections.idle
(Gauge) -
neo4j.driver.connections.acquired
(Counter)
All metrics will have the tags name
(the bean of the driver they belong to)
and poolId
(the id of the connection pool, that contributed to the corresponding counter or gauge).
3. Examples
3.1. Introduction
We provide several examples how to use this starter. The reactive demo can only be used with Neo4j version 4.
Each example comes with a dedicated readme, that guides you through it.
The examples use the "Movie" data set, that you can install into your instance by going to the Neo4j Browser.
Enter the command :play movies
and follow the instructions.
The examples expect an instance with the username neo4j
and the password secret
.
All examples use Maven, however they work completely the same with Gradle.
Also, all examples follow Spring Boot conventions, especially: Use the dependency management and starters, don’t provide dependencies for things that are available as a starter. Use version properties for overwriting any managed version instead of an explicit version.
The examples all have the default main application class, that looks like this:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SimpleApplication {
public static void main(String[] args) {
SpringApplication.run(SimpleApplication.class, args);
}
}
3.1.1. Support for Visual Studio Remote Containers
To make playing with the examples a bit easier, we have support for VS Code Remote Development. Please follow the instructions at the link above for setting up your Visual Studio Code environment.
You can open web
, reactive-web
and simple
in Visual Studio Code and let it build the container.
The development container will contain a Neo4j 4.0 database as well as the example project.
The database can be reached under http://localhost:7474 with the username / password combination neo4j/secret
.
The example than can be build and run via Visual Studios support for Spring Boot, which is automatically added to the development container.
The web examples are reachable on http://localhost:8080 from both outside and inside Visual Studio Code. Any changes to the code will be automatically reloaded and available.
3.2. Collecting metrics
This refers only to Driver metrics, not to the Neo4j server metrics. |
neo4j-java-driver-spring-boot-starter
comes with support for Micrometer metrics out of the box.
It detects Micrometer on the classpath and binds the metrics of all instances of ` org.neo4j.driver.Driver`, that have enabled their metrics, to a micrometer registry.
Add org.springframework.boot:spring-boot-starter-actuator
to your Spring Boot application, to include Micrometer.
To enable metrics for the driver instance provided by this starter, set org.neo4j.driver.config.metrics-enabled
to true then.
You’ll find this in the [Reactive web example]. Spring Boot doesn’t expose the metrics endpoint over http by default, if you need this, your complete configuration looks like this:
# Enable metrics for the driver instance provided by this starter
org.neo4j.driver.config.metrics-enabled=true
# Expose metrics in addition to info and health over http
management.endpoints.web.exposure.include=info,health,metrics
The metrics can be accessed like any other metrics by their name. The following shows a CURL request retrieving the amount of acquired connections:
curl localhost:8080/actuator/metrics/neo4j.driver.connections.acquired|jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 287 100 287 0 0 32773 0 --:--:-- --:--:-- --:--:-- 35875
{
"name": "neo4j.driver.connections.acquired",
"description": "The amount of connections that have been acquired.",
"baseUnit": "connections",
"measurements": [
{
"statistic": "COUNT",
"value": 102
}
],
"availableTags": [
{
"tag": "poolId",
"values": [
"localhost:7687"
]
},
{
"tag": "name",
"values": [
"driver"
]
}
]
}
3.3. Web
This example shows the usage of the Neo4j Java driver in a standard Spring Web-MVC application in an imperative (blocking) way.
Find the source code under examples/web.
3.4. Reactive Web
This example uses the neo4j://
protocol, which by defaults uses routing.
Apart from that, the configuration is the same as with the simple example.
The driver is however used as a source for a reactive Neo4j session, which in turn produces results for a reactive Spring Rest Controller:
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.neo4j.driver.AccessMode;
import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.reactive.RxSession;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DisplayMoviesController {
private final Driver driver;
public DisplayMoviesController(Driver driver) {
this.driver = driver;
}
@GetMapping(path = "/movies", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getMovieTitles() {
SessionConfig sessionConfig = SessionConfig.builder().withDefaultAccessMode(AccessMode.READ).build();
return Flux.usingWhen(
Mono.fromSupplier(() -> driver.rxSession(sessionConfig)),
s -> Flux.from(s.run("MATCH (m:Movie) RETURN m ORDER BY m.name ASC").records()),
RxSession::close
).map(r -> r.get("m").asNode().get("title").asString());
}
}
3.5. Using an embedded database with the Driver starter
This example demonstrates a couple things:
-
How to provide an instance of the Neo4j
DatabaseManagementService
as a managed bean into a Spring context. -
How to keep the configuration for the driver and the embedded instance in sync.
-
This starter can connect against an embedded instance with the Bolt connector enabled.
While the test harness example focuses on having an embedded instance through the harness in your context, this example focuses on having the embedded API for whatever reasons in your context.
One of those reasons might be that you want to be able to use the GraphDatabaseService
to execute Cypher without Bolt involved or traverse nodes manually.
Here is one made up example that uses the GraphDatabaseService
to find all nodes with a label Movie
:
GraphDatabaseService
from a servicetry(var tx = databaseManagementService.database("neo4j").beginTx()) {
return tx.findNodes(Label.label("Movie"))
.stream()
.map(n -> n.getProperty("title"))
.map(String.class::cast)
.collect(Collectors.toList());
}
The MovieService
in the sources of this example however is exactly the same as the one in the other examples.
It is not aware against what kind of server it is connected and just gets a Driver Session
with var session = driver.session()
.
3.5.1. Provide a managed, embedded DatabaseManagementService
and keep it in sync with the driver.
First, provide the necessary dependencies:
<dependencies>
<!-- Embbedded instance -->
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>4.3.6</version>
</dependency>
<!-- Necessary bolt server -->
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-bolt</artifactId>
<version>4.3.6</version>
</dependency>
</dependencies>
Depending on your setup, you might have to exclude the following artifact from both dependencies above: org.slf4j:slf4j-nop
,
as it clashes with Spring Boots default logger.
The enterprise edition lives under different coordinates
(com.neo4j:neo4j-enterprise
, which are not available in Maven central but only to customers).
Configuration is done with the following components:
import java.io.File;
import java.util.Optional;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "org.neo4j.database-management-service") (1)
public class DatabaseManagementServiceProperties {
/**
* The default home directory. If not configured, a random, temporary directory is used.
* If you want to keep your data around, please set it to a writeable path in your configuration.
*/
private Optional<File> homeDirectory = Optional.empty(); (2)
public Optional<File> getHomeDirectory() {
return homeDirectory;
}
public void setHomeDirectory(Optional<File> homeDirectory) {
this.homeDirectory = homeDirectory;
}
}
1 | Make this class a component containing configurational properties under the prefix org.neo4j.database-management-service .
This prefix is the canonical form. Camel- and Snakecases (databaseManagementService respectivly DATABASE_MANAGEMENT_SERVICE
work as well.
These properties can come from application.properties or application.yml as well as from the environment and of course,
config servers. |
2 | This configuration is needed for the embedded instance. The embedded instance needs to store its data somewhere. |
With a properties class like this, you can have all the relevant configuration properties in one place, instead of
manually juggling with @Value
and related.
import static java.lang.System.Logger.Level.*;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.System.Logger;
import java.net.URI;
import java.nio.file.Files;
import org.neo4j.configuration.connectors.BoltConnector;
import org.neo4j.configuration.helpers.SocketAddress;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder;
import org.neo4j.driver.springframework.boot.autoconfigure.Neo4jDriverProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;
@Configuration(proxyBeanMethods = false) (1)
@EnableConfigurationProperties(DatabaseManagementServiceProperties.class) (2)
public class Neo4jConfig {
private static final Logger LOGGER = System.getLogger(Neo4jConfig.class.getName());
@Bean (3)
@Conditional(OnDriverCouldConnectToEmbedded.class) (4)
DatabaseManagementService databaseManagementService(
Neo4jDriverProperties driverProperties,
DatabaseManagementServiceProperties serviceProperties
) {
(5)
var homeDirectory = serviceProperties.getHomeDirectory().orElseGet(() -> {
try {
return Files.createTempDirectory("neo4j").toFile();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
LOGGER.log(
INFO,
"Configuring {0} as home directory for the embedded instance.",
homeDirectory
);
var driverPort = driverProperties.getUri().getPort();
var address = new SocketAddress("localhost", driverPort); (6)
return new DatabaseManagementServiceBuilder(homeDirectory)
.setConfig(BoltConnector.enabled, true)
.setConfig(BoltConnector.listen_address, address)
.build();
}
static class OnDriverCouldConnectToEmbedded implements Condition { (7)
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// @formatter:off
boolean outcome = false;
URI configuredBoltUri = context.getEnvironment().getProperty("org.neo4j.driver.uri", URI.class);
if (configuredBoltUri == null) {
LOGGER.log(WARNING, "Driver is not configured, skipping embedded instance as well.");
} else if (!"bolt".equalsIgnoreCase(configuredBoltUri.getScheme())) {
LOGGER.log(WARNING, "Driver configured to use `{0}`, but embedded only supports bolt. Not configuring embedded.", configuredBoltUri);
} else if (!"localhost".equalsIgnoreCase(configuredBoltUri.getHost())) {
LOGGER.log(WARNING, "Driver configured to connect to `{0}`, not a local instance. Not configuring embedded.", configuredBoltUri.getHost());
} else {
outcome = true;
}
// @formatter:on
return outcome;
}
}
}
1 | Mark this class as a configurational bean. We can make the startup of the Spring context a bit fast, as we don’t need to proxy the methods. |
2 | Define the class that represents the configuration properties. |
3 | This marks the returned value as Spring bean for the context |
4 | We only fire up the embedded instance in some conditions. This is optional, but makes perfect sense in a setup where both the embedded instance and the driver should be available. |
5 | We check if someone had configured the home directory for the instance. If so, we use that, otherwise we default to something random. |
6 | Here, we use the same port that has been given to the driver for the port the embedded bolt listens to.
Choose any other port you want (or a random free one), but keep in mind,
that you have to reconfigure the driver as well then (through org.neo4j.driver.uri ). |
7 | This is a Condition to be used with @Conditional .
It makes sure in 4. that we bring up an embedded instance only when the driver actually targets something on localhost via the bolt protocol.
This later step is completely optional, but keeps you from funny surprise. |
3.5.2. Using the driver against the embedded instance
To the outside world, nothing indicates that the service runs against embedded:
GraphDatabaseService
from a serviceimport java.util.List;
import java.util.stream.Collectors;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MoviesService {
private final Driver driver;
public MoviesService(Driver driver) {
this.driver = driver;
}
@Autowired
DatabaseManagementService databaseManagementService;
public List<String> getMovieTitles() {
try (var session = driver.session()) {
return session.run("MATCH (m:Movie) RETURN m ORDER BY m.name ASC").stream()
.map(r -> r.get("m").asNode())
.map(n -> n.get("title").asString())
.collect(Collectors.toList());
}
}
}
You can of course inject the DatabaseManagementService
or - if you provide those as beans as well - dedicated
GraphDatabaseService
-instances for the databases you want to work with.
Let’s walk through a test setup. The tests prepares the data directly against the embedded instance and than tests the service:
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.util.SocketUtils;
@SpringBootTest (1)
class MoviesServiceTest {
@DynamicPropertySource (2)
static void neo4jProperties(DynamicPropertyRegistry registry) {
// Configure a free port
registry.add("org.neo4j.driver.uri", () -> "bolt://localhost:" + SocketUtils.findAvailableTcpPort()); (3)
// You could also point the embedded instance to some pre-seeded directory (4)
// registry.add(
// "org.neo4j.database-management-service.home-directory",
// () -> "/Users/msimons/tmp/preseededNeo4j"
// );
}
@BeforeAll (5)
static void prepareTestData(
@Autowired DatabaseManagementService databaseManagementService
) {
try (var tx = databaseManagementService.database("neo4j").beginTx()) {
tx.execute(
"CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n"
+ "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n"
+ "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n");
tx.commit();
}
}
@Test
void shouldRetrieveMovieTitles(@Autowired MoviesService moviesService) { (6)
assertThat(moviesService.getMovieTitles())
.hasSize(3)
.contains("The Matrix");
}
}
1 | Make it a standard, full blown @SpringBootTest , applying the same configuration found from walking the packages
starting at the @SpringBootApplication . |
2 | This brings in dynamic test properties in the registry .
It is available since Spring Boot 2.2.6. |
3 | We use SocketUtils to find a random free port.
We give that random free port to the driver URI which is in turn used by the embedded config to configure the published Bolt port.
This is necessary to not clash with any running databases on the test machine. |
4 | Uncommenting the next line gives you the opportunity to point the embedded instance to a directory containing pre-seeded data during test for example. |
5 | Springs JUnit 5 integration allows beans to be injected in all phases of a JUnit 5 test.
Here we use the @BeforeAll phase to access the embedded database to create test data. |
6 | We want to test the service shown in MovieService.java, which works with the driver. |
3.6. Testing against the Neo4j harness
We provide a special module that brings in the community edition of the Neo4j harness and some additional utilities that hopefully make your life easier.
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver-test-harness-spring-boot-autoconfigure</artifactId>
<version>4.3.6.0</version>
<scope>test</scope>
</dependency>
If you need the enterprise edition, bring in the module like this:
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver-test-harness-spring-boot-autoconfigure</artifactId>
<version>4.3.6.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.neo4j.test</groupId>
<artifactId>neo4j-harness-enterprise</artifactId>
<!-- See below regarding Neo4j version -->
<version>3.5.15</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
The disadvantage: It brings a ton of dependencies. The advantage: It starts very fast. If you don’t want to have the dependencies and can live with a slower start, we recommend Testcontainers.
They are easy to use and can be configured as shown in option 1 below as well.
3.6.1. Neo4j 3.5 or Neo4j 4.0?
Neo4j 3.5 and 4.0 have different requirements. Neo4j 4.0 requires at least JDK 11. We understand that not all of you are ready to go beyond JDK 8 (but you should). Therefore we decided to build the Test harness support by default against Neo4j 3.5 and JDK 8.
To use Neo4j 4.0 as in the following examples, please add this to your build file
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.neo4j.test</groupId>
<artifactId>neo4j-harness</artifactId>
<version>4.3.6</version>
</dependency>
</dependencies>
</dependencyManagement>
Be aware that the type of the 4.0 test harness is org.neo4j.harness.Neo4j
.
The following examples use 4.0 (org.neo4j.harness.Neo4j
) but are applicable to ServerControlls
, too.
3.6.2. Starting up the Neo4j harness and making Spring aware of it
There many different options. The main question is always: How to make Spring Boot aware that it should use different configuration properties?
Option 1: Add the embedded server as a Spring bean (recommended approach)
The recommended approach is shown in MoviesServiceAlt1Test
:
@SpringBootTest
class MoviesServiceAlt1Test {
@TestConfiguration (1)
static class TestHarnessConfig {
@Bean (2)
public Neo4j neo4j() {
return Neo4jBuilders.newInProcessBuilder()
.withDisabledServer() // No need for http
.withFixture(""
+ "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n"
+ "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n"
+ "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n"
)
.build();
// For enterprise use
// return com.neo4j.harness.EnterpriseNeo4jBuilders.newInProcessBuilder()
// .newInProcessBuilder()
// .build();
}
}
@Test
void testSomethingWithTheDriver(@Autowired Driver driver) {
}
}
1 | This is a test configuration only applicable for this test |
2 | This turns the embedded instance into a Spring Bean, bound to Springs lifecycle |
The autoconfiguration module for the test harness makes the starter aware of the harness and reconfigures the driver to use it. This would be the recommended way of doing things.
Option 2: Use the provided harness instance
MoviesServiceAlt2Test.java
demonstrates the fully automatic configuration of test harness and driver:
@SpringBootTest
public class MoviesServiceAlt2Test {
@Autowired
private MoviesService moviesService;
@BeforeEach
void prepareDatabase(@Autowired Neo4j neo4j) { (1)
try(Transaction transaction = neo4j.defaultDatabaseService().beginTx()) {
transaction.execute("MATCH (n) DETACH DELETE n");
transaction.execute(""
+ "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n"
+ "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n"
+ "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n"
);
transaction.commit();
}
}
@Test
void testSomethingWithTheDriver(@Autowired Driver driver) {
}
}
1 | As you don’t have access to the builder, you have to provide your fixtures through the embedded database service. |
This may come in handy in some scenarios, but generally, using the builder API as shown above is preferable. On the plus side: The automatic configuration of the harness takes care of disabling the embedded webserver (for Neo4j 4.0+).
Option 3: Start Neo4j outside Spring and apply its URL to configuration
Here we start the embedded instance from the JUnit 5 context and
than use an org.springframework.context.ApplicationContextInitializer
to apply TestPropertyValues
to the Spring environment.
You don’t actually need neo4j-java-driver-test-harness-spring-boot-autoconfigure for this solution. It’s enough to have the
Test harness - either 3.5.x or 4.0.x or Community or enterprise edition on the classpath.
If you have the test harness autoconfiguration support on the classpath, you have to explicitly disable it.
|
@SpringBootTest
@EnableAutoConfiguration(exclude = { Neo4jTestHarnessAutoConfiguration.class }) (1)
@ContextConfiguration(initializers = { MoviesServiceAlt3Test.Initializer.class })
class MoviesServiceAlt3Test {
private static Neo4j embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() { (2)
embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer() (3)
.withFixture(""
+ "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n"
+ "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n"
+ "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n"
)
.build();
}
@AfterAll
static void closeNeo4j() { (4)
embeddedDatabaseServer.close();
}
(5)
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"org.neo4j.driver.uri=" + embeddedDatabaseServer.boltURI().toString(),
"org.neo4j.driver.authentication.password="
).applyTo(configurableApplicationContext.getEnvironment());
}
}
@Test
void testSomethingWithTheDriver(@Autowired Driver driver) {
}
}
1 | Disable the autoconfiguration (only needed if you have neo4j-java-driver-test-harness-spring-boot-autoconfigure on the classpath) |
2 | Use a JUnit BeforeAll to boot Neo4j |
3 | The driver uses only the Bolt port, not the http port, so we don’t need the embedded webserver (that option is only available in Neo4j Harness 4.0+) |
4 | Close it in an AfterAll |
5 | This the essential part: Apply the new configuration values.
This uses an ApplicationContextInitializer which can inject TestPropertyValues into the context before the context starts. |
This is a good solution It works well with both Community and enterprise edition and decouples the creation of the server from configuring the client. The downside of it: You have to configure a lot of stuff manually and your mileage may vary.
Since Spring Boot 2.2.6 you have an additional option:
As of Spring Framework 5.2.5, the TestContext framework provides support for dynamic property sources via the @DynamicPropertySource
annotation.
This annotation can be used in integration tests that need to add properties with dynamic values.
For more information, have a look at the Spring Framework reference.
Listing 25 is conceptionally the same variant as Listing 24 but much more concise:
@SpringBootTest
@EnableAutoConfiguration(exclude = { Neo4jTestHarnessAutoConfiguration.class })
class MoviesServiceAlt4Test {
private static Neo4j embeddedDatabaseServer;
@BeforeAll
static void initializeNeo4j() {
embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder()
.withDisabledServer() // No need for http
.withFixture(""
+ "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n"
+ "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n"
+ "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n"
)
.build();
}
@DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
registry.add("org.neo4j.driver.uri", embeddedDatabaseServer::boltURI);
registry.add("org.neo4j.driver.authentication.password", () -> "");
}
@AfterAll
static void closeNeo4j() {
embeddedDatabaseServer.close();
}
@Test
void testSomethingWithTheDriver(@Autowired Driver driver) {
}
}
Running your own driver bean
You can always fall back to create your own driver bean, but that actually disables the starter for the driver. That is of course ok, but you might end up with a very different configuration in test than in production. For example the driver will not use Spring logging, but its own default.
3.7. Dedicated routing driver
Usually, a Neo4j cluster should be identified by one logical neo4j://
URL.
This can be a load balancer, a DNS resolver, anything that turns the URL into a clusters entry point.
There are however edge cases in which this is not feasible and you might want to pass multiple, physical neo4j://
URLs to the driver.
We don’t offer a dedicated configuration flag for that.
The solution here is to define your own Driver-bean, but you can reuse our Neo4jDriverProperties
that saves you from manually reading configuration properties.
The configuration is stored in src/main/resources/application.properties
:
# Those are "our" standard properties
org.neo4j.driver.authentication.username=neo4j
org.neo4j.driver.authentication.password=secret
# Use the one you need
org.neo4j.driver.pool.max-connection-pool-size=50
# This is a custom property, can have any name you want
my.physical.neo4j.uris=\
neo4j://localhost:7687,\
neo4j://localhost:7688,\
neo4j://localhost:7689
To use the custom URIs, add a configuration class like this:
import java.net.URI;
import java.util.List;
import org.neo4j.driver.AuthToken;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.springframework.boot.autoconfigure.Neo4jDriverProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RoutingDriverConfiguration {
@Bean
public Driver neo4jDriver(
@Value("${my.physical.neo4j.uris}") List<URI> routingUris, (1)
Neo4jDriverProperties neo4jDriverProperties (2)
) {
AuthToken authToken = neo4jDriverProperties.getAuthToken(); (3)
Config config = neo4jDriverProperties.asDriverConfig();
return GraphDatabase.routingDriver(routingUris, authToken, config); (4)
}
}
1 | Map your custom property with @Value onto a list of Strings or URIs. |
2 | Let our machinery inject Neo4jDriverProperties , that includes all of our properties |
3 | Use the convenience methods to create org.neo4j.driver.AuthToken and org.neo4j.driver.Config as needed. |
4 | Call the dedicated routingDriver method |
3.8. Neo4j-OGM Integration
The current Spring Boot starter you get on start.spring.io recognizes the driver bean created by this starter here.
That means you can add this starter via org.neo4j.driver:neo4j-java-driver-spring-boot-starter:4.3.6.0
and use the properties under org.neo4j.driver
to configure the connection.
Thus you don’t have to work your way into Neo4j-OGM to manipulate the encapsulated driver but inject a correctly configured driver bean into the Neo4j-OGM session factory.
4. Appendix
Configuration options
Key | Default Value | Description |
---|---|---|
|
A kerberos ticket for connecting to the database. Mutual exclusive with a given username. |
|
|
The password of the user connecting to the database. |
|
|
The realm to connect to. |
|
|
The login of the user connecting to the database. |
|
|
|
Acquisition of new connections will be attempted for at most configured timeout. |
|
|
Specify socket connection timeout. |
|
|
Flag, if the driver should use encrypted traffic. |
|
Pooled connections that have been idle in the pool for longer than this timeout will be tested before they are used again. |
|
|
|
Flag, if leaked sessions logging is enabled. |
|
|
Pooled connections older than this threshold will be closed and removed from the pool. |
|
|
The maximum amount of connections in the connection pool towards a single database. |
|
|
Specify the maximum time transactions are allowed to retry. |
|
Specify a custom server address resolver used by the routing driver to resolve the initial address used to create the driver. |
|
|
The file of the certificate to use. |
|
|
|
Flag, if hostname verification is used. |
|
Configures the strategy to use use. |
|
|
The uri this driver should connect to. The driver supports bolt or neo4j as schemes. The starter does not provide a default URI so that clashes with the SDN/OGM Spring Boot starter are avoided. |
|
|
|
Set this to true, so that the driver collects metrics, which can be exported to Micrometer. |