Building microservices with Dropwizard, MongoDB & Docker

ricdev2

Ricardo Dev

Posted on January 22, 2019

Building microservices with Dropwizard, MongoDB & Docker

Dropwizard making magic. 😎

In this tutorial I will create a microservice using Dropwizard, this framework provides all kind of libraries to build a web application. Some of the libraries that include are:

Jetty Server: this is an open-source web server, lightweight and easy to embed in any application.

Jersey: implementation for creating RESTful web services.

Jackson: allows you manipulation from JSON objects to POJOs and vice versa.

Metrics: provides information about the state of HTTP requests, database connections, queues, etc.

Guava: support with many classes to deal with collections, validations, strings etc.

Hibernate Validator: validations as constrains for java objects.

JDBI: wraps JDBC and provides a flexible way to manipulate relational databases.

Liquibase: perfect to migrations and DDL changes in a database.

MongoDB a NoSQL database and much more.🌳

This NoSQL database oriented to documents (by documents like JSON) combines some of the features from relational databases, easy to use and the multi-platform is the best option for scale up and have fault tolerance, load balancing, map reduce, etc.

Technologies used in this microservice 📝

  • OpenJDK 8
  • Docker 2.0.0.0-mac81 (version for Mac)
  • MongoDB 4.0
  • Maven 3.5
  • Mac OS mojave (or any other like windows or linux)
  • Nginx 1.15
  • IntelliJ IDEA 2018

Ok let's start to create the project, Dropwizard provide us a maven archetype that we can use from its web site, here a example of this command:



mvn archetype:generate -DarchetypeGroupId=io.dropwizard.archetypes -DarchetypeArtifactId=java-simple -DarchetypeVersion=1.3.5 -DgroupId=com.demo -DartifactId=dropwizard-mongodb-ms -Dversion=1.0.0-SNAPSHOT -Dname=DropwizardMongoDBMicroservice


Enter fullscreen mode Exit fullscreen mode

Open a terminal and paste the before the command, make sure you have installed and configurated maven and java.

The final structure looks like this:



dropwizard-mongodb-ms/
├── README.md
├── config.yml
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── demo
    │   │           ├── DropwizardMongoDBMicroserviceApplication.java
    │   │           ├── DropwizardMongoDBMicroserviceConfiguration.java
    │   │           ├── api
    │   │           ├── cli
    │   │           ├── client
    │   │           ├── core
    │   │           ├── db
    │   │           ├── health
    │   │           └── resources
    │   └── resources
    │       ├── assets
    │       └── banner.txt
    └── test
        ├── java
        │   └── com
        │       └── demo
        │           ├── api
        │           ├── client
        │           ├── core
        │           ├── db
        │           └── resources
        └── resources
            └── fixtures


Enter fullscreen mode Exit fullscreen mode

Once the project was created, add the dependencies in the pom.xml file:



. . .
<properties>
        <dropwizard.version>1.3.5</dropwizard.version>
        <mainClass>com.demo.DropwizardMongoDBMicroserviceApplication</mainClass>
        <mongodb.version>3.8.2</mongodb.version>
        <jdk.version>1.8</jdk.version>
        <dropwizard.swagger.version>1.0.6-1</dropwizard.swagger.version>
        <mockito.core.version>2.23.0</mockito.core.version>
</properties>
. . .
<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-core</artifactId>
</dependency>
<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongodb-driver-sync</artifactId>
    <version>${mongodb.version}</version>
</dependency>

<dependency>
    <groupId>com.smoketurner</groupId>
    <artifactId>dropwizard-swagger</artifactId>
    <version>${dropwizard.swagger.version}</version>
</dependency>

<!-- Testing -->
<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-testing</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>${mockito.core.version}</version>
    <scope>test</scope>
</dependency>
<!-- Testing -->
. . .


Enter fullscreen mode Exit fullscreen mode

I added some configurations to the file configuration.yaml, looks like this:



server:
  maxThreads: 512
  applicationContextPath: /dropwizard-mongodb-ms
  applicationConnectors:
    - type: http
      port: 8080
  adminConnectors:
    - type: http
      port: 8081

logging:
  level: INFO
  loggers:
    com.demo: INFO

