Ratpack

Voor het ontwikkelen van microservices hebben we in het Java-landschap al een hoop keus. Ratpack voegt hier nog een nieuwe optie aan toe. De voornaamste focus van Ratpack is efficiency en daarmee een betere performance. In een cloud-omgeving betalen we tegenwoordig voor wat we gebruiken aan resources. Er zijn betaalmodellen, die kijken naar hoeveel resources (voornamelijk geheugen) bij Java-applicaties worden gebruikt. Hoe minder resources onze HTTP-applicatie gebruikt, hoe lager de kosten. Ratpack streeft naar zo min mogelijk gebruik van resources door een applicatie. In dit artikel gaan we kijken wat Ratpack precies is en hoe we het toe kunnen passen voor het ontwikkelen van microservices over HTTP.

Ratpack maakt gebruik van Netty voor het afhandelen van HTTP requests. Netty is een stabiele library voor snelle en efficiënte IO en maakt gebruik van non blocking threads. Dit betekent dat we een asynchroon model gebruiken voor de code, die we schrijven. Ratpack is dan ook asynchroon (of reactive) vanaf de basis, maar probeert dit wel eenvoudig te maken voor ons als ontwikkelaars om te gebruiken. Ratpack biedt een zogenaamd execution model, waardoor asynchrone code op een deterministische manier uitgevoerd kan worden. We zullen later nog een voorbeeld zien van code, die hier gebruik van maakt.

Voor het afhandelen van een HTTP request maken we in Ratpack een serie van handlers. Elke handler is een stapje, die op meerdere plekken hergebruikt kan worden. In principe bevat een handler code die één ding doet, vergelijkbaar met een pure functie. Door meerdere handlers aan elkaar te knopen, implementeren we onze applicatielogica.

Verder wil Ratpack niet opdringen hoe we onze applicatie schrijven en welke tools we gebruiken. Het is daarmee een “un-opinionated” library, die veel vrijheid biedt.

 

Een simpel begin

Laten we eens gaan kijken naar de code, die we schrijven voor het ontwikkelen van een eenvoudige Ratpack applicatie. Om te beginnen is Ratpack geschreven in Java 8 en is Java 8 ook de minimale Java-versie, die nodig is om de code te schrijven en uit te voeren. Dit kan een beperking zijn in een omgeving waarbij nog geen Java 8 wordt gebruikt, maar aan de andere kant is Java 8 al heel lang beschikbaar en zou iedereen het moeten kunnen gebruiken.

Zoals eerder aangegeven is Ratpack een library en daarmee niet meer dan een dependency, die we moeten toevoegen aan onze build. Dit kan bijvoorbeeld via dependency configuratie met Maven of Gradle. Ratpack biedt voor Gradle ook nog een specifieke plug-in, die het versiemanagement van de Ratpack dependencies eenvoudiger maakt. In Listing 1 zien we een voorbeeld van een Gradle build file voor een Ratpack applicatie:


plugins {
  // Gebruik Ratpack plugin.
  id "io.ratpack.ratpack-java" version "1.3.3"
}
 
repositories {
  // Definieer Bintray's JCenter repository
  // voor het downloaden van dependencies.
  jcenter()
}
 
// Definieer class met de main() method.
mainClassName = 'jdriven.SampleApp'

listing 1

Om een Ratpack applicatie te definiëren, moeten we een Java class maken met een main methode. In de main methode kunnen we gebruik maken van de class RatpackServer om Ratpack op te starten, zodat we via HTTP requests kunnen sturen naar onze applicatie. In Listing 2 zien we de code om een applicatie te maken, die HTTP GET requests afhandelt naar de root (/) van de applicatie en naar /<message>, waarbij message variabel is:


package jdriven;
 
import ratpack.server.RatpackServer;
 
public class SampleApp {
 
  public static void main(String[] args) throws Exception {
    // Start Ratpack.
    RatpackServer.start(server ->
      server.handlers(chain ->
        // Handle HTTP requests.
        chain
          // Toon als antwoord op GET / een simpele tekst.   
          .get(context -> context.render("Sample Ratpack app"))
 
          // Toon als antwoord op GET /hello de tekst
          // You say: hello.
          .get(":message", context -> {
            final String message =
                context.getPathTokens().get("message");
            context.render("You said: " + message + ".");
          })
      )
    );
  }
 
}

listing 2

Nu kunnen we de applicatie opstarten vanuit onze IDE of vanaf de commandline met gradle run en is deze te bereiken via http://localhost:5050.

Naast de ondersteuning van Java 8 biedt Ratpack ook ondersteuning voor Groovy als taal om de applicatie in te schrijven. Ook is het mogelijk om een combinatie van Java en Groovy te gebruiken. Als we Groovy gebruiken, kunnen we net wat compactere code schrijven om dezelfde applicatie te ontwikkelen. In Listing 3 zien we de definitie van onze Ratpack applicatie met Groovy:


import static ratpack.groovy.Groovy.ratpack
 
ratpack {
  handlers {
    get {
      render('Sample Ratpack app')
    }
    get(':message') {
      final String message =
        pathTokens.message
      render("You say: $message.") 
    }
  }   
}

