Hands-on with Dropwizard REST APIs

An introduction to the lean REST framework that turns a number of popular Java libraries into a more streamlined alternative to Spring.

Hands-on with Dropwizard REST APIs
Clkr-Free-Vector-Images (CC0)

Dropwizard is a REST-oriented framework that draws together several Java packages into a cohesive whole. It is an alternative to Spring (and Spring’s WebMVC package). Dropwizard delivers a more streamlined experience. It adopts even more configuration by convention than Spring, and eliminates most of the API surface that is unrelated specifically to delivering REST APIs.

Start a new Dropwizard project

Let’s begin by scaffolding a new project via the official Dropwizard Maven archetype. Find a comfortable place on your local system, and from the command line, enter the command in Listing 1.

Listing 1. Run the archetype

mvn archetype:generate -DarchetypeGroupId=io.dropwizard.archetypes -DarchetypeArtifactId=java-simple -DarchetypeVersion=2.0.0

This will run in interactive mode. I used a group ID of com.infoworld and an artifact ID of Demo. I also used Demo for the name.

Once the archetype finishes deploying itself, you can cd into your directory (in my case, cd Demo). Now you install the dependencies with mvn clean package.

Now the app can be run with

java -jar target/Demo-1.0-SNAPSHOT.jar server 

(Remember to use the app name you gave it if different from Demo.)

If you visit localhost:8080 now, you’ll be greeted with a default JSON-formatted “not found” error:

{"code":404,"message":"HTTP 404 Not Found"}

Map an endpoint

So far, the app only returns 404s because no endpoints are mapped. I want to give you a clear understanding of how this is accomplished with Dropwizard. We’ll isolate the process by mapping a simple string endpoint.

Step 1 is to create a class that acts as the path handler. Remember, Dropwizard’s calling in life is to unite several best-in-class libraries into an easy-to-use package. For supporting RESTful endpoints, Dropwizard uses Jersey. Your app already has Jersey fully set up. The path mappings are accomplished with the Jersey syntax.

Step 2 is to register the handler class with the app. Let’s take these two steps in turn.

Create a path handler class

By convention, Dropwizard endpoint handlers go in the /src/main/java/com/infoworld/resources folder (or whatever group ID you choose). We’re going to add an endpoint handler for returning a list of songwriters. As you can see in Listing 2, we’ll return only the best. Create a new SongWriters.java file in the /resources directory.

Listing 2. The SongWriters endpoint

package com.infoworld.resources;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Optional;

@Path("/songwriters")
@Produces(MediaType.APPLICATION_JSON)
public class SongWriters {
    public SongWriters() {
    }

    @GET
    public String getBrands() {
        return "{'name':'Roger Waters','name':'Tom Petty'}";
    }
}

Listing 2 is fairly straightforward. The class itself is given a path via the @Path annotation, and JSON is specified as the media type via the @Produces annotation. Next, the getBrands() method, which returns a string, is mapped to the GET HTTP method via the @GET annotation. The method just returns a hard-coded string representing the set of songwriters.

Register the handler

Now to register the handler. Open the /src/main/java/com/infoworld/DemoApplication.java file and modify the run() method to look like Listing 3.

Listing 3. Registering the SongWriters handler

import com.infoworld.resources.SongWriters;
// ...
public void run(final DemoConfiguration configuration, final Environment environment) {
        SongWriters songWriters = new SongWriters();
        environment.jersey().register(songWriters);
    }

Again, fairly straightforward. Listing 3 imports the class you just created, then uses the Environment object to access Jersey and register the handler. Notice the run method also receives a DemoConfiguration object, which you’ll see in action momentarily.

Test the handler

Now if you visit localhost:8080/songwriters, you’ll get the JSON that you hard coded. Notice the system has seamlessly handled returning the string as JSON.

Model the domain

Now you want to move from a hard-coded string to using actual domain objects. Start by creating a /src/main/java/com/infoworld/domain/SongWriter.java file, and give it the contents of Listing 4.

Listing 4. The SongWriter domain model class

package com.infoworld.domain;

import java.util.ArrayList;
import java.util.List;

public class SongWriter {
    private final String name;
    private List songs = new ArrayList<>();

    public SongWriter(final String name, final List songs) {
        this.name = name;
        this.songs = songs;
    }

    public String getName() {
      return name;
    }

    public List getSongs(){
      return this.songs;
    }
}

Add a repository class

Now we’ll add a repository class to represent the data access layer. Create a new folder and file: /src/main/java/com/infoworld/repo/SongWriterRepo.java.

Listing 5. The SongWriterRepo class

package com.infoworld.repo;

import com.infoworld.domain.SongWriter;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.stream.Collectors;

public class SongWriterRepo {
  private final List songWriters;
  public SongWriterRepo(){
    this.songWriters = new ArrayList();     
  }

  public SongWriterRepo(List songWriters){
    this.songWriters = songWriters;
  }

  public List findAll(){
    return songWriters;
  }

  public Optional getByName(final String name){
    return songWriters.stream().filter(sw -> sw.getName().equals(name)).findFirst();
  }
}

As you can see in Listing 5, SongWriterRepo is responsible for returning all the SongWriter objects in a List via the findAll() method, while the getByName() method grabs the songwriter specified by the passed in string name.

Use the domain layer

Now we’ll use the new layer to drive the JSON returned by the resource endpoint. Modify com.infoworld.resource.SongWriters to look like Listing 6.

Listing 6. The modified SongWriters class

package com.infoworld.resources;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Optional;

import com.infoworld.repo.SongWriterRepo;
import com.infoworld.domain.SongWriter;

@Path("/songwriters")
@Produces(MediaType.APPLICATION_JSON)
public class SongWriters {
   private final SongWriterRepo repo;
    public SongWriters(SongWriterRepo repo) {
      this.repo = repo;
    }

    @GET
    public List getBrands() {
        return repo.findAll();
    }
}

Listing 6 has eliminated the hard-coded string in favor of referencing the SongWriterRepo class, which is set in the constructor. Now take a look at Listing 7, where these classes are wired together in the DemoApplication class.

Listing 7. Wiring the domain class in DemoApplication

package com.infoworld;

import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;

import com.infoworld.resources.SongWriters;
import com.infoworld.repo.SongWriterRepo;

public class DemoApplication extends Application {
// ...
    @Override
    public void run(final DemoConfiguration configuration, final Environment environment) {
        SongWriterRepo swRepo = new SongWriterRepo();
        SongWriters songWriters = new SongWriters(swRepo);
        environment.jersey().register(songWriters);
    }
}

Return to the DemoApplication class, and modify it as shown in Listing 7. Notice that you now instantiate the SongWriterRepo object and parameterize the SongWriters resource object before handing it off to Jersey.

Now if you rebuild the project and visit localhost:8080/songwriters, you will find an empty array. Let’s add a hard-coded songwriter as shown in Listing 8.

Listing 8. Adding a songwriter

public void run(final DW2Configuration configuration,
                    final Environment environment) {

        List preload = new ArrayList();
        preload.add(new SongWriter("Mark Knopfler", new ArrayList()));

        SongWriterRepo swRepo = new SongWriterRepo(preload);
        SongWriters songWriters = new SongWriters(swRepo);
        environment.jersey().register(songWriters);
    }

Now you’ll get a result when you hit the endpoint.

Use the config file

Add a new config file at src/main/resources/Demo-Config.yml and put the contents of Listing 9 in it.

Listing 9. The config YAML

---
songWriters:
  - John Lennon
  - Jeff Lynne

Now change the com.infoworld.DemoConfiguration.java file to look like Listing 10. This file is used to model the config file via Jackson.

Listing 10. Config java

package com.infoworld;

import io.dropwizard.Configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.*;
import javax.validation.constraints.*;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.List;
import java.util.ArrayList;

public class DW2Configuration extends Configuration {
  private List<String> songWriters;
  @JsonCreator
  public DW2Configuration(@JsonProperty("songWriters") List<String> songWriters){
          this.songWriters = songWriters;
  }
  public List<String> getSongWriters(){
          return this.songWriters;
  }
}

Now use the file in DemoApplication, as demonstrated in Listing 11.

Listing 11. Using the config file to preload songwriters in DemoApplication.java

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

        List preload = new ArrayList();

        configuration.getSongWriters().forEach(sw -> preload.add(new SongWriter(sw, new ArrayList())));
        SongWriterRepo swRepo = new SongWriterRepo(preload);
        SongWriters songWriters = new SongWriters(swRepo);
        environment.jersey().register(songWriters);
    }

The main point of the change in Listing 11 is that the configuration file is hydrated into the DemoConfiguration class, populating the getSongWriters() method that is used to preload the repository class.

Run the app with the config file

A small change is required to run this app with the config file. Notice in Listing 12 that the file is referenced after the call to server.

Listing 12. Running the app with the config file

java -jar target/DW2-1.0-SNAPSHOT.jar server src/main/resources/DW2-config.yml

Now when you visit the app at localhost:8080/songwriters, you’ll see the preloaded songwriters.

Dropwizard is a lean alternative to Spring-based RESTful APIs. As you’ve seen here, it focuses on providing a simple but sufficient stack to address these demands.

The sources for the demo app are available here.

Copyright © 2021 IDG Communications, Inc.