#You can choose the user and password what you want.
mongoDBConnection:
  credentials:
    username: "user_donuts" 
    password: "pAsw0Rd"
  seeds:
    - host: "mongodb"
      port: 27017
  database: "donuts"

swagger:
  basePath: /dropwizard-mongodb-ms
  resourcePackage: com.demo.resources
  scan: true
  info:
    version: "1.0.0"
    title: "Donuts API CRUD"
    description: "A simple API used for expose CRUD operation on MongoDB collection"
    termsOfService: "http://swagger.io/terms/"
    contact:
      name: "Donuts API "
    license:
      name: "Rich"


Enter fullscreen mode Exit fullscreen mode

The configuration.yml maps to a class that extends of io.dropwizard.Configuration and looks like this:



package com.demo;

import com.demo.db.configuration.MongoDBConnection;
import com.fasterxml.jackson.annotation.JsonProperty;

import io.dropwizard.Configuration;
import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration;

public class DropwizardMongoDBMicroserviceConfiguration extends Configuration {

    /**
     * The data configuration for MongoDB.
     */

    private MongoDBConnection mongoDBConnection;

    @JsonProperty("swagger")
    private SwaggerBundleConfiguration swaggerBundleConfiguration;

    //Getters and setters
}


Enter fullscreen mode Exit fullscreen mode

This microservice will expose an API REST base on a CRUD (Create, Read, Update, Delete) I've chosen a product like donuts (🍩 🤤), some parts of the code you can find in my Github repository at the end of this tutorial.

I'm using the com.mongodb.client.MongoCollection interface for access to the collection called donuts to have more granularity in the operations so I created a DAO (Data Access Object) to manipulate the CRUD operations.



package com.demo.db.daos;
//imports ... 
public class DonutDAO {

    /** The collection of Donuts */
    final MongoCollection<Document> donutCollection;

    /**
     * Constructor.
     *
     * @param donutCollection the collection of donuts.
     */
    public DonutDAO(final MongoCollection<Document> donutCollection) {
        this.donutCollection = donutCollection;
    }

    /**
     * Find all donuts.
     *
     * @return the donuts.
     */
    public List<Donut> getAll() {
        final MongoCursor<Document> donuts = donutCollection.find().iterator();
        final List<Donut> donutsFind = new ArrayList<>();
        try {
            while (donuts.hasNext()) {
                final Document donut = donuts.next();
                donutsFind.add(DonutMapper.map(donut));
            }
        } finally {
            donuts.close();
        }
        return donutsFind;
    }

    /**
     * Get one document find in other case return null.
     *
     * @param id the identifier for find.
     * @return the Donut find.
     */
    public Donut getOne(final ObjectId id) {
        final Optional<Document> donutFind = Optional.ofNullable(donutCollection.find(new Document("_id", id)).first());
        return donutFind.isPresent() ? DonutMapper.map(donutFind.get()) : null;
    }

    public void save(final Donut donut){
        final Document saveDonut = new Document("price", donut.getPrice())
                                      .append("flavor", donut.getFlavor());
        donutCollection.insertOne(saveDonut);
    }

    /**
     * Update a register.
     *
     * @param id the identifier.
     * @param donut the object to update.
     */
    public void update(final ObjectId id, final Donut donut) {
        donutCollection.updateOne(new Document("_id", id),
                new Document("$set", new Document("price", donut.getPrice())
                        .append("flavor", donut.getFlavor()))
        );
    }

    /**
     * Delete a register.
     * @param id    the identifier.
     */
    public void delete(final ObjectId id){
        donutCollection.deleteOne(new Document("_id", id));
    }


Enter fullscreen mode Exit fullscreen mode

For map the information that comes from the database I use a utility class that filters the fields from MongoDB to a POJO.



package com.demo.util;

import com.demo.api.Donut;
import org.bson.Document;

public class DonutMapper {

    /**
     * Map objects {@link Document} to {@link Donut}.
     *
     * @param donutDocument the information document.
     * @return A object {@link Donut}.
     */
    public static Donut map(final Document donutDocument) {
        final Donut donut = new Donut();
        donut.setId(donutDocument.getObjectId("_id"));
        donut.setFlavor(donutDocument.getString("flavor"));
        donut.setPrice(donutDocument.getDouble("price"));
        return donut;
    }
}


Enter fullscreen mode Exit fullscreen mode

Then create a POJO to manipulate the information on MongoDB.



package com.demo.api;
//imports ...
public class Donut implements Serializable {

    /** The id.*/
    @JsonSerialize(using = ObjectIdSerializer.class)
    private ObjectId id;

    /** The price. */
    @NotNull
    private double price;

    /** The principal flavor.*/
    @NotNull
    private String flavor;

    /**Constructor.*/
    public Donut() {
    }

//getters & setters
//hashcode, equals and toString methods


Enter fullscreen mode Exit fullscreen mode

This class is only used in response to the endpoint.



package com.demo.api;
//imports ...
public class Response {
    /** The message.*/
    private String message;

    /** Constructor.*/
    public Response() {
    }
//getters & setters
//hashcode, equals and toString methods


Enter fullscreen mode Exit fullscreen mode

Here more utility classes for convert org.bson.types.ObjectId to String objects using Jackson.



package com.demo.util;

import java.io.IOException;

import org.bson.types.ObjectId;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class ObjectIdSerializer extends JsonSerializer<ObjectId> {
    @Override
    public void serialize(final ObjectId objectId, final JsonGenerator jsonGenerator,
                          final SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(objectId.toString());
    }
}


Enter fullscreen mode Exit fullscreen mode

For convert String object to array of char.



package com.demo.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

public class PasswordSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(final String input, final JsonGenerator jsonGenerator,
                          final SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(input.toCharArray(), 0, input.toCharArray().length);
    }

    @Override
    public Class<String> handledType() {
        return String.class;
    }
}


Enter fullscreen mode Exit fullscreen mode

Use these POJOs that map information with the configuration.yml.



package com.demo.db.configuration;

import java.util.Arrays;
import java.util.Objects;

import com.demo.util.PasswordSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

/**
 * This class is used for credentials.
 * @version 1.0.0
 * @since 1.0.0
 * @author Rich Lopez
 */
public class Credentials {

    /** The user name.*/
    private String username;

    /** The password.*/
    @JsonSerialize(using = PasswordSerializer.class)
    private char[] password;

    //getters, setters, hashcode and equals methods.
}


Enter fullscreen mode Exit fullscreen mode


package com.demo.db.configuration;

import java.util.Objects;

public class Seed {

    /** The host.*/
    private String host;

    /** The port.*/
    private int port;
    //getters, setters, hashcode and equals methods.
}


Enter fullscreen mode Exit fullscreen mode


package com.demo.db.configuration;

import java.util.List;

public class MongoDBConnection {

    /**
     * The credentials user and password.
     */
    private Credentials credentials;

    /**
     * The lis of seeds.
     */
    private List<Seed> seeds;

    /**
     * The db.
     */
    private String database;
}


Enter fullscreen mode Exit fullscreen mode

For manage the connection with MongoDB we need map the information come from the yaml file configuration to this classes com.demo.db.configuration.Credentials , com.demo.db.configuration.MongoDBConnection, com.demo.db.configuration.Seed.

The class com.demo.db.MongoDBFactoryConnection creates the client for MongoDB with the options that provided in yaml file, the method for create the instance look like:



package com.demo.db;
//imports ...
public class MongoDBFactoryConnection {

    /** The configuration for connect to MongoDB Server.*/
    private MongoDBConnection mongoDBConnection;

    /**
     * Constructor.
     *
     * @param mongoDBConnection the mongoDB connection data.
     */
    public MongoDBFactoryConnection(final MongoDBConnection mongoDBConnection) {
        this.mongoDBConnection = mongoDBConnection;
    }

    /**
     * Gets the connection to MongoDB.
     *
     * @return the mongo Client.
     */
    public MongoClient getClient() {
        LOGGER.info("Creating mongoDB client.");
        final Credentials configCredentials = mongoDBConnection.getCredentials();

        final MongoCredential credentials = MongoCredential.createCredential(
                configCredentials.getUsername(),
                mongoDBConnection.getDatabase(),
                configCredentials.getPassword());

        final MongoClient client = MongoClients.create(
                MongoClientSettings.builder()
                        .credential(credentials)
                        .applyToClusterSettings(builder -> builder.hosts(getServers())).build()
        );

        return client;
    }

