Java URL shortener service with Dropwizard and Redis

It never occurred to me I should write a Java URL shortener service with Dropwizard and Redis. Not because of Dropwizard or Redis, I just didn’t see why I should write a Java URL shortener at all.

That is until I read the blog post URL shortener service in 42 lines of code in… Java (?!) Spring Boot + Redis by Tomasz Nurkiewicz. Tomasz also references other interesting implementations of URL shortener services written in Scala, Clojure or Haskell.

Java URL shortener service with Dropwizard and Redis
Dropwizard logo

I knew from the start that an implementation based on Dropwizard would probably take a few more lines than one based on Spring Boot. Saving LoCs at all costs wasn’t the primary goal, though. I wanted to complete this exercise because I only recently wrote my first Dropwizard application – and because it was a very pleasant experience.

Somewhere I read a comment about Spring Boot which quite nicely expresses the thoughts I had when I first learned about its existence.

In short, this IS Dropwizard in every way. Spring 4 tries to become Dropwizard (and succeeds), JEE7 tries to become Spring 3 and kind of gets there. Everyone is becoming everyone else.

Eventhough Dropwizard could be tweaked to work with Spring (e.g. Spring Data Redis) this isn’t an option for me. If I want Spring, which I usually do, I’ll go with Spring Boot. Hence, this is pure JAX-RS with Dropwizard/Jersey and Jedis, a popular Java client for Redis.

The code

So, here’s the final solution on some 50 lines. Find a few comments below.

import com.google.common.hash.Hashing;
import io.dropwizard.*;
import io.dropwizard.setup.*;
import org.apache.commons.validator.routines.UrlValidator;
import redis.clients.jedis.*;

import javax.servlet.http.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;

@Path("/")
public class UrlShortener extends io.dropwizard.Application<Configuration> {
  private static JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  public static void main(String[] args) throws Exception {
    new UrlShortener().run(args);
  }

  @Override
  public void initialize(Bootstrap<Configuration> configurationBootstrap) {}

  @Override
  public void run(Configuration configuration, Environment environment) throws Exception {
    environment.jersey().register(this);
  }

  @GET
  @Path("/{id}")
  public void redirect(@PathParam("id") String id, @Context HttpServletResponse response) throws IOException {
    try (Jedis jedis = pool.getResource()){
      String url = jedis.get(id);
      if (url == null) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
      } else {
        response.sendRedirect(url);
      }
    }
  }

  @POST
  public Response save(@QueryParam("url") String url, @Context HttpServletRequest request) throws MalformedURLException {
    if (new UrlValidator(new String[]{"http", "https"}).isValid(url)) {
      final String id = Hashing.murmur3_32().hashString(url, StandardCharsets.UTF_8).toString();
      try(Jedis jedis = pool.getResource()){
        jedis.set(id, url);
      }
      return Response.ok(new URL(request.getScheme(), request.getServerName(), request.getServerPort(), "/" + id).toString().replace(":80/", "/")).build();
    } else {
      return Response.status(Response.Status.BAD_REQUEST).build();
    }
  }
}

Line 17

private static JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");

The Jedis Wiki clearly states that “You shouldn’t use the same instance from different threads because you’ll have strange errors…To avoid these problems, you should use JedisPool, which is a threadsafe pool of network connections…You can store the pool somewhere statically, it is thread-safe.”

Line 22-23

@Override
public void initialize(Bootstrap<Configuration> configurationBootstrap) {}

This method is required by the Dropwizard Application class even though it does nothing. Also note that contrary to bigger Dropwizard applications which rely on configuration options there’s no need to implement your own Configuration sub class.

Line 33/47

try (Jedis jedis = pool.getResource()){

For each Redis operation one should obtain a Jedis instance from the pool in a try-with-resources statement.

Line 44

public Response save(@QueryParam("url") String url, ...

Contrary to Tomasz solution I opted to POST the URL to shorten as a query parameter rather than a path parameter. I’m aware of a number of heated debates around the Internet how to do this “properly” with regards to REST principles and the HTTP spec.

Line 45

if (new UrlValidator(new String[]{"http", "https"}).isValid(url)) {

If you want to avoid the external dependency to UrlValidator you could do something like

url.startsWith("http") && new java.net.URL(url)

instead. However, be aware that you’d then have to handle the checked MalformedURLException declared by the URL constructor.

The Maven build

The pom.xml declares only 4 dependencies

<properties>
  <commons-validator.version>1.4.0</commons-validator.version>
  <dropwizard.version>0.7.1</dropwizard.version>
  <guava.version>17.0</guava.version>
  <jedis.version>2.5.1</jedis.version>
</properties>

<dependencies>
  <dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-core</artifactId>
    <version>${dropwizard.version}</version>
  </dependency>
  <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${guava.version}</version>
  </dependency>
  <dependency>
    <groupId>commons-validator</groupId>
    <artifactId>commons-validator</artifactId>
    <version>${commons-validator.version}</version>
  </dependency>
  <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${jedis.version}</version>
  </dependency>
</dependencies>

If you use the Maven Shade Plugin as recommended by Dropwizard you can build a single fat JAR which contains all dependencies listed above.

Conclusion

Building a Java URL shortener service with Dropwizard and Redis is not difficult and you won’t have to sacrifice readability. For me this has always been way more important than saving characters or lines while coding.

Leave a Reply