I recently started working with Ratpack and I quite like it. I mostly did quick projects from scratch. But I would like to use it in an existing Spring Boot application to replace the traditional MVC part. This is actually easy to do as everything has been thought out already thanks to their Spring module.

If you follow this blog you might remember an old post about storing, indexing and searching files with Couchbase and Spring Boot. I will use the related code as an example. The idea is to replace Spring MVC by Ratpack, and making my legacy, synchronous, blocking services async and non blocking. The resulting code is available on github as well.

Adding the Right Dependencies

I am using Gradle. Ratpack is very well integrated with it. All you need to do to add a module is to add the right dependency by calling ratpack.dependency("myFavoriteModule") . So in our case, to add support for Spring Boot, you need to add ratpack.dependency("spring-boot") . Unfortunately the version automatically managed by Ratpack is less than 1.4.0.M3, which is the version that brings automatic Couchbase configuration. So this time I will have to add dependencies manually.

dependencies { compile ratpack.dependency("guice"), ratpack.dependency("rx"), ratpack.dependency("handlebars"), "com.couchbase.client:java-client:2.3.1", "org.springframework.boot:spring-boot-autoconfigure:1.4.0.M3", "io.ratpack:ratpack-spring-boot:1.3.3", "org.slf4j:slf4j-simple:1.7.12", "org.codehaus.plexus:plexus-utils:3.0.21", "commons-codec:commons-codec:1.10" } 1 2 3 4 5 6 7 8 9 10 11 12 dependencies { compile ratpack . dependency ( "guice" ) , ratpack . dependency ( "rx" ) , ratpack . dependency ( "handlebars" ) , "com.couchbase.client:java-client:2.3.1" , "org.springframework.boot:spring-boot-autoconfigure:1.4.0.M3" , "io.ratpack:ratpack-spring-boot:1.3.3" , "org.slf4j:slf4j-simple:1.7.12" , "org.codehaus.plexus:plexus-utils:3.0.21" , "commons-codec:commons-codec:1.10" }

What you can see here is that ratpack.dependency("spring-boot") is a shortcut to add org.springframework.boot:spring-boot-autoconfigure:1.4.0.M3 and io.ratpack:ratpack-spring-boot:1.3.3 . What this module gives you is the ability to integrate a Ratpack server to your Spring Application. You will be able to retrieve Spring @Beans from the Ratpack context and declare handlers as Spring configuration.

Declare Ratpack Configuration

One thing you have to love with Spring Boot is the autoconfig. You only need to make sure the Couchbase SDK is in the classpath, and that the property spring.couchbase.bootstrap-hosts is declared. At that moment Spring beans will be instantiated for a default Bucket. And this bucket instance will be available as a bean or in Ratpack’s context. So you don’t have to declare any binding for Couchbase in the Ratpack layer.

The first thing you traditionaly do with Ratpack is start a server and define the configuration and handlers. Here we already have a Spring Boot Application running. Every classes annotated with @Configuration will be picked up automatically and added to the application configuration. The first step to declare that configuration is create a Class that implements RatpackServerCustomizer and annotate it with @Confguration. It let you define a list of handlers, bindings and server configuration. In the Following example I am registering some server properties and binding several classes to Ratpack’s context. The ‘server.maxContentLength’ property is the maximum size of file you can upload.

