I recently started playing with Ratpack.
It's a set of Java libraries for buiding modern HTTP applications.
To go beyond that simple definition, Ratpack is non-blocking, asynchronous and netty-based. A bit like our Java SDK. So it comes as a natural candidate for a lightweight application framework.
One of the first thing you can do when testing a new web framework is create a basic API. And building a simple CRUD comes naturally when you work with a document database like Couchbase. So this post is a basic intro to Ratpack showcasing the Couchbase entity repository abstraction available with our Java SDK. I will not focus on the API itself here, but more on the mechanism used in Ratpack and the Couchbase repository.
Creating a Ratpack Java Project
Most of the Ratpack users I know tend to code with Groovy. I am usually coding in Java so this is what I am going to use today. The quickstart guides you'll find for Ratpack will feature Groovy though. The lazybones template is a Groovy one for instance. So I started by generating a simple Java Gradle project in my favorite IDE. Then I opened the build.gradle
file and completed it with the right dependencies. I added the jcenter repository, a dependency to ratpack-gradle 1.3.3 and made sure I was using the io.ratpack.ratpack-java
plugin. And let's not forget the Couchbase dependency.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
buildscript { repositories { jcenter() } dependencies { classpath "io.ratpack:ratpack-gradle:1.3.3" } } apply plugin: "io.ratpack.ratpack-java" apply plugin: 'eclipse' sourceCompatibility = 1.8 version = '1.0' jar { manifest { attributes 'Implementation-Title': 'Ratpack Couchbase Repository Sample', 'Implementation-Version': version } } repositories { mavenCentral() jcenter() } dependencies { compile "com.couchbase.client:java-client:2.2.5" } mainClassName = "org.couchbase.devex.Application" test { systemProperties 'property': 'value' } |
The next step is to start the app with a simple Hello World. I have an Application class as you can see on the build file above with the mainClassName
option. All I am going to do here for starter is spin up the Ratpack server and register the '/hello' URL path to return a Hello World message.
Ratpack has Handlers objects. A Handler is a function with a Context as parameter. You can associate a handler with a path. So here I am going to associate a Handler function to the '/hello' path. To understand Ratpack you have to understand handlers as you will use them in all your applications.
The entrypoint in a Ratpack application is the RatpackServer class. From this class you can use the static method start
. It takes a function with a RatpackServerSpec object as parameter. This object gives you a fluent API to configure the server. The only thing I need to do here is add my handler. The handlers
method takes a function with a Handler Chain as parameter. From that chain I can create my handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package org.couchbase.devex; import ratpack.server.RatpackServer; public class Application { public static void main(String... args) throws Exception { RatpackServer .start(server -> server.handlers(chain -> chain.path("hello", ctx -> ctx.render("Hello World!")))); } } |
When I start the application and go to localhost:5050/hello, I get my hello message. Moving on to the Couchbase part.
The Couchbase Entity Model
The entity model appeared with version 2.2.x. You can declare entity easily using the Couchbase annotation @Id. You also need to make sure that your entity class has a public, zero arg constructor and that setter and getter are available for the fields you want in the resulting JSON document. To model a user with a username, age, first name and last name, I use the following User class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
package org.couchbase.devex; import com.couchbase.client.java.repository.annotation.Field; import com.couchbase.client.java.repository.annotation.Id; public class User { @Id private String username; private Integer age; @Field("fName") private String firstName; @Field("lName") private String lastName; public User() {} public User(String username, Integer age, String firstName, String lastName) { this.username = username; this.age = age; this.firstName = firstName; this.lastName = lastName; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Override public String toString() { return "User [username=" + username + ", age=" + age + ", firstName=" + firstName + ", lastName=" + lastName + "]"; } } |
The @Id annotation is mandatory and defines the field that will be used as key of your document. You can use the @Field annotation to change the name of the properties in your resulting JSON document. Note also that the @Id field won't be in the JSON.
This Entity class has to be used with the EntityDocument. It's very similar in use with the other document implementations available with the SDK. The following code will create a JSON document in Couchbase:
1 2 3 4 5 |
User user = new User("ldoguin", 31, "Laurent", "Doguin"); EntityDocument<User> document = EntityDocument.create(user); Repository repo = CouchbaseCluster.create().openBucket().repository(); repo.upsert(document); |
Which create the following document:
Key: ldoguin
Value: { “lName”: “Doguin”, “age”: 31, “fName”: “Laurent” }
This code is currently blocking and synchronous. Is this a bad thing? It depends :) If you are running blocking code in a non-blocking thread, like you would do if it was running in place of the hello world above, than you are blocking everything else. And yes that would be a bad thing. Because this thread manages the event loop that handles all the requests. I found this blog post quite useful to understand this.
One of Ratpack's goal is to help you use blocking code in an async, non-blocking handler. Ratpack uses Promise as a way to manage asynchronous code. And it also gives you a Blocking primitive that takes a function as argument and return a promise. The function will execute the blocking code. Here I have an example that creates a user when hitting the localhost:5050/create URL and returns it when hitting the localhost:5050/get URL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static void main(String... args) throws Exception { RatpackServer.start(server -> server.handlers(chain -> chain.path("create", ctx -> { Blocking.get(() -> { EntityDocument<User> document = EntityDocument.create(new User("ldoguin", 31, "Laurent", "Doguin")); Repository repo = CouchbaseCluster.create().openBucket().repository(); return repo.upsert(document); }).then(entityDoc -> ctx.render("OK")); }).path("get", ctx -> { Blocking.get(() -> { Repository repo = CouchbaseCluster.create().openBucket().repository(); EntityDocument<User> ldoguin = repo.get("ldoguin", User.class); return ldoguin; }).then(user -> ctx.render(user.content().toString())); }) )); } |
Taking aside the fact that this is probably the worst designed API ever, this should give you an idea on how you can run blocking, synchronous code in an async handler with Ratpack. Which is the first step to migrate an existing application to Ratpack.
But then this example does not really showcase all the goodness that is our RxJava based SDK. So in the next post I will show you how to use RxJava and Ratpack together with the Async version of the Repository.