HTTP+JSON Services in Modern Java
March 1, 2013
At Airbnb, we build most of our user facing apps in Ruby on Rails, or more recently Node.js and our own Rendr framework.
We also have a number of internal services, and those are mainly written in Java for stability and performance.
Coming from a Ruby world, building anything in Java can feel pretty painful and boring. But thankfully there are modern Java libraries that make it easy and even fun. We build our Java services with Twitter Commons, a collection of libraries for building HTTP (and other) services. There isn’t much documentation about how to get started with Twitter Commons, so we thought we’d post a little tutorial to help you get started.
Twitter Commons uses Jetty, and provides a lot of the glue and miscellaneous parts of a web service, like logging, statistics, registration, and lifecycle management.
On top of Jetty we use Jersey and Jackson, which is a tried and true combination that is also used by other stacks like Yammer’s Dropwizard.
Jetty is an incredibly fast embeddable web server and servlet container.
Jackson is the de facto standard for fast JSON processing on the JVM.
In addition it also uses Google Guice, the very well-designed dependency injection framework. It takes care of all the wiring in your application and saves you from writing FactoryFactories and other fun things. Dependency injection makes your code modular and easily testable, and Guice makes dependency injection easy to write.
Aside from this combination of great existing tools we also liked the general philosophy of Twitter Commons. It’s a set of libraries you can mix and match, versus an opinionated framework that forces certain patterns on you. That way we remain flexible and can replace parts if we have to as our infrastructure evolves.
We wrote a very basic example service with two endpoints. Clients can like a place they want to travel, and the service can suggest a place to travel based on what users liked. Check it out on Github here: https://github.com/airbnb/twitter-commons-sample
The main entry point is the run method in SuggestionServiceMain. We tell the Twitter AppLauncher about it via the -app_class=com.airbnb.suggest.SuggestionServiceMain command line argument.
Run calls addRestSupport() which configures the servlet context. The main thing here is that we’re serving all requests to /suggest/* via Guice’s servlet integration. We then hand over control to the application lifecycle manager.
Note how the HTTP server, lifecycle manager, and servlet config are given to use by Guice by simply annotating them with @Inject. No need for factories or boilerplate code.
Easy command line arguments are another nice thing provided to us. Just add a static field of type Arg and annotate it with @CmdLine. The rest is taken care of.
The last piece of the main class is the getModules() method, which lists all the Guice modules that make up our application. The modules are where dependencies are configured. HttpModule, LogModule, and StatsModule are provided by Commons. RestModule is where we configure our REST application and its dependencies.
We bind JacksonJsonProvider which adds JSON support to Jersey. We also bind a custom exception mapper that returns exceptions in JSON format so our clients can easily parse them.
Next, we serve all URLs through GuiceContainer, which makes web.xml obsolete and lets us simply bind our Jersey REST resources with Guice instead.
Another really helpful feature of Twitter Commons is built in statistics. We wrote a really simple RequestStatsFilter that measures requests matching a URL pattern and records the stats. You automatically get metrics like throughput and response time percentiles. Just navigate to localhost:8080/stats or localhost:8080/graphview for a real time graphical representation.
Now on to our actual REST endpoints, which are defined in SuggestionResource. Jersey takes care of all the wiring so we can focus on implementing our business logic. We just tell it how to map URLs and parameters to classes and methods by using Java annotations.
SuggestionResource is annotated with @Path and @Produces, which tells Jersey to send all requests for /suggest/v1 to this class, and to map anything that its methods return to JSON.
We have a method like(Place place), annotated with @POST, which is the HTTP method we expect, and another @Path annotation which Jersey appends to the @Path annotation of the class. Jersey takes care of mapping the JSON in the request body to an instance of Place.
The second method is suggest(), which expects a GET request and returns an instance of Place that Jersey again turns into JSON.
The Jersey user guide has documentation on all the other available annotations for different HTTP methods, URL params, path params, and more.
Using the Service