package org.couchbase.devex; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.couchbase.devex.domain.StoredFileRenderer; import org.couchbase.devex.service.SearchService; import org.springframework.context.annotation.Configuration; import com.google.common.collect.ImmutableMap; import ratpack.form.Form; import ratpack.func.Action; import ratpack.guice.BindingsSpec; import ratpack.handlebars.HandlebarsModule; import ratpack.handlebars.Template; import ratpack.handling.Chain; import ratpack.rx.RxRatpack; import ratpack.server.BaseDir; import ratpack.server.ServerConfigBuilder; import ratpack.spring.config.RatpackServerCustomizer; import rx.Observable; @Configuration public class RatpackConfiguration implements RatpackServerCustomizer { @Override public List<action> getHandlers() { List<action> handlers = new ArrayList<action>(); handlers.add(fileApi()); return handlers; } @Override public Action getServerConfig() { return config -> config.baseDir(BaseDir.find()) .props(ImmutableMap.of("server.maxContentLength", "100000000", "app.name", "Search Store File")); } @Override public Action getBindings() { return bindingConfig -> bindingConfig.module(HandlebarsModule.class).bind(FileHandler.class) .bind(StoredFileRenderer.class).bind(ErrorHandlerImpl.class).bind(ClientHandlerImpl.class); } private Action fileApi() { return chain -> chain.prefix("file", FileHandler.class).post("fulltext", ctx -> { ctx.parse(Form.class).then(form -> { String queryString = form.get("queryString"); SearchService searchService = ctx.get(SearchService.class); Observable<map<string, object="">> files = searchService.searchFulltextFiles(queryString); RxRatpack.promise(files).then(response -> ctx .render(Template.handlebarsTemplate("uploadForm", "text/html", m -> m.put("files", response)))); }); }).post("n1ql", ctx -> { ctx.parse(Form.class).then(form -> { String queryString = form.get("queryString"); SearchService searchService = ctx.get(SearchService.class); Observable<map<string, object="">> files = searchService.searchN1QLFiles(queryString); RxRatpack.promise(files).then(response -> ctx .render(Template.handlebarsTemplate("uploadForm", "text/html", m -> m.put("files", response)))); }); }); } } </map<string,></map<string,></action</action</action 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 68 69 package org . couchbase . devex ; import java . util . ArrayList ; import java . util . Collections ; import java . util . List ; import java . util . Map ; import org . couchbase . devex . domain . StoredFileRenderer ; import org . couchbase . devex . service . SearchService ; import org . springframework . context . annotation . Configuration ; import com . google . common . collect . ImmutableMap ; import ratpack . form . Form ; import ratpack . func . Action ; import ratpack . guice . BindingsSpec ; import ratpack . handlebars . HandlebarsModule ; import ratpack . handlebars . Template ; import ratpack . handling . Chain ; import ratpack . rx . RxRatpack ; import ratpack . server . BaseDir ; import ratpack . server . ServerConfigBuilder ; import ratpack . spring . config . RatpackServerCustomizer ; import rx . Observable ; @ Configuration public class RatpackConfiguration implements RatpackServerCustomizer { @ Override public List < action > getHandlers ( ) { List < action > handlers = new ArrayList < action > ( ) ; handlers . add ( fileApi ( ) ) ; return handlers ; } @ Override public Action getServerConfig ( ) { return config - > config . baseDir ( BaseDir . find ( ) ) . props ( ImmutableMap . of ( "server.maxContentLength" , "100000000" , "app.name" , "Search Store File" ) ) ; } @ Override public Action getBindings ( ) { return bindingConfig - > bindingConfig . module ( HandlebarsModule . class ) . bind ( FileHandler . class ) . bind ( StoredFileRenderer . class ) . bind ( ErrorHandlerImpl . class ) . bind ( ClientHandlerImpl . class ) ; } private Action fileApi ( ) { return chain - > chain . prefix ( "file" , FileHandler . class ) . post ( "fulltext" , ctx - > { ctx . parse ( Form . class ) . then ( form - > { String queryString = form . get ( "queryString" ) ; SearchService searchService = ctx . get ( SearchService . class ) ; Observable < map < string , object = "" > > files = searchService . searchFulltextFiles ( queryString ) ; RxRatpack . promise ( files ) . then ( response - > ctx . render ( Template . handlebarsTemplate ( "uploadForm" , "text/html" , m - > m . put ( "files" , response ) ) ) ) ; } ) ; } ) . post ( "n1ql" , ctx - > { ctx . parse ( Form . class ) . then ( form - > { String queryString = form . get ( "queryString" ) ; SearchService searchService = ctx . get ( SearchService . class ) ; Observable < map < string , object = "" > > files = searchService . searchN1QLFiles ( queryString ) ; RxRatpack . promise ( files ) . then ( response - > ctx . render ( Template . handlebarsTemplate ( "uploadForm" , "text/html" , m - > m . put ( "files" , response ) ) ) ) ; } ) ; } ) ; } } < / map < string , > < / map < string , > < / action < / action < / action

The application templating system relies on Handlebars so you need the HandlebarsModule. FileHandler will handle all the call to the ‘/file’ API and the StoredFileRenderer make sure StoredFile will be rendered correctly. The last two bindings are for error managements.

