Application Development

Accelerate Java application development on GCP with Micronaut

Editor’s note: Want to develop microservices in Java? Today we hear from Object Computing, Inc. (OCI), a Google Cloud partner that is also the driving force behind the Micronaut JVM framework. Here, OCI senior software engineer Sergio del Amo talks about how to use Micronaut on GCP to build serverless applications, and walks you through an example.

Traditional application architectures are being replaced by new patterns and technologies. Organizations are discovering great benefits to breaking so-called monolithic applications into smaller, service-oriented applications that work together in a distributed system. The new architectural patterns introduced by this shift call for the interaction of numerous, scope-limited, independent applications: microservices.

To support microservices, modern applications are built on cloud computing technologies, such as those provided by Google Cloud. Rather than managing the health of servers and data centers, organizations can deploy their applications to platforms where the details of servers are abstracted away, and services can be scaled, redeployed, and monitored using sophisticated tooling and automation.

In a cloud-native world, optimizing how a Java program’s logic is interpreted and run on cloud servers via annotations and other compilation details takes on new importance. Additionally, serverless computing adds incentive for applications to be lightweight and responsive and to consume minimal memory. Today's JVM frameworks need to ease not just development, as they have done over the past decade, but also operations.  

Enter Micronaut. Last year, a team of developers at OCI  released this open source JVM framework that was designed to simplify developing and deploying microservices and serverless applications.

Micronaut comes with built-in support for GCP services and hosting. Then, in addition to out-of-the-box auto-configurations, job scheduling, and myriad security options, Micronaut provides a suite of built-in cloud-native features, including:

  • Service discovery. Service discovery means that applications are able to find each other (and make themselves findable) on a central registry, eliminating the need to look up URLs or hardcode server addresses in configuration. Micronaut builds service-discovery support directly into the @Client annotation, meaning that performing service discovery is as simple as supplying the correct configuration and then using the "service ID" of the desired service.
  • Load balancing. When multiple instances of the same service are registered, Micronaut provides a form of "round-robin" load-balancing, cycling requests through the available instances to ensure that no one instance is overwhelmed or underutilized. This is a form of client-side load-balancing, where each instance either accepts a request or passes it along to the next instance of the service, spreading the load across available instances automatically.
  • Retry mechanism and circuit breakers. When interacting with other services in a distributed system, it’s inevitable that at some point, things won’t work out as planned—perhaps a service goes down temporarily or drops a request. Micronaut offers a number of tools to gracefully handle these mishaps. Retry provides the ability to invoke failed operations. Circuit breakers protect the system from repetitive failures.

As a result of this natively cloud-native construction, you can use Micronaut in scenarios that would not be feasible with a traditional Model-View-Controller framework in the JVM, including low-memory microservices, Android applications, serverless functions, IoT deployments, and CLI applications.

Micronaut also provides a reactive HTTP server and client based on Netty, an asynchronous networking framework that offers high performance and a reactive, event-driven programming model.

Sample App: Google Cloud Translate API

To see how easy it is to integrate a Micronaut application with Google Cloud services, review this tutorial for building a sample application that consumes the Google Cloud Translation API.

Step 1: Install Micronaut

You can build Micronaut from the source on Github or download it as a binary and install it on your shell path. However the recommended way to install Micronaut is via SDKMAN!. If you do not have SDKMAN! installed already, you can do so in any Unix-based shell with the following commands:

  curl -s "https://get.sdkman.io" | bash
    source "$HOME/.sdkman/bin/sdkman-init.sh"
    sdk version

    SDKMAN 5.6.4+305

You can now install Micronaut itself with the following SDKMAN! command (use sdk list micronaut to view available versions; at the time of this writing, the latest is 1.0.5):


  sdk install micronaut 1.0.5

Confirm that you have installed Micronaut by running mn -v:

  mn -v
    | Micronaut Version: 1.0.5
    | JVM Version: 1.8.0_171

Step 2: Create the project

The mn command serves as Micronaut's CLI. You can use this command to create your new Micronaut project. 

For this exercise, we will create a stock Java application, but you can also choose Groovy or Kotlin as your preferred language by supplying the -lang flag (-lang groovy or -lang kotlin).

The mn command accepts a features flag, where you can specify features that add support for various libraries and configurations in your project. You can view available features by running mn profile-info service.

We're going to use the spock feature to add support for the Spock testing framework to our Java project. Run the following command:

  mn create-app example.micronaut.translator -features spock
    | Application created at /Users/dev/translator

Note that we can supply a default package prefix (example.micronaut) to the project name (translator). If we did not do so, the project name would be used as a default package. This package will contain the Application class and any classes generated using the CLI commands (as we will do shortly).

By default the create-app command generates a Gradle build. If you prefer Maven as your build tool, you can do so using the -build flag (e.g., -build maven). This exercise uses the default Gradle project.

At this point, you can run the application using the Gradle run task.


  ./gradlew run
    Starting a Gradle Daemon (subsequent builds will be faster)
     Task :run
     03:00:04.807 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in     1109ms. Server Running: http://localhost:8080

TIP: If you would like to run your Micronaut project using an IDE, be sure that your IDE supports Java annotation processors and that this support is enabled for your project. In the IntelliJ IDEA, the relevant setting can be found under Preferences -> Build, Execution, Deployment -> Compiler -> Annotation Processors -> Enabled.

Step 3: Create a simple interface

Create a Java interface to define the translation contract:

src/main/java/example/micronaut/TranslationService.java


  package example.micronaut.micronaut;

public interface TranslationService {
    TranslationResult translate(String text, String sourceLanguage, String targetLanguage);
}

If I want to translate Hello World to Spanish, you can invoke any available implementations of the previous interface with  translationService.translate( "Hello World", "en", "es")  .

We create a POJO to encapsulate the translation result.

src/main/java/example/micronaut/TranslationResult.java


  package example.micronaut.micronaut;

public class TranslationResult {
    private String text;
    private String source;
    private String target;
    private String translatedText;

    public TranslationResult() {}

    public TranslationResult(String text, String source, String target, String translatedText) {
        this.text = text;
        this.source = source;
        this.target = target;
        this.translatedText = translatedText;
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public String getTranslatedText() {
        return translatedText;
    }

    public void setTranslatedText(String translatedText) {
        this.translatedText = translatedText;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getTarget() {
        return target;
    }

    public void setTarget(String target) {
        this.target = target;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TranslationResult that = (TranslationResult) o;

        if (text != null ? !text.equals(that.text) : that.text != null) return false;
        if (source != null ? !source.equals(that.source) : that.source != null) return false;
        if (target != null ? !target.equals(that.target) : that.target != null) return false;
        return translatedText != null ? translatedText.equals(that.translatedText) : that.translatedText == null;
    }

    @Override
    public int hashCode() {
        int result = text != null ? text.hashCode() : 0;
        result = 31 * result + (source != null ? source.hashCode() : 0);
        result = 31 * result + (target != null ? target.hashCode() : 0);
        result = 31 * result + (translatedText != null ? translatedText.hashCode() : 0);
        return result;
    }
}

Step 4: Expose an endpoint

Similar to other MVC frameworks such as Grails or Spring Boot, you can expose an endpoint by creating a controller.

The endpoint, which we will declare in a moment, consumes a JSON payload that encapsulates the translation request. We can map such JSON payload with a POJO.

src/main/java/example/micronaut/TranslationCommand.java


  package example.micronaut.micronaut;

import javax.validation.constraints.NotBlank;

public class TranslationCommand {

    @NotBlank
    private String text;

    @NotBlank
    private String source;

    @NotBlank
    private String target;

    public TranslationCommand() {
    }

    public TranslationCommand(String text, String source, String target) {
        this.text = text;
        this.source = source;
        this.target = target;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public String getTarget() {
        return target;
    }

    public void setTarget(String target) {
        this.target = target;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TranslationCommand that = (TranslationCommand) o;

        if (text != null ? !text.equals(that.text) : that.text != null) return false;
        if (source != null ? !source.equals(that.source) : that.source != null) return false;
        return target != null ? target.equals(that.target) : that.target == null;
    }

    @Override
    public int hashCode() {
        int result = text != null ? text.hashCode() : 0;
        result = 31 * result + (source != null ? source.hashCode() : 0);
        result = 31 * result + (target != null ? target.hashCode() : 0);
        return result;
    }
}

Please note that the previous class uses the annotation @ javax.validation.constraint.NotBlank to declare text, source, and target as required. Micronaut's validation is built in with the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API. You need an implementation of the validation API in the classpath. Thus, add the next snippet to build.gradle

build.gradle

  compile "io.micronaut.configuration:micronaut-hibernatevalidator"

Next, create a controller:

src/ main/java/example/micronaut/TranslationController.java


  package example.micronaut.micronaut;

import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import javax.validation.Valid;

@Validated
@Controller("/translate")
public class TranslationController {

    private final TranslationService translationService;

    public TranslationController(TranslationService translationService) {
        this.translationService = translationService;
    }

    @Post("/{?translationCommand*}")
    public TranslationResult translate(@Body @Valid TranslationCommand translationCommand) {
        return translationService.translate(translationCommand.getText(),
                translationCommand.getSource(),
                translationCommand.getTarget());
    }
}

There are several things worth mentioning about the previous code listing:

  • The Controller exposes a /translate endpoint which could be invoked with a POST request.
  • The value of @Post and @Controller annotations is a RFC-6570 URI template.
  • Via constructor injection, Micronaut supplies a collaborator; TranslatorService.
  • Micronaut controllers consume and produce JSON by default.
  • @Body indicates that the method argument is bound from the HTTP body.
  • To validate the incoming request, you need to annotate your controller with @Validated and the binding POJO with @Valid.

In addition to constructor injection, as illustrated in the previous snippet, Micronaut supports the following types of dependency injection: Field injection, JavaBean property injection or Method parameter injection.

Integrate with Google Cloud Translation API

Now you want to add a dependency to Google Cloud Translate library:

build.gradle

  dependencies {
        ...
        compile 'com.google.cloud:google-cloud-translate:1.55.0'
        ...
    }

Micronaut implements the JSR 330 specification for Java dependency injection, which provides a set of semantic annotations under the javax.inject package (such as @Inject and @Singleton) to express relationships between classes within the DI container.

Create a singleton implementation of TranslationService that uses the Google Cloud Translation API.

src/main/java/example/micronaut/GoogleTranslationService.java

  package example.micronaut.micronaut;

import com.google.cloud.translate.Translate;
import com.google.cloud.translate.TranslateOptions;
import com.google.cloud.translate.Translation;

import javax.annotation.PostConstruct;
import javax.inject.Singleton;

@Singleton
public class GoogleTranslationService implements TranslationService {

    private Translate translate;

    @PostConstruct
    private void initialize() {
        translate = TranslateOptions.getDefaultInstance().getService();
    }

    @Override
    public TranslationResult translate(String text, String sourceLanguage, String targetLanguage) {
        Translation translation =
                translate.translate(
                        text,
                        Translate.TranslateOption.sourceLanguage(sourceLanguage),
                        Translate.TranslateOption.targetLanguage(targetLanguage));
        return new TranslationResult(text,
                translation.getSourceLanguage(),
                targetLanguage,
                translation.getTranslatedText());
    }
}

Here are a few things to mention about the above  code:

  • @Singleton annotation is used to declare the class as a Singleton.

  • A method annotated with @PostConstruct will be invoked once the object is constructed and fully injected.

Test the app Thanks to Micronaut’s fast startup time, it is easy to write functional tests with it.

Here’s how to write a functional test that verifies the behavior of the whole application.

  package example.micronaut

import example.micronaut.micronaut.TranslationCommand
import example.micronaut.micronaut.TranslationResult
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Requires
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll

class TranslationControllerSpec extends Specification {
    @Shared
    @AutoCleanup
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

    @Shared
    @AutoCleanup
    RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

    @Requires({env['GOOGLE_APPLICATION_CREDENTIALS']})
    def "verify integration with google cloud translation api"() {
        when:
        TranslationCommand translationCommand = new TranslationCommand("Hello World",
                "en",
                "es")
        HttpRequest request = HttpRequest.POST("/translate", translationCommand)
        TranslationResult result = client.toBlocking().retrieve(request, TranslationResult)

        then:
        result

        and:
        result == new TranslationResult(translationCommand.text,
                translationCommand.source,
                translationCommand.target,
                "Hola Mundo")
    }

    @Unroll
    def "body #attribute is required"(String text, String source, String target, String attribute) {
        when:
        TranslationCommand cmd = new TranslationCommand(text, source, target)
        HttpRequest request = HttpRequest.POST("/translate", cmd)
        client.toBlocking().retrieve(request, TranslationResult)

        then:
        HttpClientResponseException e = thrown()
        e.response.status == HttpStatus.BAD_REQUEST

        where:
        text            | source | target
        null            | "en"   | "es"
        "Hello World"   | null   | "es"
        "Hello World"   | "en"   | null

        attribute = text == null ? 'text' : source == null ? 'source' : target == null ? 'target' : 'undefined'
    }
}

Here are a few things to note about the above code:

  • It’s easy to run the application from a test with the EmbeddedServer interface.
  • You can easily create an HTTP Client bean to consume the embedded server.
  • Micronaut HTTP Client makes it easy to parse JSON into Java objects.
  • Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
  • We verify the that server responds 400 (Bad request status code) when the validation of the incoming JSON payload fails.

Deploy to Google Cloud

There are multiple ways to deploy a Micronaut application to Google Cloud. You may choose to containerize your app or deploy it as a FAT jar. Check out these tutorials to learn more:

Deploy a Micronaut application to Google Cloud App Engine

Deploy a Micronaut application containerized with Jib to Google Kubernetes Engine

Micronaut performance

In addition to its cloud-native features, Micronaut also represents a significant step forward in microservice frameworks for the JVM, by supporting common Java framework features such as dependency injection (DI) and aspect-oriented programming (AOP), without compromising startup time, performance, and memory consumption.

Micronaut features a custom-built DI and AOP model that does not use reflection. Instead, an abstraction over the Java annotation processor tool (APT) API and Groovy abstract syntax tree (AST) lets developers build efficient applications without giving up features they know and love.

By moving the work of the DI container to the compilation phase, there is no longer a link between the size of the codebase and the time needed to start an application or the memory required to store reflection metadata. As a result, Micronaut applications written in Java typically start within a second.

This approach has opened doors to a variety of framework features that are more easily achieved with AOT compilation and that are unique to Micronaut.

Conclusion

Cloud-native development is here to stay, and Micronaut was built with this landscape in mind. Like the cloud-native architecture that motivated its creation, Micronaut’s flexibility and modularity allows developers to create systems that even its designers could not have foreseen.

To learn more about using Micronaut for your cloud-based projects, check out the Micronaut user guide. Learn how to use Micronaut in concert with Google Cloud Platform services, such as Cloud SQL, Kubernetes, and Google’s Instance Metadata Server in our upcoming webinar. There’s also a small but growing selection of step-by-step tutorials, including guides for all three of Micronaut's supported languages: Java, Groovy, and Kotlin.

Finally, the Micronaut community channel on Gitter is an excellent place to meet other developers who are already building applications with the framework and interact directly with the core development team.