    /**
     * Map the object {@link Seed} to objects {@link ServerAddress} that contain the information of servers.
     *
     * @return the list of servers.
     */
    private List<ServerAddress> getServers() {
        final List<Seed> seeds = mongoDBConnection.getSeeds();
        return seeds.stream()
                .map(seed -> {
                    final ServerAddress serverAddress = new ServerAddress(seed.getHost(), seed.getPort());
                    return serverAddress;
                })
                .collect(Collectors.toList());
    }


Enter fullscreen mode Exit fullscreen mode

In Dropwizard life cycle the object can be manage by the interface io.dropwizard.lifecycle.Managed and the class io.dropwizard.lifecycle.MongoDBManaged.



package com.demo.db;
//imports ...
public class MongoDBManaged implements Managed {

    /** The mongoDB client.*/
    private MongoClient mongoClient;

    /**
     * Constructor.
     * @param mongoClient   the mongoDB client.
     */
    public MongoDBManaged(final MongoClient mongoClient) {
        this.mongoClient = mongoClient;
    }

    @Override
    public void start() throws Exception {
    }

    @Override
    public void stop() throws Exception {
        mongoClient.close();
    }


Enter fullscreen mode Exit fullscreen mode

Another important class is the Healthcheck, for creating this class is only required to extend from com.codahale.metrics.health.HealthCheck and implement the method check.



package com.demo.health;
//imports ...
public class DropwizardMongoDBHealthCheck extends HealthCheck {
     /** A client of MongoDB.*/
    private MongoClient mongoClient;

    /**
     * Constructor.
     *
     * @param mongoClient the mongo client.
     */
    public DropwizardMongoDBHealthCheck(final MongoClient mongoClient) {
        this.mongoClient = mongoClient;
    }
    @Override
    protected Result check() {
        try {
            final Document document = mongoClient.getDatabase("donuts").runCommand(new Document("buildInfo", 1));
            if (document == null) {
                return Result.unhealthy("Can not perform operation buildInfo in Database.");
            }
        } catch (final Exception e) {
            return Result.unhealthy("Can not get the information from database.");
        }
        return Result.healthy();
    }


Enter fullscreen mode Exit fullscreen mode

The entry point of this microservice is the class com.demo.DropwizardMongoDBMicroserviceApplication loads all bundles configurations, initialization classes, etc. This class must extend from io.dropwizard.Application and is parametrized by a class that extends of io.dropwizard.Configuration.



package com.demo;

import com.demo.db.MongoDBFactoryConnection;
import com.demo.db.daos.DonutDAO;
import com.demo.db.MongoDBManaged;
import com.demo.health.DropwizardMongoDBHealthCheck;
import com.demo.resources.DonutResource;

import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.federecio.dropwizard.swagger.SwaggerBundle;
import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DropwizardMongoDBMicroserviceApplication extends Application<DropwizardMongoDBMicroserviceConfiguration> {

    /**
     * Logger class.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardMongoDBMicroserviceApplication.class);

    /**
     * Entry point for start Application.
     *
     * @param args the args.
     * @throws Exception when the app can not start.
     */
    public static void main(final String[] args) throws Exception {
        LOGGER.info("Start application.");
        new DropwizardMongoDBMicroserviceApplication().run(args);
    }

    @Override
    public String getName() {
        return "DropwizardMongoDBMicroservice";
    }

    @Override
    public void initialize(final Bootstrap<DropwizardMongoDBMicroserviceConfiguration> bootstrap) {
        bootstrap.addBundle(new SwaggerBundle<DropwizardMongoDBMicroserviceConfiguration>() {
            @Override
            protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(
                        final DropwizardMongoDBMicroserviceConfiguration dropwizardMongoDBMicroserviceConfiguration) {
                return dropwizardMongoDBMicroserviceConfiguration.getSwaggerBundleConfiguration();
            }
        });
    }

    @Override
    public void run(final DropwizardMongoDBMicroserviceConfiguration configuration,
                    final Environment environment) {

        final MongoDBFactoryConnection mongoDBManagerConn = new MongoDBFactoryConnection(configuration.getMongoDBConnection());

        final MongoDBManaged mongoDBManaged = new MongoDBManaged(mongoDBManagerConn.getClient());

        final DonutDAO donutDAO = new DonutDAO(mongoDBManagerConn.getClient()
                .getDatabase(configuration.getMongoDBConnection().getDatabase())
                .getCollection("donuts"));

        environment.lifecycle().manage(mongoDBManaged);
        environment.jersey().register(new DonutResource(donutDAO));
        environment.healthChecks().register("DropwizardMongoDBHealthCheck",
                new DropwizardMongoDBHealthCheck(mongoDBManagerConn.getClient()));
    }
}


Enter fullscreen mode Exit fullscreen mode

For building the project using the command:

$ mvn clean package

Before to start the application is required to have the database, with MongoDB could you download from the website or using a docker image in DockerHub.

Once download the image of MongoDB, start a new container:

$ docker run --name mongodb -d -p 27017:27017 mongo

NOTE: Ensure the creation of a volume to maintain the persistence of information.

enter the container and type the next commands:

$ docker exec -it [container name or id] /bin/bash
$ mongo
$ use donuts
> db.createUser({ user: "user_donuts", pwd: "pAsw0Rd", roles: [ { role: "readWrite", db: "donuts"} ]});

By default, MongoDB sets the authentication mechanism to SCRAM.

And now start the application with the command:

$ java -jar target/dropwizard-mongodb-ms-1.0.0-SNAPSHOT.jar server configuration.yml

Once we have created the user and the database, we can enter the swagger documentation in the following URL:

http://localhost:8080/dropwizard-mongodb-ms/swagger

Swagger Documentation

Using docker compose 🐳

One of the reasons for use docker every day is because provide us with a unique environment and all developers keep the dependencies, databases, web servers, etc synchronized. The easy way to build and deploy artifacts reduce delivery time.

I’m using docker for start the application and the MongoDB server, besides I add Nginx proxy for receiving the request, this is an example of docker compose file:



version: '3'
services:
  mongodb:
    image: mongo
    restart: always
    container_name: mongodb
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: admin
    ports:
      - 27017:27017
    networks:
      - dropw-mongodb-ntw

  nginx:
    image: nginx
    container_name: nginx
    volumes:
    - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
    - "8080:80"
    - "443:443"
    networks:
    - dropw-mongodb-ntw

  dropw-ms:
    image: openjdk:8-jre
    container_name: dropw-ms
    volumes:
    - ./target/dropwizard-mongodb-ms-1.0.0-SNAPSHOT.jar:/microservice/dropwizard-mongodb-ms-1.0.0-SNAPSHOT.jar
    - ./configuration.yml:/microservice/configuration.yml
    working_dir: /microservice
    command: ["java", "-jar", "dropwizard-mongodb-ms-1.0.0-SNAPSHOT.jar", "server", "configuration.yml"]
    ports:
    - "8090:8080"
    - "8091:8081"
    networks:
    - dropw-mongodb-ntw

networks:
  dropw-mongodb-ntw:
    external:
      name: dropw-mongodb-ntw


Enter fullscreen mode Exit fullscreen mode

We create the docker network with the command :

docker network create dropw-mongodb-ntw

build the project using the command:

$ mvn clean package

Start docker compose:

$ docker-compose up

once we have created all the services we need to create the user for the database, we will access the container with the following commands.

$ docker exec -it [container name or id] /bin/bash
$ mongo
> use donuts
> db.createUser({ user: "user_donuts", pwd: "pAsw0Rd", roles: [ { role: "readWrite", db: "donuts"} ]});

http://localhost:8080/dropwizard-mongodb-ms/swagger

Conclusion

Dropwizard provides us with several tools to create Microservices in a way easy. You can integrate with other technologies like MongoDB, Docker, Nginx by example. Remember that all depends on the requirements and the problem to solve, but for me, this is good for start and share experiences.

Github repository: https://github.com/ricdev2/dropwizard-mongodb-ms

💖 💪 🙅 🚩
ricdev2
Ricardo Dev

Posted on January 22, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related