listing 3

 

Werken met handlers

We zien in het simpele voorbeeld dat we een chain object hebben, waarbij we de get methode aanroepen. Het argument van de get methode is een Java lambda expressie als implementatie van de Handler interface. De Handler interface is een functional interface met één methode handle, die als argument een Context object heeft. Het Context object kan gebruikt worden om gegevens door te geven en antwoorden terug te sturen naar de client van de applicatie. Ook goed om te weten is dat naast de get methode de Chain class ook methoden als post, put en delete heeft voor het afhandelen van andere soorten HTTP requests.

In een wat grotere applicatie is het beter om handlers in een eigen class te implementeren in plaats van inline, zoals we zagen in het eerdere voorbeeld. Hierdoor wordt de code beter onderhoudbaar en is het makkelijk om een handler opnieuw te gebruiken. Listing 4 laat de handler implementatie zien van onze simpele Ratpack applicatie:


package jdriven;
 
import ratpack.handling.Context;
import ratpack.handling.Handler;
 
public class SampleHandler implements Handler {
  @Override
  public void handle(final Context context) throws Exception {
    final String message =
        context.getPathTokens().get("message");
    if (message != null) {
      context.render("You say: " + message + ".");
    } else {
      context.render("Sample Ratpack app");
    }
  }
}

listing 4

We passen ook de main class aan, zodat de standalone handler wordt gebruikt in plaats van de lambda expressies (zie Listing 5):


package jdriven;
 
import ratpack.server.RatpackServer;
 
public class SampleApp {
 
  public static void main(String[] args) throws Exception {
    RatpackServer.start(server ->
      server.handlers(chain ->
        chain
          .get(new SampleHandler())
          .get(":message", new SampleHandler())
      )
    );
  }
 
}

listing 5

Er zijn nog meer manieren om handlers te schrijven en gebruiken in Ratpack, maar die zullen we hier niet behandelen. Voor meer informatie kun je terecht op de website van Ratpack http://ratpack.io.

 

Gebruik services via registries

In Ratpack is dependency injection beschikbaar via een zogenaamde registry. Een registry bevat objecten, die we kunnen gebruiken in bijvoorbeeld onze handlers. Via het Context object hebben we altijd toegang tot de registry en de daarbij behorende objecten. Ook hier is te zien dat Ratpack keuzes open wil laten voor ons als ontwikkelaars. De registry kan namelijk geïmplementeerd zijn met Google Guice of met Spring Boot.

Laten we ons simpele voorbeeld uitbreiden en een service toevoegen aan de registry, die een default message terug geeft. De service is beschreven met een interface en we hebben een class, die deze interface implementeert. De code van de interface en implementatie laten we hier nu niet zien, maar het maakt het idee van de registry wel duidelijk. In de handler gebruiken we vervolgens de default message als antwoord naar de gebruiker. We voegen een registry toe aan onze applicatie in de Ratpack server definitie. Er zijn verschillende manieren, maar één van de manieren is om de registryOf methode te gebruiken. In Listing 6 zien we hoe we een implementatie van de MessageService interface toevoegen aan de registry:


package jdriven;
 
import ratpack.server.RatpackServer;
 
public class SampleApp {
 
  public static void main(String[] args) throws Exception {
    RatpackServer.start(server ->
      server
        // Voeg een registry toe.
        .registryOf(registry ->
            // Registreer implementatie van de
            // MessageService interface in de
            // registry.
            registry.add(
                MessageService.class,
                new MessageServiceImpl())
        ) 
        .handlers(chain ->
          chain
            .get(new SampleHandler())
            .get(":message", new SampleHandler())
        )
    );
  }
 
}

listing 6

In onze handler kunnen we dan de MessageService implementatie opvragen en gebruiken (zie Listing 7):


...
final MessageService messageService =
  context.get(MessageService.class);
context.render(messageService.defaultMessage());
...

listing 7

Als we liever Spring Boot gebruiken voor de configuratie van onze componenten, dan kan dat ook. Ratpack heeft de class ratpack.spring.Spring met de methode spring, waarbij we een Spring Boot configuratie kunnen opgeven. Hiermee hebben we dan een registry, die gevuld kan worden met Spring Boot gedefinieerde componenten. We kunnen dus gebruik maken van de autoconfiguratie features van Spring Boot en de standaard Spring configuratiemogelijkheden. Voor de handler code maakt het niet uit hoe het object in de registry komt of hoe de registry wordt gemaakt.

 

Promises: een betere manier voor asynchroon programmeren

Het gebruik van Netty zorgt ervoor dat eigenlijk alles wat we in Ratpack doen asynchroon is. Ook de code, die we zelf schrijven, moet daar rekening mee houden. Het is zo dat Ratpack bij het opstarten een event loop thread opstart om alle requests, die binnenkomen te verwerken. Een kenmerk is dat deze loop non-blocking is. Hierdoor kan één thread heel veel requests afhandelen, omdat er niet gewacht hoeft te worden.

Ratpack biedt de Promise interface om een waarde aan te duiden die later, door asynchrone code berekend, bekend wordt. We kunnen een serie van operaties uitvoeren, zoals transformatie en filtering, met een Promise object. Het gebruik van de Promise API zorgt ervoor, dat onze code geen “callback hell” krijgt, maar Ratpack doet nog meer. Het execution model van Ratpack biedt continuations in combinatie met de Promise API. De operaties worden in volgorde uitgevoerd en een volgende operatie gaat pas aan het werk als de vorige klaar is. Je weet dus altijd zeker dat de data aanwezig is in een operatie. Ratpack heeft dankzij het execution model inzicht in wat er allemaal gebeurt en kan de client dan ook informeren als er geen response mogelijk is, in plaats van dat de client op een timeout wacht. 

Maar het kan zo zijn dat we ook code hebben, die synchroon werkt. Bijvoorbeeld een third-party library, die we willen gebruiken of databasetoegang met een synchrone driver. Dan moeten we aparte maatregelen nemen. Ratpack heeft een groep van threads beschikbaar voor code die synchroon, oftewel blocking, is. Ratpack zal blocking code dan ook uitvoeren op een thread uit deze groep en wanneer het resultaat bekend is weer teruggeven aan de non-blocking thread

Om een blocking call te doen gebruiken we de Blocking class. Deze class heeft static methoden om code uit te voeren, die synchroon is en gebruik moet maken van de blocking threads. Het gebruik hiervan is heel makkelijk en zorgt ervoor dat wij als ontwikkelaars niet zelf met threads aan de gang hoeven te gaan.

In Listing 8 hebben we een interface met methoden, die een Promise teruggeven, zodat we dit kunnen gebruiken in onze Ratpack applicatie:


package jdriven;
 
import ratpack.exec.Promise;
 
public interface BookService {
  Promise<String> findISBN(final String title);
  Promise<Book> findBook(final String isbn);
}

listing 8

Laten we ook een nieuwe handler schrijven, die deze interface gaat gebruiken. Aan de hand van een opgegeven titel zoeken we eerst het ISBN, om vervolgens met het ISBN meer gegevens van het boek op te vragen als een Book object. Uiteindelijk willen we de author property van het Book object in hoofdletters teruggeven aan de client. Deze stappen kunnen we als een serie van operaties definiëren in de handler (zie Listing 9):


package jdriven;
 
import ratpack.handling.Context;
import ratpack.handling.Handler;
 
public class BookHandler implements Handler {
  @Override
  public void handle(Context ctx) throws Exception {
    final BookService bookService = ctx.get(BookService.class);
    final String title = ctx.getPathTokens().get("title");
    bookService
      .findISBN(title)
      .flatMap(isbn -> bookService.findBook(isbn))
      .map(book -> book.getAuthor().toUpperCase())
      // Subscribe to Promise to calculate
      // result with previous operations. 
      .then(author -> ctx.render(author));
  }
}

listing 9

Voor de implementatie van de BookService interface maken we gebruik van een BookRepository, die synchrone toegang tot de database heeft. Daarom gebruiken we de Blocking class om deze BookRepository aan te roepen. In Listing 10 zien we de implementatie:


package jdriven;
 
import ratpack.exec.Blocking;
import ratpack.exec.Promise;
 
import javax.inject.Inject;
 
public class BookImpl implements BookService {
 
  private final BookRepository bookRepository;
     
  @Inject
  public BookImpl(final BookRepository bookRepository) {
    this.bookRepository = bookRepository;
  }
 
  @Override
  public Promise<String> findISBN(final String title) {
    return Blocking.get(() ->
        bookRepository.findISBNByTitle(title));
  }
 
  @Override
  public Promise<Book> findBook(final String isbn) {
    return Blocking.get(() ->
        bookRepository.findByISBN(isbn));
  }
}

listing 10

 

Testen van Ratpack applicaties

Ratpack heeft ook goede support voor het schrijven van testen. Het testen van asynchrone code kan lastig zijn, maar Ratpack heeft verschillende manieren om ook asynchrone code te testen vanuit unit tests. Omdat het opstarten van de applicatie heel snel gaat, zijn integratietesten ook goed te gebruiken.

Voor meer informatie over Ratpack is http://ratpack.io een goede bron. Ook het boek “Learning Ratpack” van Dan Woods (uitgeverij O’Reilly) is een heel goede referentie voor het gebruik van Ratpack.

 

Conclusie

In dit artikel hebben we een aantal features gezien van Ratpack. Het gebruik van handlers om requests af te handelen is erg krachtig, omdat we in deelfunctionaliteiten kunnen denken en deze onafhankelijk implementeren. Daarnaast biedt de Promise API een krachtig middel, samen met het onderliggend execution model, om asynchrone code te schrijven en uit te voeren. Het aanroepen van synchrone code met een blocking thread is ook erg eenvoudig en krachtig.

Door het gebruik van Netty voor de request-afhandeling is het resourcegebruik laag. Dit maakt Ratpack erg geschikt voor het maken van microservices over http, waarbij veel requests worden afgehandeld door de applicatie, die in een cloud-omgeving wordt gedeployed.