© Adam L. Davis 2016

Adam L. Davis, Learning Groovy, 10.1007/978-1-4842-2117-4_15

15. Ratpack

Adam L. Davis

(1)New York, USA

At its core, Ratpack1 enables asynchronous, stateless HTTP applications. It is built on Netty, the event-driven networking engine. Unlike some web frameworks, there is no expectation that one thread handles one request. Instead, you are encouraged to handle blocking operations in a way that frees the current thread, thus allowing high performance.

Ratpack can be used to make responsive, RESTful microservices , although it’s not a requirement.

REST stands for REpresentational State Transfera. It was designed in a PhD dissertation and has gained some popularity as the new web service standard. At the most basic level in REST, each CRUD operation is mapped to an HTTP method (GET, POST, PUT, and so on).

a http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

Unlike Grails and other popular web frameworks, Ratpack aims not to be a framework, but instead a set of libraries. Although it’s a lean set of libraries, Ratpack comes packed with support for JSON, websockets, SSE (Server Sent Events), SSL, SQL, logging, Dropwizard Metrics, newrelic, health checks, hystrix, and more.

Ratpack uses Guice by default for DI (dependency injection ).

A426440_1_En_15_Figa_HTML.jpgTip

Ratpack is written in Java (8) and you can write Ratpack applications in pure Java, but we are focusing on the Groovy side.

Script

In its simplest form, you can create a Ratpack application using only a Groovy script. For example:

1   @Grab('io.ratpack:ratpack-groovy:1.1.1')
2   
3   import static ratpack.groovy.Groovy.ratpack
4   
5   ratpack {
6     handlers {
7       handler {
8         response.send "Hello World!"
9       }
10     }
11   }

Gradle

For production systems you should use a Gradle build. Ratpack has its own Gradle plugin that you can use, as follows (jcenter refers to the Maven central alternative, Bintray’s JCenter):

1   buildscript {
2     repositories {
3       jcenter()
4     }
5     dependencies {
6       classpath 'io.ratpack:ratpack-gradle:1.1.1'
7     }
8   }
9   
10   apply plugin: 'io.ratpack.ratpack-groovy'
11   
12   repositories {
13     jcenter()
14   }

Using the ratpack-gradle plugin, you can run tasks such as distZip, distTar, installApp, and run which create a ZIP distribution file, TAR distribution file, install the Ratpack application locally, and run the application respectively .

The run task is very useful for test driving your application. After invoking gradle run, you should see the following output:

1   [main] INFO ratpack.server.RatpackServer - Starting server...
2   [main] INFO ratpack.server.RatpackServer - Building registry...
3   [main] INFO ratpack.server.RatpackServer - Ratpack started (development)
4   for http://localhost:5050

Ratpack Layout

Unlike in Grails, you need to create these files and directories yourself (or use Lazybones).

  • build.gradle (the Gradle build file)

  • src

main/groovy—Where you put general Groovy classes

main/resources—Where you put static resources such as Handlebars templates

ratpack—Contains Ratpack.groovy, which defines your Ratpack handlers and bindings and ratpack.properties

ratpack/public—Any public files like HTML, JavaScript, and CSS

ratpack/templates—Holds your Groovy-Markup templates (if you have any)

Handlers

Handlers are the basic building blocks for Ratpack. They form something like a pipeline or “Chain of Responsibility”2. Multiple handlers can be called per request, but one must return a response. If none of your handlers is matched, a default handler returns a 404 status code.

1   import static ratpack.groovy.Groovy.ratpack
2   
3   ratpack {
4     handlers {
5       all() { response.headers.add('x-custom', 'x'); next() }
6       get("foo") {
7           render "foo handler"
8       }
9       get(":key") {
10           def  key = pathTokens.key
11           render "{"key": "$key"}"
12       }
13       files { dir "public" }
14     }
15   }

The first handler all() is used for every HTTP request and adds a custom header. It then calls next() so the next matching handler gets called. The next handler that matches a given HTTP request will be used to fulfill the request with a response.

