BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Build High Performance JVM Microservices with Ratpack & Spring Boot

Build High Performance JVM Microservices with Ratpack & Spring Boot

Bookmarks

 

Ratpack and Spring Boot are a match made in microservice heaven. Each is a developer-centric web framework for the JVM, focused on productivity, efficiency, and lightweight deployments. They have their respective benefits in the area of microservice development, in that they bring different offerings to the table. Ratpack brings a reactive programming model with a high throughput, non-blocking web layer, and a convenient handler chain for defining application structure and HTTP request processing; Spring Boot brings an integration to the entire Spring ecosystem, and simplistic way to configure and autowire components into an application. For building cloud-native and data-driven microservices, they are a compliment that is unparalleled.

Ratpack ships with no opinion on an application’s underlying dependency injection framework. Instead, it allows applications to access service layer components through its DI abstraction, known as the Registry. Ratpack’s Registry is an integral aspect of its infrastructure, providing an interface for DI providers to participate in the component resolution sequence through a registry backing.

Out of the box, Ratpack ships with registry backings for both Guice and Spring Boot, giving the flexibility of implementation back to the developer.

In this post, we will demonstrate building a RESTful data-driven Ratpack and Spring Boot microservice that leverages Spring Data behind the scenes.

The best way to get started with a Ratpack project is by creating a Gradle build script and the standard Java project structure. Gradle is the supported build system for Ratpack, but since Ratpack is simply a collection of JVM libraries, it can really be built by any build system (although your mileage may vary). The easiest way to get Gradle installed if you don’t already have it is through the Groovy enVironment Manager. Our project’s buildscript is depicted in Listing 1.

Listing 1

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'io.ratpack:ratpack-gradle:0.9.18'
  }
}

apply plugin: 'io.ratpack.ratpack-java'
apply plugin: 'idea'
apply plugin: 'eclipse'

repositories {
  jcenter()
}

dependencies {
  compile ratpack.dependency('spring-boot') (1)
}

mainClassName = "springpack.Main" (2)

eclipse {
  classpath {
    containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
    containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
  }
}

The buildscript imports the Ratpack Spring Boot integration through the use of the Ratpack Gradle plugin’s ratpack.dependency(..) capability at (1). With the buildscript and project structure in place, we can create a "main class", which will be the runnable class to start and run our application. Note that at (2) we are specifying the main class name so that the command line tooling will work better. This means that our main class must correspond to this, so we’ll create a springpack.Main class in the src/main/java tree of the project.

Within the main class, we build an instance of a RatpackServer through a factory method, start, to which we supply the definition of our application. Within this definition will exist our RESTful HTTP API handler chain. As an initial demonstration, consider the main class shown in Listing 2. Note that Ratpack requires Java 8.

Listing 2

package springpack;

import ratpack.server.RatpackServer;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .handlers(chain -> chain (1)
          .prefix("api", pchain -> pchain (2)
            .all(ctx -> ctx (3)
              .byMethod(method -> method (4)
                .get(() -> ctx.render("Received GET request"))
                .post(() -> ctx.render("Received POST request"))
                .put(() -> ctx.render("Received PUT request"))
                .delete(() -> ctx.render("Received DELETE request"))
              )
            )
          )
      )
    );
  }
}

If we dissect the application definition within the main class, we can identify a few key areas that are worth explaining for those unfamiliar with Ratpack. The first notable point is that HTTP requests in Ratpack flow through a handler chain as defined by the handlers section of the definition at (1). Handlers are defined within the chain that describe the type of request they are capable of satisfying. Specifically, at (2) we define a prefix handler type, and specify that it should bind to the "api" HTTP route. The prefix handler, in turn created a new chain that will be delegated to for incoming requests that match the "/api" endpoint. At (3) we use the all handler type to specify that all incoming requests should be run through the provided handler, and at (4) we use Ratpack’s byMethod mechanism to bind get, post, put, and delete handlers to their respective HTTP methods.

We can now run the application from the command line by simply issuing the gradle “run” command at the root of the project. This will start and bind the webserver on port 5050. To demonstrate the existing functionality of the project and to make sure the handler structure is working as expected, we can run a few test with curl from the command line:

As you can see, the application handler chain is properly routing the request, and we have the structure for our RESTful API in place. Now we need to make it do something…​

For the sake of demonstration, let’s keep it simple and make this microservice responsible for the CRUD operations related to a User domain object. Through the REST endpoint, clients should be able to:

  • request a specific user account through a GET request with the username as a path variable;
  • list all users through a GET request when no username is specified;
  • create a user by POSTing a JSON encoded user object;
  • update the email address of a user by issuing a PUT request with the username as a path variable;
  • delete a user by issuing a DELETE request with the the username as a path variable.

Most of the infrastructure for handling these requirements is already in place based on the handlers we defined in the prior section, but the requirements mean that we will need to change things slightly. For example, we will now need to bind handlers that accept the username path variable. The updated code in Listing 3 shows the main class, now with handlers to match the requirements.

Listing 3

package springpack;

import ratpack.server.RatpackServer;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain (1)
          .prefix(":username", uchain -> uchain (2)
            .all(ctx -> { (3)
              String username = ctx.getPathTokens().get("username");
              ctx.byMethod(method -> method (4)
                .get(() -> ctx.render("Received request for user: " + username))
                                               .put(() -> {
                  String json = ctx.getRequest().getBody().getText();
                  ctx.render("Received update request for user: " + username + ", JSON: " + json);
                })
                .delete(() -> ctx.render("Received delete request for user: " + username))
              );
            })
          )
          .all(ctx -> ctx (5)
            .byMethod(method -> method
              .post(() -> { (6)
                String json = ctx.getRequest().getBody().getText();
                ctx.render("Received request to create a new user with JSON: " + json);
              })
              .get(() -> ctx.render("Received request to list all users")) (7)
            )
          )
        )
      )
    );
  }
}

The API has now been restructured to follow a more resource-oriented pattern centralized around our user domain object with the following changes:

  • at (1), we change the entry-level prefix to /api/users;
  • at (2), we bind a new prefix handler, this time on the :username path variable. Any value present in the incoming request path will be translated and made accessible to the Ratpack handler via the ctx.getPathTokens() map;
  • at (3), we bind a handler for all traffic following the /api/users/:username URI pattern;
  • and at (4) we use the byMethod mechanism to attach handlers to the HTTP GET, PUT, and DELETE methods. These handlers allow us to understand the intention of the client’s operation against a given user. Within the PUT handler, we make the ctx.getRequest().getBody().getText() call to capture the JSON from the incoming request;
  • at (5), we attach a handler to match all incoming requests to the /api/users endpoint;
  • at (6), we leverage the byMethod mechanism again within the /api/users handler to attach a POST handler that called when creating new users. We again make a call to capture the JSON from the incoming request;
  • and finally, at (7), we attach the GET handler that will be called when a client desires a list of all users.

If you again start the application and make another series of curl command line calls, we can test that the endpoints are operating as intended:

Now that we have the scaffolding in place that represents the requirements for our API, we need to make it do something useful. We can start by setting up the dependencies for our service layer. In this example, we will leverage the Spring Data JPA project for our data access object; the changes to the buildscript are reflected in Listing 4.

Listing 4

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'io.ratpack:ratpack-gradle:0.9.18'
  }
}

apply plugin: 'io.ratpack.ratpack-java'
apply plugin: 'idea'
apply plugin: 'eclipse'

repositories {
  jcenter()
}

dependencies {
  compile ratpack.dependency('spring-boot')
  compile 'org.springframework.boot:spring-boot-starter-data-jpa:1.2.4.RELEASE' (1)
  compile 'com.h2database:h2:1.4.187' (2)
}

mainClassName = "springpack.Main"

eclipse {
  classpath {
    containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
    containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
  }
}

 

The only changes are at (1), we now include the Spring Boot Spring Data JPA dependency, and at (2) we bring in the H2 embedded database dependency. When H2 is found on the classpath, Spring Boot will autoconfigure Spring Data to use it as an in-memory data source. Configuring and working with Spring Data datasources is well documented on the project page.

With the new dependencies in place, the first thing we must do is start by modeling our microservice’s domain object: the user. The User class can be fairly simple for the sake of demonstration, and the code in Listing 5 shows a properly modeled JPA domain entity. We place this in the src/main/java/springpack/model/User.java class file within the project.

Listing 5

package springpack.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {
  private static final long serialVersionUID = 1l;

  @Id
  @GeneratedValue
  private Long id;

  @Column(nullable = false)
  private String username;

  @Column(nullable = false)
  private String email;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }
}

We can make use of the javax.persistence.* annotations since Spring Data is now on the project’s compile-time classpath. Spring Boot makes it a seamless process to get up-and-running with data access objects, so we can model our DAO around the Repository service type, as afforded to us by Spring Data. Since our API follows relatively straight-forward CRUD operations, we can utilize the CrudRepository fixture provided by Spring Data to minimize the code necessary for the UserRepository DAO implementation.

Listing 6

package springpack.model;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {

  User findByUsername(String username); (1)
}

Amazingly, the UserRepository DAO implementation shown in Listing 6 is all that is necessary for us to have a fully formed service layer for our User domain object. The Repository interface offered by Spring Data allows us to create "helper" lookup methods based on the convention of the entity we are searching against. Based on the requirements, we know that our API layer will need to lookup users by their username, so we can add the findByUsername method at <1>. We place the UserRepository into the src/main/java/springpack/model/UserRepository.java class file within the project.

Before we can dig in to modifying the API to make use of the UserRepository, we first must define our Spring Boot application class. This class acts as a configuration entry point into the Spring Boot autoconfiguration engine and constructs a Spring ApplicationContext that we can use as a registry backing within our Ratpack application. Listing 7 depicts the Spring Boot configuration class.

Listing 7

package springpack;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBootConfig {

  @Bean
  ObjectMapper objectMapper() { (1)
    return new ObjectMapper();
  }
}

The stunningly small amount of code required for our SpringBootConfig class goes into the src/main/java/springpack/SpringBootConfig.java class file. In this, we are explicitly wiring in a bean definition for the Jackson ObjectMapper. We’ll use this within our API layer to read and write JSON.

The @SpringBootApplication annotation does the majority of the heavy lifting here. When we initialize the Spring Boot registry backing, we provide this class as the entry point. Its infrastructure will then use that annotation to scan the classpath for any available components, autowire them into the application context, and autoconfigure them according to the conventional rules of Spring Boot. For example, the mere presence of the UserRepository class (annotated with @Repository) on our application’s classpath will cause Spring Boot to proxy that interface through the Spring Data engine, which will also be configured to work with the H2 embedded database, which is also on the classpath. At this point, nothing more is needed from the Spring Boot side of things.

The next thing that we must do before we can implement our API layer is instruct Ratpack to use our Spring Boot application as a registry. Ratpack’s Spring Boot integration provides a fixture to seamlessly translate a Spring Boot application to a registry backing, making it a single line of code to merge the two worlds. The code in Listing 8 shows an updated main class, this time with the SpringBootConfig class stood up as a registry for our API layer.

Listing 8

package springpack;

import ratpack.server.RatpackServer;
import ratpack.spring.Spring;
import springpack.config.SpringBootConfig;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .registry(Spring.spring(SpringBootConfig.class)) (1)
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain
          .prefix(":username", uchain -> uchain
            .all(ctx -> {
              String username = ctx.getPathTokens().get("username");
              ctx.byMethod(method -> method
                .get(() -> ctx.render("Received request for user: " + username))
                .put(() -> {
                  String json = ctx.getRequest().getBody().getText();
                  ctx.render("Received update request for user: " + username + ", JSON: " + json);
                })
                .delete(() -> ctx.render("Received delete request for user: " + username))
              );
            })
          )
          .all(ctx -> ctx
            .byMethod(method -> method
              .post(() -> {
                String json = ctx.getRequest().getBody().getText();
                ctx.render("Received request to create a new user with JSON: " + json);
              })
              .get(() -> ctx.render("Received request to list all users"))
            )
          )
        )
      )
    );
  }
}

The only change necessary is at (1), where we provide the Ratpack application definition with an explicit Registry implementation. Now we can get started implementing the API layer.

As you follow through on the upcoming changes, it is again important to understand that Ratpack differs a great deal from traditional servlet-based web applications. As noted earlier, Ratpack’s HTTP layer is built on a non-blocking network interface, which supports its nature as a highly performant web framework. A servlet-based web application will spawn a new thread for each incoming request, which is resource inefficient, but allows each request processing flow to act in isolation. In this paradigm, a web application is able to do things like make calls to a database and wait for the corresponding results without worrying about (relatively) impacting its ability to service subsequent clients. In a non-blocking web application, the networking layer does not block while the client or server is not sending data, so a high number of concurrent requests are able to be made across a small pool of "request taking" threads. It means, however, that if the application code were to block on one of these request taking threads, throughput would suffer dramatically. To that extent, it is important that blocking operations, like calls to a database, do not take place within the request thread.

Luckily, Ratpack makes it easy to work with blocking operations within your application by exposing a blocking interface on the request’s context. This will schedule blocking operations to a different thread pool, and allow those calls to complete synchronously, while still servicing new incoming requests at a high volume. Once the blocking call is complete, the processing flow will return to the request taking thread, and a response can be written back to the client. As we build out the API layer, we need to ensure that any calls that make use of the UserRepository are routed through the blocking fixture, as shown in API layer’s implementation in Listing 9.

Listing 9

package springpack;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import ratpack.exec.Promise;
import ratpack.handling.Context;
import ratpack.server.RatpackServer;
import ratpack.spring.Spring;
import springpack.model.User;
import springpack.model.UserRepository;

import java.util.HashMap;
import java.util.Map;

public class Main {
  private static final Map<String, String> NOT_FOUND = new HashMap<String, String>() {{
    put("status", "404");
    put("message", "NOT FOUND");
  }};
  private static final Map<String, String> NO_EMAIL = new HashMap<String, String>() {{
    put("status", "400");
    put("message", "NO EMAIL ADDRESS SUPPLIED");
  }};

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .registry(Spring.spring(SpringBootConfig.class))
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain
          .prefix(":username", uchain -> uchain
            .all(ctx -> {
              // extract the "username" path variable
              String username = ctx.getPathTokens().get("username");
              // pull the UserRepository out of the registry
              UserRepository userRepository = ctx.get(UserRepository.class);
              // pull the Jackson ObjectMapper out of the registry
              ObjectMapper mapper = ctx.get(ObjectMapper.class);
              // construct a "promise" for the requested user object. This will
              // be subscribed to within the respective handlers, according to what
              // they must do. The promise uses the "blocking" fixture to ensure
              // the DB call doesn't take place on a "request taking" thread.
              Promise<User> userPromise = ctx.blocking(() -> userRepository.findByUsername(username));
              ctx.byMethod(method -> method
                .get(() ->
                  // the .then() block will "subscribe" to the result, allowing
                  // us to send the user domain object back to the client
                  userPromise.then(user -> sendUser(ctx, user))
                )
                .put(() -> {
                  // Read the JSON from the request
                  String json = ctx.getRequest().getBody().getText();
                  // Parse out the JSON body into a Map
                  Map<String, String> body = mapper.readValue(json, new TypeReference<Map<String, String>>() {
                  });
                  // Check to make sure the request body contained an "email" address
                  if (body.containsKey("email")) {
                    userPromise
                      // map the new email address on to the user entity
                      .map(user -> {
                        user.setEmail(body.get("email"));
                        return user;
                      })
                      // and use the blocking thread pool to save the updated details
                      .blockingMap(userRepository::save)
                      // finally, send the updated user entity back to the client
                      .then(u1 -> sendUser(ctx, u1));
                  } else {
                    // bad request; we didn't get an email address
                    ctx.getResponse().status(400);
                    ctx.getResponse().send(mapper.writeValueAsBytes(NO_EMAIL));
                  }
                })
                .delete(() ->
                  userPromise
                    // make the DB delete call in a blocking thread
                    .blockingMap(user -> {
                      userRepository.delete(user);
                      return null;
                    })
                    // then send a 204 back to the client
                    .then(user -> {
                      ctx.getResponse().status(204);
                      ctx.getResponse().send();
                    })
                )
              );
            })
          )
          .all(ctx -> {
            // pull the UserRepository out of the registry
            UserRepository userRepository = ctx.get(UserRepository.class);
            // pull the Jackson ObjectMapper out of the registry
            ObjectMapper mapper = ctx.get(ObjectMapper.class);
            ctx.byMethod(method -> method
              .post(() -> {
                // read the JSON request body...
                String json = ctx.getRequest().getBody().getText();
                // ... and convert it into a user entity
                User user = mapper.readValue(json, User.class);
                // save the user entity on a blocking thread and
                // render the user entity back to the client
                ctx.blocking(() -> userRepository.save(user))
                  .then(u1 -> sendUser(ctx, u1));
              })
              .get(() ->
                // make the DB call, on a blocking thread, to list all users
                ctx.blocking(userRepository::findAll)
                  // and render the user list back to the client
                  .then(users -> {
                    ctx.getResponse().contentType("application/json");
                    ctx.getResponse().send(mapper.writeValueAsBytes(users));
                  })
              )
            );
          })
        )
      )
    );
  }

  private static void notFound(Context context) {
    ObjectMapper mapper = context.get(ObjectMapper.class);
    context.getResponse().status(404);
    try {
      context.getResponse().send(mapper.writeValueAsBytes(NOT_FOUND));
    } catch (JsonProcessingException e) {
      context.getResponse().send();
    }
  }

  private static void sendUser(Context context, User user) {
    if (user == null) {
      notFound(context);
    }

    ObjectMapper mapper = context.get(ObjectMapper.class);
    context.getResponse().contentType("application/json");
    try {
      context.getResponse().send(mapper.writeValueAsBytes(user));
    } catch (JsonProcessingException e) {
      context.getResponse().status(500);
      context.getResponse().send("Error serializing user to JSON");
    }
  }
}

The most noteworthy element in the API layer’s implementation is the use of the blocking mechanism, which can be extracted from the Context object that comes through with each request. When calling ctx.blocking(), a Promise object is returned, which must be subscribed to in order to have the code execute. This allows us to stage a promise, as shown in the prefix(":username") chain, for reuse within different handlers, keeping the code clean.

Now that the API is implemented, we can again run a series of curl tests to ensure that the microservice is working as intended:

Following this sequence of commands, we can indeed see that our API layer is functioning properly, and we have a fully formed, data-driven Ratpack and Spring Boot microservice that is using Spring Data JPA!

The final step of the whole process is to prepare it for deployment. The easiest way to accomplish this is to execute the gradle installDist command. This will package your application and all of its runtime dependencies in tarball (.tar file) and zip (.zip file) archives. It will additionally create cross-platform start scripts for you to be able to start your microservice on any system that has Java 8 installed. After the installDist task completes, you can find these archives in the build/distributions directory of your project.

Through this post you have learned how to create a microservice application that builds the performance and ease of use of Ratpack, while leveraging the vast ecosystem offered by Spring Boot. You can use this example as a lift-off point on your journey to building cloud native and data driven microservices on the JVM.

Happy Ratpacking! Happy Spring Booting!

About the Author

Daniel Woods is a Technology Enthusiast specialising in enterprise Java, Groovy, and Grails development. He has over a decade of experience in JVM software development, and shares his experience by contributing to open source projects like the Grails and Ratpack web frameworks. Dan has been a speaker at the Gr8conf and SpringOne 2GX conferences, where he presents his expertise in enterprise application architecture on the JVM.

Rate this Article

Adoption
Style

BT