DropWizard HelloWorld API from scratch

Hey! All these techs I have been skimming through their quickstart are related to each other. So far, I have seen Java, Junit, Maven, and now going for DropWizard, that you may see as a Django’s alternative for Java. And from definition, Dropwizard is a Java framework for developing ops-friendly, high-performance, RESTful web services. Let’s explore and get our hands on this Wizard!

Updates (I added later are in this list)

I would be using my IntelliJ Community Edition 2020.3.1 for going through this quickstart. Let’s get to their official docs: #1 https://www.dropwizard.io/en/latest/getting-started.html

Okay, I read through the #1 and it feels like, if you generate your Dropwizard project from their archetype, then a lot of code you get by default, similar to Django startup. But using archetype to generate your project is more like using a cookiecutter (template based generation) rather than “Django startproject command”.

I also want to note here that if you directly start looking into DropWizard, then it might be confusing until you understand a bit of Java and Maven at least. Here are my recent posts, if you want to take a look.

  1. Java Quickstart – Live Learning Notes
  2. Maven Quickstart – Live Learning Notes

So, I think I should create a HelloWorld REST API using DropWizard, but without using archetype generation. So that I can understand how everything is working in DropWizard as a whole. I’ll start from scratch.

Prerequisite

  1. I have java 11 (OpenJDK – Amazon Corretto): Java Quickstart – Live Learning Notes
  2. I have maven 3.6.4: Maven Quickstart – Live Learning Notes
  3. I have IntelliJ Community Edition 2020.3.1

First step

  1. mkdir helloword
  2. cd helloworld
  3. vi pom.xml

The pom.xml

Add the minimum content required in this file “pom.xml” refer: https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#minimal-pom

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.helloworld</groupId>
  <artifactId>helloworld</artifactId>
  <version>0.0.0</version>
</project>

Try creating the package with this single file only with mvn package, and here we go, the pom.xml is working alright as I got a new target directory having the Jar of this package.

.
├── pom.xml
└── target
    ├── helloworld-0.0.0.jar
    └── maven-archiver
        └── pom.properties

2 directories, 3 files

Now let’s create a typical maven project directory structure.

  1. mkdir -p src/main/java/com/mycompany/helloworld
  2. mkdir -p src/test/java/com/mycompany/helloworld

The above directory structure matches with <groupId>com.mycompany.helloworld</groupId> in pom.xml.

.
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── com
│   │           └── mycompany
│   │               └── helloworld
│   └── test
│       └── java
│           └── com
│               └── mycompany
│                   └── helloworld
└── target

Add Dropwizard as dependency and other updates in pom.xml

We need to define Dropwizard as a dependency so that Maven can install it once we would compile the code later on. Along with it also define some other tags as well. My pom.xml has become:

<project>

  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.helloworld</groupId>
  <artifactId>helloworld</artifactId>
  <version>0.0.0</version>

  <name>HelloWorld DropWizard</name>

  <properties>
      <dropwizard.version>2.0.0</dropwizard.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-dependencies</artifactId>
      <version>${dropwizard.version}</version>
    </dependency>
  </dependencies>

</project>

Now we need the following files, to get DropWizard started. These files contains:

  1. Application Class File
  2. Configuration Class File
  3. Representation Class File
  4. Resource Class File
  5. Health Check Class (not necessary but highly recommended) File
  6. Config File (yaml)

The main class would be our Application Class. The location and name of each of these files are as follows. I am creating them with touch command.

  1. touch src/main/java/com/mycompany/helloworld/HelloWorldApplication.java
  2. touch src/main/java/com/mycompany/helloworld/HelloWorldConfiguration.java
  3. mkdir src/main/java/com/mycompany/helloworld/api
  4. touch src/main/java/com/mycompany/helloworld/api/HelloWorld.java
  5. mkdir src/main/java/com/mycompany/helloworld/resources
  6. touch src/main/java/com/mycompany/helloworld/resources/HelloWorldResource.java
  7. mkdir src/main/java/com/mycompany/helloworld/health
  8. touch src/main/java/com/mycompany/helloworld/health/ConfigHealthCheck.java
  9. touch ./config.yml

Current tree is here:

.
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── com
│   │           └── mycompany
│   │               └── helloworld
│   │                   ├── HelloWorldApplication.java
│   │                   ├── HelloWorldConfiguration.java
│   │                   ├── api
│   │                   │   └── HelloWorld.java
│   │                   └── resources
│   │                   │   └── HelloWorldResource.java
│   │                   └── health
│   │                       └── ConfigHealthCheck.java
│   └── test
│       └── java
│           └── com
│               └── mycompany
│                   └── helloworld
└── target

Let’s add some content to each one of them. Our motive is just to develop a REST API which can print “Hello World” on a “GET” request.

Configuration Class

Each Dropwizard application has its own subclass of the Configuration class which specifies environment-specific parameters. These parameters are specified in a YAML configuration file which is deserialized to an instance of your application’s configuration class and validated.

