Welcome to the thirtieth post of 52-technologies-in-2016 blog series. This week I decided to spend time on Dropwizard. Back in 2013, I first learnt about DropWizard when I was exploring Java micro-frameworks. [Dropwizard] impressed me a lot with its opinionated approach to building ops-friendly, high-performance RESTful backends. You can spin up REST backends in a matter of minutes taking your productivity to the next level. Dropwizard is an open source Java framework which was developed by Yammer to power their JVM based backend. It consists of following components :
-
Embedded Jetty : Every application is packaged as a jar instead of war file and starts its own embedded jetty container. There is no WAR file and no external servlet container.
-
JAX-RS : Jersey(the reference implementation for JAX-RS) is used to write RESTful web services. So, your existing JAX-RS knowledge is not wasted.
-
JSON : The REST services speaks JSON. Jackson library is used to do all the JSON processing.
-
Logging : It is done using Logback and SLF4J.
-
Hibernate Validator : Dropwizard uses Hibernate Validator API for declarative validation.
-
Metrics : Dropwizard has inbuilt support for monitoring using the metrics library. It provides unparalleled insight into what our code does in production.
The code for today’s demo application is available on github:
-
Basic Java knowledge is required.
-
Install JDK 8 on your operating system.
-
Download latest version of IntelliJ Idea community edition from the JetBrains official website. IntelliJ Idea community edition is best IDE for Java developers.
Launch IntelliJ and click on Create New Project
. You will see a screen where you will have to select the type of project you want to create.
Press Next
, you will be asked to provide GroupId
and ArtifactId
.
Next, you will be asked to configure few Gradle options. We will enable auto import so that project is kept in sync with build.gradle
.
Finally, you will be asked to provide the name and location of the project. As we are going to create a simple blog application we have used name as blog.
Open build.gradle
and define couple of properties as shown below.
group 'com.shekhargulati'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Now, we will update the build.gradle
to include the dropwizard-core maven dependency. This will download a bunch of dependencies so please remain patient.
def dropwizardVersion = '1.0.0'
dependencies {
compile 'io.dropwizard:dropwizard-core:' + dropwizardVersion
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Every Dropwizard application has one configuration class which specifies the environment specific parameters. Later in this blog post, we will add MongoDB configuration parameters like host, port, and db name to it. These parameters are specified in a YAML configuration file which is deserialized to an instance of your application's configuration class and validated.
Create a new class AppConfiguration
in the blog
package. This class will extend io.dropwizard.Configuration
class.
package blog;
import io.dropwizard.Configuration;
public class AppConfiguration extends Configuration{
}
The Dropwizard project is bootstrapped by an Application
class. This class pulls together various bundles and commands which provide basic functionality. It also starts the embedded jetty server and extends io.dropwizard.Application
.
package blog;
import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
public class BlogApp extends Application<AppConfiguration> {
public static void main(String[] args) throws Exception {
new BlogApp().run("server");
}
@Override
public void run(AppConfiguration configuration, Environment environment) throws Exception {
}
@Override
public void initialize(Bootstrap<AppConfiguration> bootstrap) {
}
}
The application class shown above does the following:
- The class has a main method which acts as our application entry point. Inside the main method, we create an instance of
BlogApp
and call the run method passing itserver
command as its argument. Theserver
command will start the embedded jetty server. - The initialize method is called before running the service run method. Currently, we are not doing anything in the service method.
- Next, we have
run
method which will be called when application runs. Later in this blog, we will add REST resources inside this method.
You can run this application now. Just right click on the BlogApp
class and select Run BlogApp.main
option. This will launch the inbuilt Jetty server and start server at http://localhost:8080/. If you make cURL request to it you will get 404. The reason is that you have not defined any endpoints yet.
$ curl -i http://localhost:8080/
HTTP/1.1 404 Not Found
Date: Mon, 15 Aug 2016 19:37:38 GMT
Content-Type: application/json
Content-Length: 43
{"code":404,"message":"HTTP 404 Not Found"}
Let us write our first resource which will be invoked when a GET request is made to the /
url. Create a new JAX-RS resource as shown below. This resource will list all the blogs.
package blog.resources;
import blog.model.Blog;
import com.codahale.metrics.annotation.Timed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Collections;
import java.util.List;
@Path("/")
public class IndexResource {
@GET
@Produces(value = MediaType.APPLICATION_JSON)
@Timed
public List<Blog> index() {
return Collections.singletonList(
new Blog("Groovy AST Transformations By Example",
"https://github.com/shekhargulati/52-technologies-in-2016/blob/master/32-groovy-ast-transformations/README.md"));
}
}
The code shown above is a standard JAX-RS resource class. It is annotated with @Path
annotation and defines index
method. The index
returns a collection of blogs. These blogs will be converted to JSON document. The @Timed
annotation makes sure that Dropwizard time this method.
The above mentioned IndexResource
used a blog representation. It is shown as below. The Blog
representation uses hibernate validator annotations to make sure the content is valid. For example, we use @URL
annotation to make sure only valid URLs are stored in the MongoDB database.
package blog.model;
import java.util.Date;
import java.util.UUID;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.URL;
public class Blog {
private String id = UUID.randomUUID().toString();
@NotBlank
private String title;
@URL
@NotBlank
private String url;
private final Date publishedOn = new Date();
public Blog() {
}
public Blog(String title, String url) {
super();
this.title = title;
this.url = url;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
public Date getPublishedOn() {
return publishedOn;
}
}
Next, we register IndexResource
in the application class run
method. Update the run method in BlogApp
with the one shown below.
@Override
public void run(AppConfiguration configuration, Environment environment) throws Exception {
environment
.jersey()
.register(new IndexResource());
}
Run the application again by right clicking on the BlogApp
and then selecting Run BlogApp.main
option. This time if you make GET request to http://localhost:8080
you will see our hard coded response.
$ curl http://localhost:8080 |jq '.'
[
{
"id": "ec96b252-8dc7-4c84-b26b-d41dd479e8f7",
"title": "Groovy AST Transformations By Example",
"url": "https://github.com/shekhargulati/52-technologies-in-2016/blob/master/32-groovy-ast-transformations/README.md",
"publishedOn": 1471290772422
}
]
The administrative interface is available at http://localhost:8081/.
We can check the metrics of the IndexResource
by clicking Metrics
. The data is available in JSON format.
"timers": {
"blog.resources.IndexResource.index": {
"count": 9,
"max": 0.011309738000000001,
"mean": 0.00039725750894129734,
"min": 0.000171131,
"p50": 0.00040392600000000004,
"p75": 0.0005585350000000001,
"p95": 0.0005585350000000001,
"p98": 0.0005585350000000001,
"p99": 0.0005585350000000001,
"p999": 0.011309738000000001,
"stddev": 0.0004745239115316346,
"m15_rate": 0.12787342984256184,
"m1_rate": 0.00982297533390726,
"m5_rate": 0.05629074869076141,
"mean_rate": 0.019586528291019135,
"duration_units": "seconds",
"rate_units": "calls/second"
}
}
The above shows that we have accessed IndexResource
9 times.
Add the mongo-jackson-mapper
dependency in build.gradle
.
compile 'net.vz.mongodb.jackson:mongo-jackson-mapper:1.4.2'
Update the AppConfiguration
class with MongoDB database details i.e. host, port, and database name.
package blog;
import io.dropwizard.Configuration;
import org.codehaus.jackson.annotate.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
public class AppConfiguration extends Configuration {
@JsonProperty
@NotEmpty
public String mongohost = "localhost";
@JsonProperty
@Min(2000)
@Max(65535)
public int mongoport = 27017;
@JsonProperty
@NotEmpty
public String mongodb = "blogdb";
}
Next we will create a new class named MongoManaged which allows us to manage resources on application start and stop. This implements io.dropwizard.lifecycle.Managed
.
package blog.mongo;
import com.mongodb.Mongo;
import io.dropwizard.lifecycle.Managed;
public class MongoManaged implements Managed {
private Mongo mongo;
public MongoManaged(Mongo mongo) {
this.mongo = mongo;
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
mongo.close();
}
}
In the code shown above, we close the MongoDB connections in stop method.
Next we will write a MongoHealthCheck
which will check if MongoDB is connected or not. A health check is Dropwizard feature to do a runtime test to verify the service’s behaviour in production environment.
package blog.mongo;
import com.codahale.metrics.health.HealthCheck;
import com.mongodb.Mongo;
public class MongoHealthCheck extends HealthCheck {
private Mongo mongo;
public MongoHealthCheck(Mongo mongo) {
this.mongo = mongo;
}
@Override
protected Result check() throws Exception {
mongo.getDatabaseNames();
return Result.healthy();
}
}
Now we will update the BlogApp
class to include the MongoDB configuration.
package blog;
import blog.mongo.MongoHealthCheck;
import blog.mongo.MongoManaged;
import blog.resources.IndexResource;
import com.mongodb.Mongo;
import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
public class BlogApp extends Application<AppConfiguration> {
public static void main(String[] args) throws Exception {
new BlogApp().run("server");
}
@Override
public void run(AppConfiguration configuration, Environment environment) throws Exception {
Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport);
MongoManaged mongoManaged = new MongoManaged(mongo);
environment
.lifecycle()
.manage(mongoManaged);
environment
.healthChecks()
.register("MongoHealthCheck", new MongoHealthCheck(mongo));
environment
.jersey()
.register(new IndexResource());
}
@Override
public void initialize(Bootstrap<AppConfiguration> bootstrap) {
}
}
In the code shown above :
- A new Mongo instance is created using the
AppConfiguration
object. - A new instance of
MongoManaged
is created and added to the environment. - A health check is added.
Run the application as a main program. You can check if MongoDB is running or not by going to the HealtCheck page loacated at http://localhost:8081/healthcheck. If MongoDB is not running, you will see an exception stacktrace.
$ curl http://localhost:8081/healthcheck | jq '.'
{
"MongoHealthCheck": {
"healthy": false,
"message": "can't call something : Shekhar-2.local/192.18.0.2:27017/admin",
"error": {
"message": "can't call something : Shekhar-2.local/192.168.1.2:27017/admin",
"stack": [
"com.mongodb.DBTCPConnector.call(DBTCPConnector.java:227)",
"com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:305)",
"com.mongodb.DB.command(DB.java:160)",
"com.mongodb.DB.command(DB.java:183)",
"com.mongodb.Mongo.getDatabaseNames(Mongo.java:327)",
"blog.mongo.MongoHealthCheck.check(MongoHealthCheck.java:16)",
"com.codahale.metrics.health.HealthCheck.execute(HealthCheck.java:172)",
"com.codahale.metrics.health.HealthCheckRegistry.runHealthChecks(HealthCheckRegistry.java:77)",
"com.codahale.metrics.servlets.HealthCheckServlet.runHealthChecks(HealthCheckServlet.java:120)",
"com.codahale.metrics.servlets.HealthCheckServlet.doGet(HealthCheckServlet.java:89)",
"javax.servlet.http.HttpServlet.service(HttpServlet.java:687)",
"java.lang.Thread.run(Thread.java:745)"
]
}
},
"deadlocks": {
"healthy": true
}
}
Now, start the MongoDB server and you will see health checks green.
$ curl http://localhost:8081/healthcheck | jq '.'
{
"MongoHealthCheck": {
"healthy": true
},
"deadlocks": {
"healthy": true
}
}
Now, we will write the BlogResource class which will be responsible for creating the blog entries.
package blog.resources;
import blog.model.Blog;
import com.codahale.metrics.annotation.Timed;
import net.vz.mongodb.jackson.JacksonDBCollection;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/blogs")
@Produces(value = MediaType.APPLICATION_JSON)
@Consumes(value = MediaType.APPLICATION_JSON)
public class BlogResource {
private JacksonDBCollection<Blog, String> collection;
public BlogResource(JacksonDBCollection<Blog, String> blogs) {
this.collection = blogs;
}
@POST
@Timed
public Response publishNewBlog(@Valid Blog blog) {
collection.insert(blog);
return Response.noContent().build();
}
}
Next, we will update the BlogApp
run method to add BlogResource
as well.
@Override
public void run(AppConfiguration configuration, Environment environment) throws Exception {
Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport);
MongoManaged mongoManaged = new MongoManaged(mongo);
environment
.lifecycle()
.manage(mongoManaged);
environment
.healthChecks()
.register("MongoHealthCheck", new MongoHealthCheck(mongo));
environment
.jersey()
.register(new IndexResource());
DB db = mongo.getDB(configuration.mongodb);
JacksonDBCollection<Blog, String> blogs = JacksonDBCollection.wrap(db.getCollection("blogs"), Blog.class, String.class);
environment
.jersey()
.register(new BlogResource(blogs));
}
Run the BlogApp
class as a Java application. To test the BlogResource
, make a curl request as shown below:
$ curl -i -X POST -H "Content-Type: application/json" -d '{"title":"Groovy AST Transformations By Example","url":"https://github.com/shekhargulati/52-technologies-in-2016/blob/master/32-groovy-ast-transformations/README.md"}' http://localhost:8080/blogs
Finally, we will update the IndexResource index() method to get all the blog documents from MongoDB.
package blog.resources;
import blog.model.Blog;
import com.codahale.metrics.annotation.Timed;
import net.vz.mongodb.jackson.DBCursor;
import net.vz.mongodb.jackson.JacksonDBCollection;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
@Path("/")
public class IndexResource {
private JacksonDBCollection<Blog, String> collection;
public IndexResource(JacksonDBCollection<Blog, String> blogs) {
this.collection = blogs;
}
@GET
@Produces(value = MediaType.APPLICATION_JSON)
@Timed
public List<Blog> index() {
DBCursor<Blog> dbCursor = collection.find();
List<Blog> blogs = new ArrayList<>();
while (dbCursor.hasNext()) {
Blog blog = dbCursor.next();
blogs.add(blog);
}
return blogs;
}
}
We will update the BlogApp
run method to pass the blogs collection to the IndexResource
.
@Override
public void run(AppConfiguration configuration, Environment environment) throws Exception {
Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport);
MongoManaged mongoManaged = new MongoManaged(mongo);
environment
.lifecycle()
.manage(mongoManaged);
environment
.healthChecks()
.register("MongoHealthCheck", new MongoHealthCheck(mongo));
DB db = mongo.getDB(configuration.mongodb);
JacksonDBCollection<Blog, String> blogs = JacksonDBCollection.wrap(db.getCollection("blogs"), Blog.class, String.class);
environment
.jersey()
.register(new IndexResource(blogs));
environment
.jersey()
.register(new BlogResource(blogs));
}
Run the BlogApp
class as a Java application and test the application.
That's all for this week. Please provide your valuable feedback by adding a comment to shekhargulati#43.