The pathTokens map contains all parameters passed in the URL as specified in the matching path pattern, as in :key, by prefixing the : to a variable name you specify.

You can define a handler using a method corresponding to any one of the HTTP methods :

  • get

  • post

  • put

  • delete

  • patch

  • options

To accept multiple methods on the same path, you should use the path handler and byMethod with each method handler inside it. For example:

1   path("foo") {
2       byMethod {
3           post() { render 'post foo'}
4           put() { render 'put foo'}
5           delete() { render 'delete foo'}
6       }
7   }

Rendering

There are many ways to render a response. The “grooviest” ways are using the groovyMarkupTemplate or groovyTemplate methods.

For example, here’s how to use the groovyMarkupTemplate:

1   import   ratpack.groovy.template.MarkupTemplateModule          
2   import   static   ratpack.groovy.Groovy.groovyMarkupTemplate
3   import static ratpack.groovy.Groovy.ratpack
4   ratpack {
5     bindings {
6       module MarkupTemplateModule
7     }
8     handlers {
9       get(":key") {
10           def  key = pathTokens.key
11           render groovyMarkupTemplate("index.gtpl", title: "$key")
12       }
13       files { dir "public" }
14     }
15   }

This allows you to use the Groovy Markup language and, by default, it looks in the ratpack/templates directory for markup files. For example, your index.gtpl might look like the following :

1   yieldUnescaped '<!DOCTYPE html>'
2   html {
3     head {
4       meta(charset:'utf-8')
5       title("Ratpack: $title")
6       meta(name: 'apple-mobile-web-app-title', content: 'Ratpack')
7       meta(name: 'description', content: '')
8       meta(name: 'viewport', content: 'width=device-width, initial-scale=1')
9       link(href: '/images/favicon.ico', rel: 'shortcut icon')
10     }
11     body {
12       header {
13         h1 'Ratpack'
14         p 'Simple, lean &amp; powerful HTTP apps'
15       }
16       section {
17         h2 title
18         p 'This is the main page for your Ratpack app.'
19       }
20     }
21   }

Groovy Text

If you prefer to use a plain old text document with embedded Groovy (much like a GString), you can use the TextTemplateModule:

1   import  ratpack.groovy.template.TextTemplateModule            
2   import static ratpack.groovy.Groovy.groovyTemplate
3   import static ratpack.groovy.Groovy.ratpack
4   ratpack {
5     bindings {
6       module TextTemplateModule
7     }
8     handlers {
9       get(":key") {
10           def  key = pathTokens.key
11           render groovyTemplate("index.html", title: "$key")
12       }
13     }
14   }

Then create a file named index.html in the src/main/resources/templates directory with the following content:

1   <html><h1>${model.title}</h1></html>

It supplies a model map to your template, which contains all of the parameters you supply .

Handlebars and Thymeleaf

Ratpack also supports Handlebars and Thymeleaf templates, two alternative methods of generating dynamic web pages. Since these are static resource, you should put these templates in the src/main/resources directory.

You will first need to include the appropriate Ratpack project in your Gradle build file:

1   runtime 'io.ratpack:ratpack-handlebars:1.1.1'
2   runtime 'io.ratpack:ratpack-thymeleaf:1.1.1'

To use Handlebars, include the HandlebarsModule and render Handlebar templates as follows:

1   import ratpack.handlebars.HandlebarsModule              
2   import   static   ratpack.handlebars.Template.handlebarsTemplate
3   import static ratpack.groovy.Groovy.ratpack
4   ratpack {
5     bindings {
6       module HandlebarsModule
7     }
8     handlers {
9       get("foo") {
10           render handlebarsTemplate('myTemplate.html', title: 'Handlebars')
11       }
12     }
13   }

Create a file named myTemplate.html.hbs in the src/main/resources/handlebars directory with Handlebar content. For example:

1   <span>{{title}}</span>

Thymeleaf works in a similar way:

1   import ratpack.thymeleaf.ThymeleafModule              
2   import   static   ratpack.thymeleaf.Template.thymeleafTemplate
3   import static ratpack.groovy.Groovy.ratpack
4   ratpack {
5     bindings {
6       module ThymeleafModule
7     }
8     handlers {
9       get("foo") {
10           render thymeleafTemplate('myTemplate', title: 'Thymeleaf')
11       }
12     }
13   }

The requested file should be named myTemplate.html and be located in the src/main/resources/thymeleaf directory of your project. Example content:

1   <span th:text="${title}"/>

JSON

Integration with the Jackson JSON marshaling library provides the ability to work with JSON as part of ratpack-core.

The ratpack.jackson.Jackson class provides most of the Jackson-related functionality. For example, to render JSON, you can use Jackson.json:

1   import  static  ratpack.jackson.Jackson.json
2   ratpack {
3     bindings {
4     }
5     handlers {
6       get("user") {
7         render json([user: 1])
8       }
9     }
10   }

The Jackson integration also includes a parser for converting JSON request bodies into objects. The Jackson.jsonNode() and Jackson.fromJson(Class) methods can be used to create parseable objects to be used with the parse() method.

For example, the following handler would parse a JSON string representing a Person object and render the Person's name:

1   post("personNames") {
2     render( parse(fromJson(Person.class)).map {it.name} )
3   }
A426440_1_En_15_Figa_HTML.jpgTip

Context.parse returns a ratpack.exec.Promise. A Promise is a commonly used pattern in concurrent programming that allows a simplified syntax. It implements methods such as map as shown previously.

Bindings

Bindings in Ratpack make objects available to the handlers. If you are familiar with Spring, Ratpack uses a registry, which is similar to the application context in Spring. It can also be thought of as a simple map from classtypes to instances. Ratpack-Groovy uses Guice by default, although other direct-injection frameworks can be used (or none at all).

Instead of a formal plugin system, reusable functionality can be packaged as modules. You can create your own modules to properly decouple and organize your application into components. For example, you might want to create a MongoModule or a JdbcModule. Alternatively, you could break your application into services. Either way, the registry is where you put them.

Anything in the registry can be automatically used in a handler and gets wired in by classtype from the registry. Here’s an example of bindings in action:

1   bindings {
2     bindInstance(MongoModule, new   MongoModule())
3     bind(DragonService, DefaultDragonService)
4   }
5   handlers {
6     get('dragons') { DragonService dService ->
7       dService.list().then { dragons ->
8         render(toJson(dragons))
9       }
10     }
11   }

Blocking

Blocking operations should be handled by using the blocking API. A blocking operation is anything that is IO-bound, such as querying the database.

The Blocking class is located at ratpack.exec.Blocking. For example, the following handler calls a method on the database :

1   get("deleteOlderThan/:days") {
2     int days = pathTokens.days as int
3     Blocking.get { database.deleteOlderThan(days) }
4                  .then { int i -> render("$i records deleted") }
5   }

You can chain multiple blocking calls using the then method . The result of the previous closure is passed as the parameter to the next closure (an int above).

A426440_1_En_15_Figa_HTML.jpg Promise

Ratpack makes this possible by using its own implementation of a promise. Blocking.get returns a ratpack.exec.Promise.

Ratpack handles the thread scheduling for you and then joins with the original computation thread. This way, you can rejoin with the original HTTP request thread and return a result.

1   get("deleteOlderThan/:days") {
2     int days = pathTokens.days as int
3     int result
4     Blocking.get { database.deleteOlderThan(days) }
5                  .then { int count -> result = count }
6     render("$result records deleted")
7   }

If no return value is required, use the Blocking.exec method.

Configuration

Any self-respecting web application should allow configuration to come from multiple locations: the application, the filesystem, environment variables, and system properties. This is a good practice in general, but especially for cloud-native apps.