https://www.dropwizard.io/en/latest/getting-started.html
package com.mycompany.helloworld;

import io.dropwizard.Configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;

public class HelloWorldConfiguration extends Configuration {
    @NotEmpty
    private String defaultMessage = "Hello World!";

    @JsonProperty
    public String getDefaultMessage() {
        return defaultMessage;
    }

    @JsonProperty
    public void setDefaultMessage(String message) {
        this.defaultMessage = message;
    }
}

It is simple Java file, having a class, with one attribute and its corresponding getter and setter. The package on the top defines its parent directory and one new thing to note here is JsonProperty the package which is providing this to us is Jackson and it is already provided by Dropwizard-core from pom.xml.

Now I tried to run mvn compile but I am getting an error:

Could not resolve dependencies for project com.mycompany.helloworld:helloworld:jar:0.0.0: Could not find artifact io.dropwizard:dropwizard-dependencies:jar:2.0.0 in central (https://repo.maven.apache.org/maven2)

Gotcha! Update the pom.xml with compilers and fixed the Dropwizard core artifact ID.

<project>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany.helloworld</groupId>
    <artifactId>helloworld</artifactId>
    <version>0.0.0</version>

    <name>HelloWorld DropWizard</name>

    <properties>
        <dropwizard.version>2.0.0</dropwizard.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.dropwizard</groupId>
            <artifactId>dropwizard-core</artifactId>
            <version>${dropwizard.version}</version>
        </dependency>
    </dependencies>

</project>

Application Class

Combined with your project’s Configuration subclass, its Application subclass forms the core of your Dropwizard application. The Application class pulls together the various bundles and commands which provide basic functionality. (More on that later.) For now, though, our HelloWorldApplication looks like this

https://www.dropwizard.io/en/latest/getting-started.html
package com.example.helloworld;

import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import com.mycompany.helloworld.resources.HelloWorldResource;
import com.mycompany.helloworld.health.ConfigHealthCheck;

public class HelloWorldApplication extends Application<HelloWorldConfiguration> {
    public static void main(String[] args) throws Exception {
        new HelloWorldApplication().run(args);
    }

    @Override
    public String getMessage() {
        return "Hello World Application!!";
    }

    @Override
    public void run(HelloWorldConfiguration configuration,
                    Environment environment) {
        // nothing to do yet
    }

}

But compiling this would now get me error as the Resource and Health Checks Classes are not yet defined.

Representation Class

This would be our API representation. Like in Django we define our Serializers to define the JSON representation.

So we need just a simple JSON representation for our use case:

{
    "id": 1,
    "message": "Hello World!"
}

To create this representation, create the following class in your /api/HelloWorld.java

package com.example.helloworld.api;

import com.fasterxml.jackson.annotation.JsonProperty;

public class HelloWorld {
    private long id;

    private String content;

    public HelloWorld(long id, String content) {
        this.id = id;
        this.content = content;
    }

    @JsonProperty
    public long getId() {
        return id;
    }

    @JsonProperty
    public String getContent() {
        return content;
    }
}

Resource Class

This is going to act like our Flask view where we specify our URL on top of a view with a decorator and write our logic inside that view and corresponding returning responses.

The path to our resource class would be: /src/main/java/com/mycompany/helloworld/resources/HelloWorldResrouce.java

package com.mycompany.helloworld.resources;

import com.mycompany.helloworld.api.HelloWorld;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.concurrent.atomic.AtomicLong;

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
    private final String defaultMessage;
    private final AtomicLong counter;

    public HelloWorldResource(String defaultMessage) {
        this.defaultMessage = defaultMessage;
        this.counter = new AtomicLong();
    }

    @GET
    public HelloWorld sayHello() {
        return new HelloWorld(counter.incrementAndGet(), defaultMessage);
    }
}

Health Class

This is a simple health check that would test whether there is a default value available for message or not. The file would exist at src/main/java/com/mycompany/helloworld/health/ConfigHealthCheck.java

package com.mycompany.helloworld.health;

import com.codahale.metrics.health.HealthCheck;

public class ConfigHealthCheck extends HealthCheck {
    private final String defaultMessage;

    public ConfigHealthCheck(String defaultMessage) {
        this.defaultMessage = defaultMessage;
    }

    @Override
    protected Result check() throws Exception {
        if (this.defaultMessage == null) {
            return Result.unhealthy("Config file doesn't contain key - defaultMessage");
        }
        return Result.healthy();
    }
}

Registering Resources and Healthchecks to your Application class’s run method

Modify the run method. To include our resources and health checks at the run time.

@Override
public void run(HelloWorldConfiguration configuration, Environment environment) {
    final HelloWorldResource resource = new HelloWorldResource(
        configuration.getDefaultName()
    );

    @Override
    public void run(HelloWorldConfiguration configuration, Environment environment) {
        final HelloWorldResource resource = new HelloWorldResource(
                configuration.getDefaultMessage()
        );

        final ConfigHealthCheck healthCheck;
        healthCheck = new ConfigHealthCheck(configuration.getDefaultMessage());
        
        environment.healthChecks().register("defaultMessage", healthCheck);
        environment.jersey().register(resource);
    }

    environment.jersey().register(resource);
}

Config YAML file

Just add a key in it would work.

defaultMessage: "Hello World!"

MVN Compile and MVN package

I compiled the app with mvn compile and then ran mvn package . My build went successful!

Run your API

java -jar target/helloworld-0.0.0.jar

Oh I got the error: no main manifest attribute, in target/helloworld-0.0.0.jar

That is because we need to specify our main class in pom.xml. That main class file is our Application file. So I added the following in pom.xml.

    <properties>
        <dropwizard.version>2.0.0</dropwizard.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <mainClass>com.mycompany.helloworld.HelloWorldApplication</mainClass>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>${mainClass}</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <createDependencyReducedPom>true</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.mycompany.helloworld.HelloWorldApplication</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

Adding these plugins and changes would bring the following functionality as well: refer: #1

  • Produce a pom.xml file which doesn’t include dependencies for the libraries whose contents are included in the fat JAR.
  • Exclude all digital signatures from signed JARs. If you don’t, then Java considers the signature invalid and won’t load or run your JAR file.
  • Collate the various META-INF/services entries in the JARs instead of overwriting them. (Neither Dropwizard nor Jersey works without those.)
  • Set com.example.helloworld.HelloWorldApplication as the JAR’s MainClass. This will allow you to run the JAR using java -jar.

Now try running again mvn package and then java -jar target/helloworld-0.0.0.jar

Oh I got the error because I didn’t specified my config file in the command above.

The final steps

  1. mvn clean
  2. mvn package
  3. java -jar target/helloworld-0.0.0.jar server config.yml

Output

Yups! It is running, verify it by visiting your http://0.0.0.0:8080

I am getting the JSON response on every successive GET request.

{"id":1,"content":"Hello World!"}

Trimming the content

I still think there are lots of places where I have written extra code. Let me trim that all.

Trimmed and refactored the Java files a bit. I think mainly I can reduce the pom.xml. After doing this refactoring I will upload the code for you on GitHub.

In pom.xml: Added encoding properties and xml encoding specification. Also removed maven-jar-plugin, because I want to keep my pom.xml as simple as possible.

The HelloWorld app is available at Here: https://github.com/GeekyShacklebolt/dropwizard-helloworld

Commit hash as per current time is: ce50ec3

Testing the API endpoint

You can test the DropWizard sparingly every part of it individually. As described here: https://www.dropwizard.io/en/release-0.7.x/manual/testing.html

You may refer this one as well: https://nickb.dev/blog/dropwizard-getting-started-testing

We are going to test only serialization and integration.

Create needed test files and folders:

  1. mkdir -p src/test/resources/fixtures
  2. touch src/test/resources/fixtures/helloworld.json
  3. touch src/test/java/com/mycompany/helloworld/HelloWorldTest.java
  4. touch src/test/java/com/mycompany/helloworld/HelloWorldIntegration.java
  5. touch src/test/resources/config.yml

Also, copy contents of your actual config.yml to this test’s config.yml

cat config.yml >> src/test/resources/config.yml

Directory structure of src/test is as follows

src/test
├── java
│   └── com
│       └── mycompany
│           └── helloworld
│               ├── HelloWorldIntegration.java
│               └── HelloWorldTest.java
└── resources
    ├── config.yml
    └── fixtures
        └── helloworld.json

6 directories, 4 files

Add dependency to pom.xml

        <dependency>
            <groupId>io.dropwizard</groupId>
            <artifactId>dropwizard-testing</artifactId>
            <version>${dropwizard.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

       <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.6.2</version>
            <scope>test</scope>
        </dependency>

Run this command to download the newly added packages. mvn clean package

Test Representation (serialization)

Create a demo resource representation of JSON response in helloworld.json

{"id": 1,"content": "Hello World!"}

Write serialization test in a file HelloWorldTest.java

package com.mycompany.helloworld;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mycompany.helloworld.api.HelloWorld;
import io.dropwizard.jackson.Jackson;
import org.junit.Test;

import static io.dropwizard.testing.FixtureHelpers.fixture;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloWorldTest {

    private static final ObjectMapper MAPPER = Jackson.newObjectMapper();

    @Test
    public void serializesToJSON() throws Exception {
        final HelloWorld message = new HelloWorld(1, "Hello World!");
        assertThat(MAPPER.writeValueAsString(message))
                .isEqualTo(fixture("fixtures/helloworld.json"));
    }
}

Since we are not performing any deserialisation, we are not going to test that.

You may run the above test with command mvn test

Integration Test

Write in file HelloWorldIntegration.java

Run the tests with mvn test.

Everything is working fine for me here. I have updated the repository with these latest changes: https://github.com/GeekyShacklebolt/dropwizard-helloworld

Commit hash currently: 9075b14

Well great then! I am leaving this post here 🙂

Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s