The most imortant thing going on here is the fileAPI method that declares my handler. A handler defines what’s going on when a user hits a particular URL. Here we associate every ‘/file/*’ call to the FileHandler class. We also define the behavior for POST on ‘/fulltext’ and ‘/n1ql’.

Ratpack uses promises. So when you parse a Form coming from a POST request, you’ll get a promise. What you can see in each of those POST is that the SearchService is fetched from Ratpack’s context. Even if it was never binded in the configuration. That’s because Spring beans are available in the context as part of the integration.

The next step is to call that search service which returns an Observable. We can use Ratpack’s rx-java module that provides a wrapper for Observables. It will wrap this as a promise. Then you can simply render the response.

At this point we got rid of all the Spring MVC controllers. As you can see my service return an Observable. Which is not the case in my previous application.

Migrating Services for Ratpack

Most of my services relies on Couchbase. The SDK is based on RxJava so it’s really easy to convert most of those to an async, non-blocking fashion and have them returns Observable.

Using RxJava

This is a very simple example. It’s a N1QL query that maps the results to a List of Map. The two first lines don’t change at all as they are mostly defining the query. You can see that the mapping feels more natural when using the synchronous bucket in the second version.

public List<map<string, object="">> searchN1QLFiles(String whereClause) { N1qlQuery query = N1qlQuery.simple( "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause); query.params().consistency(ScanConsistency.STATEMENT_PLUS); N1qlQueryResult res = bucket.query(query); List<map<string, object="">> filenames = res.allRows().stream().map(row -> row.value().toMap()) .collect(Collectors.toList()); return filenames; } </map<string,></map<string,> 1 2 3 4 5 6 7 8 9 10 public List < map < string , object = "" > > searchN1QLFiles ( String whereClause ) { N1qlQuery query = N1qlQuery . simple ( "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause ) ; query . params ( ) . consistency ( ScanConsistency . STATEMENT_PLUS ) ; N1qlQueryResult res = bucket . query ( query ) ; List < map < string , object = "" > > filenames = res . allRows ( ) . stream ( ) . map ( row - > row . value ( ) . toMap ( ) ) . collect ( Collectors . toList ( ) ) ; return filenames ; } < / map < string , > < / map < string , >

Becomes

public Observable<map<string, object="">> searchN1QLFiles(String whereClause) { N1qlQuery query = N1qlQuery.simple( "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause); query.params().consistency(ScanConsistency.STATEMENT_PLUS); return bucket.async().query(query).flatMap(AsyncN1qlQueryResult::rows).map(r -> r.value().toMap()); } </map<string,> 1 2 3 4 5 6 7 public Observable < map < string , object = "" > > searchN1QLFiles ( String whereClause ) { N1qlQuery query = N1qlQuery . simple ( "SELECT binaryStoreLocation, binaryStoreDigest FROM `default` WHERE type= 'file' " + whereClause ) ; query . params ( ) . consistency ( ScanConsistency . STATEMENT_PLUS ) ; return bucket . async ( ) . query ( query ) . flatMap ( AsyncN1qlQueryResult : : rows ) . map ( r - > r . value ( ) . toMap ( ) ) ; } < / map < string , >

What about Blocking Legacy Code?

Some of my services relies on old, blocking code. While there is no magical way to make them non-blocking, we can easily wrap them in a Promise. This will allow us use them easily in the Handlers. Wrapping blocking call is super easy, all you need to do is wrap your function with ‘Blocking.get()’. Here’s a very simple example:

public String getSha1Digest(InputStream is) { return DigestUtils.sha1Hex(is); } 1 2 3 4 public String getSha1Digest ( InputStream is ) { return DigestUtils . sha1Hex ( is ) ; }

becomes

public Promise getSha1Digest(InputStream is) { return Blocking.get(() -> DigestUtils.sha1Hex(is)); } 1 2 3 4 public Promise getSha1Digest ( InputStream is ) { return Blocking . get ( ( ) - > DigestUtils . sha1Hex ( is ) ) ; }

Conclusion

Now you know pretty much everything you need to know to give some Ratpack Love to your Spring Boot application. If you feel like anything is missing please reach out to me on twitter or in the comments below.