Spring Boot Auto-configuration for Embedded MongoDB with Support for Transactions

By using EmbeddedMongoAutoConfiguration in Spring Boot, it is possible to test your code against a live MongoDB instance that is started and stopped automatically.

In version 4.0, MongoDB supports multi-document transactions on replica sets. As of now (circa June 2020), Spring Boot doesn’t have out of the box support for testing with embedded MongoDB that supports such transactions.

Below is the source code for a class – EmbeddedMongoWithTransactionsConfig – that allows you to do just that: start a single embedded MongoDB instance and configure it to support transactions on a replica set. To use it, just enable the embedMongoWithTxns profile by adding it to “spring.active.profiles”. For example:

-Dspring.active.profiles=...,embedMongoWithTxns

Here is the complete source code (put it in a package of your liking):

import java.io.IOException;
import java.net.UnknownHostException;

import org.bson.Document;
import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;

import com.mongodb.BasicDBList;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoDatabase;

import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.IMongodConfig;
import de.flapdoodle.embed.mongo.config.MongoCmdOptionsBuilder;
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.runtime.Network;

/**
 * Class for auto-configuring and starting an embedded MongoDB with support for transactions.
 * As there's some overhead in using it and slower startup time, use it only if support for
 * transactions is needed.
 */
@Profile("embedMongoWithTxns")
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore({ MongoAutoConfiguration.class })
@ConditionalOnClass({ MongoClient.class, MongodStarter.class })
@Import({ 
    EmbeddedMongoAutoConfiguration.class,
    EmbeddedMongoWithTransactionsConfig.DependenciesConfiguration.class 
})
public class EmbeddedMongoWithTransactionsConfig {

    // You may get a warning in the log upon shutdown like this:
    // "...Destroy method 'stop' on bean with name 'embeddedMongoServer' threw an
    // exception: java.lang.IllegalStateException: Couldn't kill mongod process!..."
    // That seems harmless as the MongoD process shuts down and frees up the port.
    // There are multiple related issues logged on GitHub:
    // https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues?q=is%3Aissue+Couldn%27t+kill+mongod+process%21

    public static final int DFLT_PORT_NUMBER = 27017;
    public static final String DFLT_REPLICASET_NAME = "rs0";
    public static final int DFLT_STOP_TIMEOUT_MILLIS = 200;

    private Version.Main mFeatureAwareVersion = Version.Main.V4_0;
    private int mPortNumber = DFLT_PORT_NUMBER;
    private String mReplicaSetName = DFLT_REPLICASET_NAME;
    private long mStopTimeoutMillis = DFLT_STOP_TIMEOUT_MILLIS;

    @Bean
    public IMongodConfig mongodConfig() throws UnknownHostException, IOException {
        final IMongodConfig mongodConfig = new MongodConfigBuilder().version(mFeatureAwareVersion)
            .withLaunchArgument("--replSet", mReplicaSetName)
            .stopTimeoutInMillis(mStopTimeoutMillis)
            .cmdOptions(new MongoCmdOptionsBuilder().useNoJournal(false).build())
            .net(new Net(mPortNumber, Network.localhostIsIPv6())).build();
        return mongodConfig;
    }

    /**
     * Initializes a new replica set.
     * Based on code from https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues/257
     */
    class EmbeddedMongoReplicaSetInitialization {

        EmbeddedMongoReplicaSetInitialization() throws Exception {
            MongoClient mongoClient = null;
            try {
                final BasicDBList members = new BasicDBList();
                members.add(new Document("_id", 0).append("host", "localhost:" + mPortNumber));

                final Document replSetConfig = new Document("_id", mReplicaSetName);
                replSetConfig.put("members", members);

                mongoClient =
                    new MongoClient(new ServerAddress(Network.getLocalHost(), mPortNumber));
                final MongoDatabase adminDatabase = mongoClient.getDatabase("admin");
                adminDatabase.runCommand(new Document("replSetInitiate", replSetConfig));
            }
            finally {
                if (mongoClient != null) {
                    mongoClient.close();
                }
            }
        }
    }

    @Bean
    EmbeddedMongoReplicaSetInitialization embeddedMongoReplicaSetInitialization() throws Exception {
        return new EmbeddedMongoReplicaSetInitialization();
    }

    /**
     * Additional configuration to ensure that the replica set initialization happens after the
     * {@link MongodExecutable} bean is created. That's it - after the database is started.
     */
    @ConditionalOnClass({ MongoClient.class, MongodStarter.class })
    protected static class DependenciesConfiguration
        extends AbstractDependsOnBeanFactoryPostProcessor {

        DependenciesConfiguration() {
            super(EmbeddedMongoReplicaSetInitialization.class, null, MongodExecutable.class);
        }
    }

}

Perhaps it is worth noticing that the MongoDB instance isn’t really embedded in the JVM of your application. It actually runs as a separate process that is started and stopped as part of the lifecycle of your application.

Enjoy!

 

As always,

Happy API Simulating!

…with the API Simulator