Ratpack includes a built-in configuration API. The ratpack.config.ConfigData class allows you to layer multiple sources of configuration. Using the of method with a passed closure allows you to define a factory of sorts for configuration from JSON, YAML, and other sources.

First, you define your configuration classes. These class properties will define the names of your configuration properties. For example :

1   class Config {
2       DatabaseConfig database
3   }
4   class  DatabaseConfig  {
5       String username = "root"
6       String password = ""
7       String hostname = "localhost"
8       String database = "myDb"
9   }

In this case, your JSON configuration might look like:

1   {
2       "database": {
3           "username":  "user",
4           "password":  "changeme",
5           "hostname":  "myapp.dev.company.com"
6       }
7   }

Later, in the binding declaration, add the following to bind the configuration defined by the previous classes:

1   def configData = ConfigData.of { d -> d.
2       json(getResource("/config.json")).
3       yaml(getResource("/config.yml")).
4       sysProps().
5       env().build()
6   }
7   bindInstance(configData.get(Config))

Each declaration overrides the previous declarations. So in this case the order would be “class definition,” config.json, config.yml, system-properties, and then environment variables. This way you could override properties at runtime.

System property and environment variable configuration must be prefixed with the ratpack. and RATPACK_ prefixes. For example, the hostname property would be ratpack.database.hostname as a system property.

Testing

Ratpack includes many test fixtures for aiding your unit, functional, and integration tests. It assumes you’ll be using Spock to test your application.

First, if you’re using the Ratpack-Gradle plugin, simply add the following dependencies:

1   dependencies {
2       testCompile ratpack.dependency('test')
3       testCompile "org.spockframework:spock-core:0.7-groovy-2.0"
4       testCompile 'cglib:cglib:2.2.2'
5       testCompile 'org.objenesis:objenesis:2.1'
6   }

The cglib and objenesis dependencies are needed for object mocking.

Second, add your Spock tests under the src/test/groovy directory using the same package structure as your main project. A simple test might look like the following:

1   package myapp.services                                    
2   import spock.lang.Specification
3   
4   class MyServiceSpec extends Specification {
5       void "default service should return Hello World"() {
6           setup:
7           "Set up the service for testing"
8           def service = new  MyService()
9           when:
10           "Perform the service call"
11           def result = service.doStuff()
12           then:
13           "Ensure that the service call returned the proper result"
14           result == "Hello World"
15           "Shutdown the service when this feature is complete"
16           service.shutdown()
17       }
18   }

Ratpack enables your functional tests by running your full application within a test environment using GroovyRatpackMainApplicationUnderTest. For example, the following test would test the text rendered by your default handler:

1   package  myapp
2   import  ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
3   import spock.lang.Specification
4   
5   class FunctionalSpec extends Specification {
6       void "default handler should render Hello World"() {
7           setup:
8           def aut = new GroovyRatpackMainApplicationUnderTest()
9           when:
10           def response = aut.httpClient.text
11           then:
12           response == "Hello World!"
13           cleanup:
14           aut.close()
15       }
16   }

Merely calling text on the httpClient invokes a GET request. However, more complex requests can be invoked using requestSpec. For example, to test a specific response is returned based on the User-Agent header:

1   void "should properly render for v2.0 clients"() {
2       when:
3       def response = aut.httpClient.requestSpec { spec ->
4           spec.headers.'User-Agent' = ["Client v2.0"]
5       }.get("api").body.text
6       then:
7       response == "V2 Model"
8   }

Summary

This chapter taught you about the following:

  • How to get started with Ratpack.

  • Using handlers.

  • How to use bindings as a plugin architecture and for decoupling your modules.

  • How to render using Groovy Markup, Groovy Text, Thymeleaf, and Handlebars templates.

  • How to render and parse JSON.

  • Doing blocking operations.

  • Configuring a Ratpack app.

  • Testing a Ratpack app.

A426440_1_En_15_Figb_HTML.jpg Info

For more information, check out the Ratpack API3. Also, you should read the book Learning Ratpack 4 by Dan Woods.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset