Introduction to Ratpack

Przemysław Sobierajski

Ratpack is a set of Java libraries that enable writing efficient HTTP applications. It’s built on Netty event-driven networking engine. In some cases it may be considered as better alternative to Java Servlet technology, since it’s non-blocking and event-driven.

Ratpack’s main goal is to be fast, scalable and efficient. It’s also very lightweight and purely a runtime. Another advantages is that it has its own testing libraries which allow applications to be easily and thoroughly tested. It has Groovy and Java API and support for Guice dependency management. It’s worth to mention that Ratpack has first-class Gradle support provided via the Ratpack’s Gradle plugin. The plugin supports Gradle’s Continuous Build feature. Use it to have changes to your source code be automatically applied to your running application.

Event based HTTP I/O engine

Java Servlet technology assumes that requests are being processed by separate threads. Each I/O operation makes a thread to sleep for some time. It might be inefficient, especially in a high-traffic environment. In such application, the processor wastes memory and time switching between threads.

Ratpack is fully compliant with the reactive design pattern. The constant number of threads work as an Event Loop. The Event Loop waits for and dispatches events. Received HTTP request and prepared answer for this request are examples of an event. Processing events never blocks the loop. That’s the main idea. When the Event Loop receive an event, it dispatch it to some Event Handler immediately and start waiting for new events. Such architecture allows the CPU to be efficiently used.

Hello World example

You need to add the Ratpack dependency to your build.gradle file (you can also use maven):

dependencies {
    compile group: 'io.ratpack', name: 'ratpack-core', version: '1.6.0 '
}

Let’s create a REST Hello World simple application:

public class HelloWorld {

    public static void main(String[] args) throws Exception {
        RatpackServer.start(serverDefinition -> serverDefinition
                .handlers(handler ->
                        handler.path("hello/:name", HelloWorld::sayHello)));
    }

    private static void sayHello(Context ctx) {
        String name = ctx.getPathTokens().get("name");
        ctx.render("Hello " + name);
    }
}

If you run this code and hit the browser at http://localhost:5050/hello/j-labs, then “Hello j‑labs” should be displayed.

In above example there is a handlers() method. It consumes a lambda expression. In the lambda expression you can configure handlers. Handler is responsible for processing requests. It’s assigned to the URL address, HTTP method etc.

Simple CRUD example

The Hello World example has only one handler. Let’s create something more advanced:

public class CrudExample {

    private static final String ID = "id";
    private Collection<Book> booksRepo = new HashSet<>();

    public static void main(String[] args) throws Exception {
        new CrudExample().runServer();
    }

    private void runServer() throws Exception {
        RatpackServer.start(serverDefinition -> serverDefinition
                .handlers(handler -> handler
                        .path("books", ctx -> ctx.byMethod(action -> action
                                .get(this::listBooks)
                                .put(this::saveBook)))
                        .path("books/:" + ID, ctx -> ctx.byMethod(
                                action -> action
                                        .get(this::getBook)
                                        .post(this::updateBook)
                                        .delete(this::removeBook)))));
    }

    private void listBooks(Context ctx) {
        ctx.render(Jackson.json(booksRepo));
    }

    private void saveBook(Context ctx) {
        ctx.parse(Book.class)
                .onError(error -> ctx.getResponse().status(500)
                        .send(error.getMessage()))
                .then(book -> {
                    booksRepo.add(book);
                    respondWith201(ctx, book.id);
                });
    }

    private void respondWith201(Context ctx, long bookId) {
        PublicAddress url = ctx.get(PublicAddress.class);
        ctx.getResponse().getHeaders()
                .set("Location",
                        url.builder().path("books/" + bookId).build());
        ctx.getResponse().status(201).send();
    }

    private void getBook(Context ctx) {
        int id = ctx.getPathTokens().asInt(ID);
        booksRepo.stream()
                .filter(book -> book.id == id)
                .findFirst()
                .ifPresentOrElse(book -> ctx.render(Jackson.json(book)),
                        () -> ctx.getResponse().status(404)
                                .send("Not found"));
    }

    private void updateBook(Context ctx) {
        int id = ctx.getPathTokens().asInt(ID);
        booksRepo.stream().filter(book -> book.id == id)
                .findFirst()
                .ifPresentOrElse(book -> {
                            booksRepo.remove(book);
                            saveBook(ctx);
                        },
                        () -> ctx.getResponse().status(404)
                                .send("Not found"));
    }

    private void removeBook(Context ctx) {
        int id = ctx.getPathTokens().asInt(ID);
        booksRepo.removeIf(book -> book.id == id);
        ctx.getResponse().status(204).send();
    }

    public static class Book {
        private long id;
        private String author;
        private String title;

        //getters, setters and equals/hashCode methods ommited
    }
}

Let me explain the code method by method:

  • runServer() starts HTTP server and defines handler to the chain specified by the given action. It simply says that listBooks() method should process GET request to /books URL, updateBook() method should process POST /books{id} URL etc.
  • listsBook() returns all elements from booksRepo (converted to json with Jackson, which is delivered with ratpack-core.
  • saveBook() parses the json received with PUT request body. In case of failure it responds with 500 status. In case of success it adds book to booksRepo and call respondWith201() method.
  • respondWith201() sends 201 status response. It also build an URL to freshly added book and respond it in response header.
  • getBook() gets id parameter provided in  the request path and respond with the book from the booksRepo or 404 status when the requested book doesn’t exist.
  • removeBook() removes the book from the booksRepo and respond with 204 status code.

Testing

Ratpack allows you to write functional tests easily. With MainClassApplicationUnderTest class is able to set up working application, which you can test by sending real HTTP request.

class CrudExampleTest {

    private static MainClassApplicationUnderTest systemUnderTest;

    @BeforeAll
    static void setUp() {
        systemUnderTest =
                new MainClassApplicationUnderTest(CrudExample.class);
    }

    @AfterAll
    static void tearDown() {
        systemUnderTest.close();
    }

    @Test
    void whenGetWithNonExistingPathThen404() throws Exception {
        systemUnderTest.test(httpClient -> {
            ReceivedResponse response = httpClient.get("nonExistingPath");
            assertThat(response.getStatus()).isEqualTo(Status.NOT_FOUND);
        });
    }

    @Test
    void crudTest() throws Exception {
        Book book = new Book(1, "Mario Puzo", "The Godfather");
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(book);

        systemUnderTest.test(httpClient -> {
            ReceivedResponse response = httpClient.requestSpec(requestSpec ->
                    requestSpec.body(body -> body.type("application/json")
                            .text(json))
            ).put("books");

            assertThat(response.getStatus()).isEqualTo(Status.CREATED);
            assertThat(response.getHeaders().get("Location"))
                    .containsPattern("http://localhost:[0-9]+/books/1");

            response = httpClient.get("books");

            assertThat(response.getStatus()).isEqualTo(Status.OK);
            assertThat(response.getBody().getText()).contains(json);
        });
    }
}

Summary

Ratpack is an example of non-blocking HTTP server. It’s very lightweight, scalable and efficient. It allows you to write a code very fast and test it thoroughly. All this attributes make Ratpack a very good choice for small projects.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami