My First Ratpack App: What I Learned
Recently I had the opportunity to rewrite an existing Spring Boot application as a Ratpack application at my current client. The application is a sort of security proxy to pass through requests from the wild, untamed internet to sane, internal services. We decided to try Ratpack over Spring Boot for the potential performance gains that Ratpack should provide.
If you’ve been living under a rock and haven’t heard of Ratpack, it’s a collection of libraries for building web applications. It’s not full stack like Grails, so you will need to plug in pretty much anything such as an ORM layer, if you need it. Ratpack will certainly work to build a monolithic application with a frontend, but it seems well suited to a microservice architecture
A big difference between Spring Boot and Ratpack is the threading model. Spring Boot follows the typical thread-per-request model which means that the thread that handles the request also handles all downstream processing. If accessing a database or making an outgoing service call is part of the request, the thread is bound for the whole lifetime of that request, even while waiting for the database or webservice call to return. The threadpool is arbitrarily set to a certain size based on various metrics (or the default value if no performance testing was completed).
Ratpack does things a little differently. The threadpool is sized to the number of processor cores available in the system. This will typically mean a much smaller threadpool than with Spring Boot and Tomcat, for example. When a request comes in, a compute thread handles it. If blocking IO needs to be performed, that work is passed off to an asynchronous blocking thread and the compute thread goes back to handling requests. When the blocking thread is ready, a compute thread picks execution back up from the blocking thread. This model should provide better performance with lower resource utilization.
All my code samples are using Ratpack version 0.9.15 and will be in Groovy, but Java is, of course, supported. Groovy just works so much better. I realize the code samples are short and contrived, but I was trying to keep them short.
To use the latest and greatest versions of Ratpack, you will need Java 8 installed and on your path. Make sure when you run
java -version that version 8 is displayed. Ratpack takes advantage of some of the facilities that Java 8 provides like lambda expressions.
Get a skeleton project setup with Lazybones
Lazybones is a great way to get various applications up and running with little effort. Peter Ledbrook wrote it specifically to bootstrap a Ratpack project and it grew to include other frameworks and project types.
You should be able to point your browser to http://localhost:5050 and see a basic index page indicating everything worked.
When I started up my application, many screenfuls of exceptions are thrown and displayed. For some reason the latest Springloaded libraries didn’t make it up to Bintray JCenter or Maven Central repositories. So if you want to use Springloaded, you will need to add in the Spring repository and change the version of the Springloaded artifact in your build.gradle file:
The Ratpack.groovy file always reloads on code changes, with or without Springloaded. In Intellij Idea, you will need to do a ‘Make Project’ for other code changes to be picked up when using Springloaded.
Reading External Configuration is Easy
Ratpack allows you to load properties from different sources like regular old .properties files to YAML files and even settings from the environment. The settings can be loaded into an object if desired or even into a Map or a String if it’s a basic key/value property. To enable the configuration module, add this line to your gradle.build dependencies: compile ratpack.dependency(‘config’)
Here are some examples of setting up the configurations:
They can all be loaded at once and if a property is found in more than one place, the last one is used:
The .properties file overwrites the property ‘property2.subproperty2’ from the yaml file. Here is the intended output:
I will admit, I never got the environment variable to work. The default is to look for environment variables prefixed with ‘RATPACK_’ and system properties that start with ‘ratpack.’. Let me know if you figure it out.
Different Ways To Use Handlers
Handlers are the entry point into your application code. They may or may not have a URL mapping associated with them. The normal use case is to chain some handlers together into a pipeline to get some work done. Handlers are executed in order and a handler MUST either generate a response or delegate to another handler by calling next() or inserting another handler.
This is a very basic example of using the Groovy DSL in Ratpack.groovy. This will respond to all requests to the application.
If you want to respond only to certain HTTP methods and mappings, here is another simple example:
The above code can be moved out of the Groovy DSL and into a regular class as a handler chain. Here is the Ratpack.groovy file:
Here is the handler chain:
Alternatively, the handlers can be moved into their own classes and then inserted into the pipeline.
If you need more advanced mappings, use path variables and regular expressions:
Guice is Google’s lightweight dependency injection framework. Ratpack has support for Guice built-in if you’d like to use it. It makes most things much easier if you do.
Guice works by using constructor injection with the @Inject annotation (the one from com.google.inject.Inject, not javax.inject.Inject). This is a basic example showing constructor injection. The injected constructor parameters need to be bound in Guice or have a no-argument default constructor.
This example shows the configuration data and MyAppInjectService being bound and then used in the code.
Here is an example of using bindInstance to bind an instance of a service and then using that service in a handler:
Organize Into Modules
You can also load up Guice with modules of classes that contain related functionality. These modules instantiate the objects required to get work done and can be reused across applications.
Modules are also useful for instantiating complex objects that can’t be instantiated with a simple constructor. Guice provides a couple of annotations to help with this (@Provides and @Singleton).
Ratpack comes with a bunch of modules that you can add in if you need their functionality. Some examples include modules for security, database access, metrics, and others. They are defined like this in build.gradle:
A Handler can be used like a Servlet Filter
If the functionality of a servlet filter is needed, just add in another handler at the beginning or end of the chain. For instance, if you need to check that a certain header exists for all requests, do something like this:
If you need to do something special when certain exceptions are thrown, create an error handler and bind it.
It works pretty well to handle them all in a case statement. If you need more complex handling, it might be beneficial to register an error handler for that particular case.
To support it’s asynchronous nature, Ratpack provides the Promise class. One of the main ways to use this asynchronous functionality is when a blocking operation is needed. Blocking operations are things that take a relatively long period of time like database calls, file system operations, or webservice calls. These blocking operations are performed in a separate thread pool from those that accept and process requests. This separate thread pool is the secret to Ratpack’s performance gains over other web frameworks.
To setup a blocking operation in the Groovy DSL, use the blocking method:
To use the blocking threadpool in a service, inject ExecControl into it and call the blocking method on it. Ratpack and Guice provide ExecControl for free without you having to do anything special.
The promise also provides a few methods for doing some processing on the returned results before they are handed back to the calling code. The map method is a great example:
Note that the map() method runs in a compute thread. Check out the API for further methods that can pre-process the Promised value.
Promises are Lazy But Order is Maintained
Promises are not executed right away. They wait to execute until the current code block finishes. The important thing about Promises is that they execute in the order they are defined which allows things to execute in a deterministic way.
The two service calls above don’t really do anything as implemented. They just print out a log statement to prove the order that they executed in. I added in a call to sleep for 5 seconds in the get() method to illustrate this powerful feature of Ratpack. When executed, the println statement at the end is actually executed first, then the call to get (with a 5 second wait) and then the call to save. The code will execute like this IN EVERY REQUEST because Promises execute at the end of the current code block and Promises are executed in the order they are defined. Every time. This leads to completely predictable behavior in Ratpack where in other asynchronous frameworks, you might not know what order things are executed. This allows you to build up pipelines of (potentially) asynchronous processing. Another great feature of this is that the Entertainer variable doesn’t need to be thread-safe since only one thread will be operating on it at a time. That is a huge complexity saver.
Use the Built-In HttpClient (or not)
If you need to make webservice calls, Ratpack comes with a pretty decent HttpClient that returns a Promise out of the box.
In my case, I chose to use the Retrofit OkClient. The reason for that is it’s much easier to mock out in unit and integration tests than the Ratpack HttpClient and it’s much more flexible. I can still put the call on a blocking thread and not lose the asynchronicity.
It, admittedly, took a little bit of thinking (and refactoring) to get my code into what I think is the Ratpack way of doing things. Setting up a pipeline of promises through a series of handlers and knowing that my code is executed in the correct order was a bit of a leap coming from Spring Boot. But now that it is at that point, it makes a lot of sense. While Ratpack might not be full-stack, it certainly has a lot of great features and I hope to explore more of them in the future.