Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Give Some Ratpack Love to Your Spring Boot Application

DZone's Guide to

Give Some Ratpack Love to Your Spring Boot Application

This article shows how to use Ratpack in an existing Spring Boot application to replace the traditional MVC part, making legacy, synchronous, blocking services async and non-blocking.

· Integration Zone
Free Resource

Learn how API management supports better integration in Achieving Enterprise Agility with Microservices and API Management, brought to you in partnership with 3scale

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, 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" 
}

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 traditionally do with Ratpack is start a server and define the configuration and handlers. Here we already have a Spring Boot Application running. Every class annotated with @Configuration will be picked up automatically and added to the application configuration. The first step to declare that configuration is to 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<Chain>> getHandlers() {
        List<Action<Chain>> handlers = new ArrayList<Action<Chain>>();
        handlers.add(fileApi());
        return handlers;
    }

    @Override
    public Action<ServerConfigBuilder> getServerConfig() {
        return config -> config.baseDir(BaseDir.find())
                .props(ImmutableMap.of("server.maxContentLength", "100000000", "app.name", "Search Store File"));

    }

    @Override
    public Action<BindingsSpec> getBindings() {
        return bindingConfig -> bindingConfig.module(HandlebarsModule.class).bind(FileHandler.class)
                .bind(StoredFileRenderer.class).bind(ErrorHandlerImpl.class).bind(ClientHandlerImpl.class);
    }

    private Action<Chain> 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))));
            });
        });
    }
}

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 important 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 bound 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 rely 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;
}

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());
}

What about Blocking Legacy Code?

Some of my services rely 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 to 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);
}

becomes

public Promise<String> 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.

Unleash the power of your APIs with future-proof API management - Create your account and start your free trial today, brought to you in partnership with 3scale.

Topics:
ratpack ,couchbase ,spring ,mvc ,boot

Published at DZone with permission of Laurent Doguin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}