In the previous article, I introduced FerretDB, an OSS database compatible with MongoDB.
However, DocumentDB itself, which is used as the backend for Documentdb, now supports the MongoDB API, making FerretDB unnecessary.
In this article, I'll memo how to access DocumentDB using Spring Boot + Spring Data MongoDB.
From an application perspective, it's the same as MongoDB - only the Testcontainers configuration changes.
The following is basically a rework of the previous article.
Creating Project Template
Use Spring Initializr to create a Spring Boot project template.
curl -s https://start.spring.io/starter.tgz \
-d artifactId=demo-documentdb \
-d name=demo-documentdb \
-d baseDir=demo-documentdb \
-d packageName=com.example \
-d dependencies=web,data-mongodb,actuator,configuration-processor,prometheus,native,testcontainers \
-d type=maven-project \
-d applicationName=DemoDocumentDbApplication | tar -xzvf -
cd demo-documentdb
Creating Sample Application
Create a very simple application that saves and retrieves messages.
cat <<EOF > src/main/java/com/example/Message.java
package com.example;
public record Message(String id, String text) {
}
EOF
cat <<EOF > src/main/java/com/example/HelloController.java
package com.example;
import java.util.List;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
private final MongoTemplate mongoTemplate;
public HelloController(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@PostMapping(path = "/messages")
public Message postMessage(@RequestBody String text) {
return mongoTemplate.save(new Message(null, text));
}
@GetMapping(path = "/messages")
public List<Message> getMessages() {
return mongoTemplate.findAll(Message.class);
}
}
EOF
Since we added Testcontainers with Spring Initializr, the following file is included in the project.
package com.example;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
@ServiceConnection
MongoDBContainer mongoDbContainer() {
return new MongoDBContainer(DockerImageName.parse("mongo:latest"));
}
}
Let's start the app using Testcontainers. Run src/test/java/com/example/TestDemoDocumentDbApplication.java or execute the following command:
./mvnw spring-boot:test-run
Once the application and MongoDB are started, try POSTing & GETting messages as follows:
$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!"
{"id":"6879a6f3ba99e4ec5c9419fd","text":"Hello MongoDB!"}
$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello DocumentDB\!"
{"id":"6879a6f8ba99e4ec5c9419fe","text":"Hello DocumentDB!"}
$ curl -s http://localhost:8080/messages | jq .
[
{
"id": "6879a6f3ba99e4ec5c9419fd",
"text": "Hello MongoDB!"
},
{
"id": "6879a6f8ba99e4ec5c9419fe",
"text": "Hello DocumentDB!"
}
]
The test code equivalent to this operation verification would be as follows:
cat <<EOF > src/test/java/com/example/DemoDocumentDbApplicationTests.java
package com.example;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
@Import(TestcontainersConfiguration.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoDocumentDbApplicationTests {
RestClient restClient;
@BeforeEach
void setUp(@LocalServerPort int port, @Autowired RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.defaultStatusHandler(statusCode -> true, (req, res) -> {
/* NO-OP */}).baseUrl("http://localhost:" + port).build();
}
@Test
void contextLoads() {
{
ResponseEntity<Message> res = this.restClient.post()
.uri("/messages")
.contentType(MediaType.TEXT_PLAIN)
.body("Hello MongoDB!")
.retrieve()
.toEntity(Message.class);
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
Message message = res.getBody();
assertThat(message).isNotNull();
assertThat(message.text()).isEqualTo("Hello MongoDB!");
assertThat(message.id()).isNotNull();
}
{
ResponseEntity<Message> res = this.restClient.post()
.uri("/messages")
.contentType(MediaType.TEXT_PLAIN)
.body("Hello DocumentDB!")
.retrieve()
.toEntity(Message.class);
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
Message message = res.getBody();
assertThat(message).isNotNull();
assertThat(message.text()).isEqualTo("Hello DocumentDB!");
assertThat(message.id()).isNotNull();
}
{
ResponseEntity<List<Message>> res = this.restClient.get()
.uri("/messages")
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {
});
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
List<Message> messages = res.getBody();
assertThat(messages).isNotNull();
assertThat(messages).hasSize(2);
assertThat(messages).map(Message::id).allSatisfy(id -> assertThat(id).isNotNull());
assertThat(messages).map(Message::text).containsExactly("Hello MongoDB!", "Hello DocumentDB!");
}
}
}
EOF
The test also starts MongoDB with Testcontainers. You can run the test with the following command:
./mvnw clean test
Switching to DocumentDB
Switch from MongoDB to DocumentDB. The application code remains the same, only the Testcontainers configuration needs to be changed.
Since the Documentdb container image was not compatible with MongoDBContainer, we use GenericContainer.
Therefore, instead of using ServiceConnection, we use DynamicPropertyRegistrar to dynamically register MongoDB connection information. Since Documentdb has authentication enabled by default, we set spring.data.mongodb.uri including username and password.
Change TestcontainersConfiguration as follows:
cat <<EOF > src/test/java/com/example/TestcontainersConfiguration.java
package com.example;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
GenericContainer<?> documentdbContainer() {
return new GenericContainer<>(DockerImageName.parse("ghcr.io/microsoft/documentdb/documentdb-local:latest"))
.withExposedPorts(10260 /* MongoDB Port */, 9712 /* PostgreSQL port */)
.withEnv("USERNAME", "user")
.withEnv("PASSWORD", "password")
.withEnv("ENFORCE_SSL", "false")
.waitingFor(new HostPortWaitStrategy().forPorts(10260, 9712));
}
@Bean
DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer<?> documentdbContainer) {
return registry -> registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test"
.formatted(documentdbContainer.getHost(), documentdbContainer.getMappedPort(10260)));
}
}
EOF
The same test should pass after changing TestcontainersConfiguration:
./mvnw clean test
Re-run src/test/java/com/example/TestDemoDocumentDbApplication.java or re-execute the following command:
./mvnw spring-boot:test-run
Try POSTing & GETting messages as before:
$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!"
{"id":"6879af654c503243968ecba0","text":"Hello MongoDB!"}
$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello DocumentDB\!"
{"id":"6879af6a4c503243968ecba1","text":"Hello DocumentDB!"}
$ curl -s http://localhost:8080/messages | jq .
[
{
"id": "6879af654c503243968ecba0",
"text": "Hello MongoDB!"
},
{
"id": "6879af6a4c503243968ecba1",
"text": "Hello DocumentDB!"
}
]
We confirmed that switching from MongoDB to Documentdb works without any issues.
Running Standalone
Let's try running the application standalone without using Testcontainers.
Start Documentdb with the following docker run command:
docker run --rm --name documentdb -p 10260:10260 -e USERNAME=user -e PASSWORD=password -e ENFORCE_SSL=false ghcr.io/microsoft/documentdb/documentdb-local:latest
Create an executable jar file with the following command:
./mvnw clean package
Specify spring.data.mongodb.uri at runtime to connect to Documentdb:
java -jar target/demo-documentdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:10260/test
You should be able to POST & GET messages as before.
Enabling TLS Connection to DocumentDB
The Docker image ghcr.io/microsoft/documentdb/documentdb-local has TLS enabled by default.
In the previous example, we disabled TLS with the environment variable ENFORCE_SSL=false, but removing this setting requires TLS connection.
To enable TLS connection to MongoDB during testing, you need to register the CA certificate of the TLS certificate configured in DocumentDB to the TrustStore.
This time, we'll use netty-pkitesting to create a self-signed certificate for testing and dynamically configure to use this certificate.
Add the following dependency to pom.xml:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-pkitesting</artifactId>
<version>4.2.4.Final</version>
<scope>test</scope>
</dependency>
Change TestcontainersConfiguration as follows:
cat <<EOF > src/test/java/com/example/TestcontainersConfiguration.java
package com.example;
import io.netty.pkitesting.CertificateBuilder;
import io.netty.pkitesting.X509Bundle;
import java.io.File;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.utility.DockerImageName;
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
@Bean
X509Bundle selfSignedCertificate() throws Exception {
return new CertificateBuilder().subject("CN=localhost").setIsCertificateAuthority(true).buildSelfSigned();
}
@Bean
GenericContainer<?> documentdbContainer(X509Bundle selfSignedCertificate) throws Exception {
File tempCertChainPem = selfSignedCertificate.toTempCertChainPem();
File tempPrivateKeyPem = selfSignedCertificate.toTempPrivateKeyPem();
return new GenericContainer<>(DockerImageName.parse("ghcr.io/microsoft/documentdb/documentdb-local:latest"))
.withExposedPorts(10260, 9712)
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("documentdb")))
.waitingFor(new HostPortWaitStrategy().forPorts(10260, 9712))
.withFileSystemBind(tempCertChainPem.getAbsolutePath(), "/tmp/cert.pem", BindMode.READ_ONLY)
.withFileSystemBind(tempPrivateKeyPem.getAbsolutePath(), "/tmp/private.key", BindMode.READ_ONLY)
.withEnv("CERT_PATH", "/tmp/cert.pem")
.withEnv("KEY_FILE", "/tmp/private.key")
.withEnv("USERNAME", "user")
.withEnv("PASSWORD", "password");
}
@Bean
DefaultSslBundleRegistry sslBundles(X509Bundle selfSignedCertificate) {
DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry();
bundles.registerBundle("self-signed", SslBundle.of(new PemSslStoreBundle(null,
PemSslStoreDetails.forCertificate(selfSignedCertificate.getRootCertificatePEM()))));
return bundles;
}
@Bean
DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer<?> documentdbContainer) {
return registry -> {
registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test"
.formatted(documentdbContainer.getHost(), documentdbContainer.getMappedPort(10260)));
registry.add("spring.data.mongodb.ssl.enabled", () -> "true");
registry.add("spring.data.mongodb.ssl.bundle", () -> "self-signed");
};
}
}
EOF
Confirm that the test succeeds even with this configuration change:
./mvnw clean test
As before, let's try running the application standalone without using Testcontainers.
Create a self-signed certificate with the following command:
DIR=/tmp/self-signed
mkdir -p ${DIR}
# Create CA certificate
openssl req -new -nodes -out ${DIR}/ca.csr -keyout ${DIR}/ca.key -subj "/CN=@making/O=LOL.MAKI/C=JP"
chmod og-rwx ${DIR}/ca.key
cat <<EOF > ${DIR}/ext_ca.txt
basicConstraints=CA:TRUE
keyUsage=digitalSignature,keyCertSign
EOF
openssl x509 -req -in ${DIR}/ca.csr -days 3650 -signkey ${DIR}/ca.key -out ${DIR}/ca.crt -extfile ${DIR}/ext_ca.txt
cat <<EOF > ${DIR}/ext.txt
basicConstraints=CA:FALSE
keyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement
extendedKeyUsage=serverAuth,clientAuth
EOF
# Create Server certificate signed by CA
openssl req -new -nodes -out ${DIR}/server.csr -keyout ${DIR}/server.key -subj "/CN=localhost"
chmod og-rwx ${DIR}/server.key
openssl x509 -req -in ${DIR}/server.csr -days 3650 -CA ${DIR}/ca.crt -CAkey ${DIR}/ca.key -CAcreateserial -out ${DIR}/server.crt -extfile ${DIR}/ext.txt
Start Documentdb with the following docker run command:
docker run --rm --name documentdb -p 10260:10260 -v /tmp/self-signed:/tmp/self-signed -e USERNAME=user -e PASSWORD=password -e CERT_PATH=/tmp/self-signed/server.crt -e KEY_FILE=/tmp/self-signed/server.key ghcr.io/microsoft/documentdb/documentdb-local:latest
Create an executable jar file with the following command:
./mvnw clean package
Specify spring.data.mongodb.uri at runtime to connect to Documentdb:
java -jar target/demo-documentdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:10260/test --spring.ssl.bundle.pem.self-signed.truststore.certificate=file:/tmp/self-signed/ca.crt --spring.data.mongodb.ssl.enabled=true --spring.data.mongodb.ssl.bundle=self-signed
You should be able to POST & GET messages as before.
The verified source code is available here.
I introduced how to use Documentdb with Spring Boot + Spring Data MongoDB + Testcontainers.
By using DocumentDB as an alternative to SSPL MongoDB, it seems possible to avoid license issues while utilizing MongoDB-compatible features.
For other information, please refer to the documentation.