BT

InfoQ Homepage Articles Project Helidon Tutorial: Building Microservices with Oracle’s Lightweight Java Framework

Project Helidon Tutorial: Building Microservices with Oracle’s Lightweight Java Framework

This item in japanese

Bookmarks

Key Takeaways

  • Helidon is a lightweight microservices framework introduced by Oracle in September 2018.
  • Helidon is a collection of Java libraries designed for creating microservices-based applications.
  • Helidon was designed to be simple and fast and ships with two versions: Helidon SE and Helidon MP.
  • Helidon supports GraalVM to convert Helidon SE applications to native executable code.
  • In this tutorial, you will be introduced to Helidon, explore Helidon SE and Helidon MP, and download GitHub repositories related to the content in this tutorial.
  • Helidon 1.4.4 is the current stable release, but Helidon 2.0 is scheduled to be released in late Spring 2020.

Oracle introduced its new open-source framework, Project Helidon, in September 2018. Originally named J4C (Java for Cloud), Helidon is a collection of Java libraries for creating microservices-based applications. Within six months of its introduction, Helidon 1.0 was released in February 2019. The current stable release is Helidon 1.4.4, but Oracle is well on their way to releasing Helidon 2.0 planned for late Spring 2020.

This tutorial will introduce Helidon SE and Helidon MP, explore the three core components of Helidon SE, how to get started, and introduce a movie application built on top of Helidon MP. There will also be a discussion on GraalVM and what you can expect with the upcoming release of Helidon 2.0.

Helidon Landscape

Helidon, designed to be simple and fast, is unique because it ships with two programming models: Helidon SE and Helidon MP. In the graph below, you can see where Helidon SE and Helidon MP align with other popular microservices frameworks.

Helidon SE

Helidon SE is a microframework that features three core components required to create a microservice -- a web server, configuration, and security -- for building microservices-based applications. It is a small, functional style API that is reactive, simple and transparent in which an application server is not required.

Let’s take a look at the functional style of Helidon SE with this very simple example on starting the Helidon web server using the WebServer interface:

WebServer.create(
    Routing.builder()
        .get("/greet", (req, res)
             -> res.send("Hello World!"))
        .build())
    .start();

Using this example as a starting point, we will incrementally build a formal startServer() method, part of a server application for you to download, to explore the three core Helidon SE components.

Web Server Component

Inspired by NodeJS and other Java frameworks, Helidon's web server component is an asynchronous and reactive API that runs on top of Netty. The WebServer interface provides basic server lifecycle and monitoring enhanced by configuration, routing, error handling, and building metrics and health endpoints.

Let’s start with the first version of the startServer() method to start a Helidon web server on a random available port:

private static void startServer() {
    Routing routing = Routing.builder()
            .any((request, response) -> response.send("Greetings from the web server!" + "\n"))
            .build();

    WebServer webServer = WebServer
            .create(routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

First, we need to build an instance of the Routing interface that serves as an HTTP request-response handler with routing rules. In this example, we use the any() method to route the request to the defined server response, “Greetings from the web server!”, which will be displayed in the browser or via the curl command.

In building the web server, we invoke the overloaded create() method designed to accept various server configurations. The simplest one, as shown above, accepts the instance variable, routing, that we just created to provide default server configuration.

The Helidon web server was designed to be reactive which means the start() method returns and an instance of the CompletionStage<WebServer> interface to start the web server. This allows us to invoke the toCompletableFuture() method. Since a specific server port wasn’t defined, the server will find a random available port upon startup.

Let’s build and run our server application with Maven:

$ mvn clean package
$ java -jar target/helidon-server.jar

When the server starts, you should see the following in your terminal window:

Apr 15, 2020 1:14:46 PM io.helidon.webserver.NettyWebServer <init>
INFO: Version: 1.4.4
Apr 15, 2020 1:14:46 PM io.helidon.webserver.NettyWebServer lambda$start$8
INFO: Channel '@default' started: [id: 0xcba440a6, L:/0:0:0:0:0:0:0:0:52535]
INFO: Server started at: http://localhost:52535

As shown on the last line, the Helidon web server selected port 52535. While the server is running, enter this URL in your browser or execute it with following curl command in a separate terminal window:

$ curl -X GET http://localhost:52535

You should see “Greetings from the web server!

To shut down the web server, simply add this line of code:

webServer.shutdown()
        .thenRun(() -> System.out.println("INFO: Server is shutting down...Good bye!"))
        .toCompletableFuture();

Configuration Component

The configuration component loads and processes configuration properties. Helidon’s Config interface will read configuration properties from a defined properties file, usually but not limited to, in YAML format.

Let’s create an application.yaml file that will provide configuration for the application, server, and security.

app:
  greeting: "Greetings from the web server!"

server:
  port: 8080
  host: 0.0.0.0

security:
  config:
    require-encryption: false

  providers:
    - http-basic-auth:
        realm: "helidon"
        users:
          - login: "ben"
            password: "${CLEAR=password}"
            roles: ["user", "admin"]
          - login: "mike"
            password: "${CLEAR=password}"
            roles: ["user"]
    - http-digest-auth:

There are three main sections, or nodes, within this application.yaml file - app, server and security. The first two nodes are straightforward. The greeting subnode defines the server response that we hard-coded in the previous example. The port subnode defines port 8080 for the web server to use upon startup. However, you should have noticed that the security node is a bit more complex utilizing YAML’s sequence of mappings to define multiple entries. Separated by the ‘-’ character, two security providers, http-basic-auth and http-digest-auth, and two users, ben and mike, have been defined. We will discuss this in more detail in the Security Component section of this tutorial.

Also note that this configuration allows for clear-text passwords as the config.require-encryption subsection is set to false. You would obviously set this value to true in a production environment so that any attempt to pass a clear-text password would throw an exception.

Now that we have a viable configuration file, let’s update our startServer() method to take advantage of the configuration we just defined.


private static void startServer() {
    Config config = Config.create();
    ServerConfiguration serverConfig = ServerConfiguration.create(config.get("server"));

    Routing routing = Routing.builder()
            .any((request, response) -> response.send(config.get("app.greeting").asString().get() + "\n"))
            .build();

    WebServer webServer = WebServer
            .create(serverConfig, routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

First, we need to build an instance of the Config interface by invoking its create() method to read our configuration file. The get(String key) method, provided by Config, returns a node, or a specific subnode, from the configuration file specified by key. For example, config.get("server") will return the content under the server node and config.get("app.greeting") will return “Greetings from the web server!”.

Next, we create an instance of ServerConfiguration, providing immutable web server information, by invoking its create() method by passing in the statement, config.get("server").

The instance variable, routing, is built like the previous example except we eliminate hard-coding the server response by calling config.get("app.greeting").asString().get().

The web server is created like the previous example except we use a different version of the create() method that accepts the two instance variables, serverConfig and routing.

We can now build and run this version of our web server application using the same Maven and Java commands. Executing the same curl command:

$ curl -X GET http://localhost:8080

You should see “Greetings from the web server!

Security Component

Helidon’s security component provides authentication, authorization, audit and outbound security. There is support for a number of implemented security providers for use in Helidon applications:

  • HTTP Basic Authentication
  • HTTP Digest Authentication
  • HTTP Signatures
  • Attribute Based Access Control (ABAC) Authorization
  • JWT Provider
  • Header Assertion
  • Google Login Authentication
  • OpenID Connect
  • IDCS Role Mapping

You can use one of three approaches to implement security in your Helidon application:

  • a builder pattern where you manually provide configuration
  • a configuration pattern where you provide configuration via a configuration file
  • a hybrid of the builder and configuration patterns

We will be using the hybrid approach to implement security in our application, but we need to do some housekeeping first.

Let’s review how to reference the users defined under the security node of our configuration file. Consider the following string:

security.providers.0.http-basic-auth.users.0.login

When the parser comes across a number in the string, it indicates there are one or more subnodes in the configuration file. In this example, the 0 right after providers will direct the parser to move into the first provider subnode, http-basic-auth. The 0 right after users will direct the parser to move into the first user subnode containing login, password and roles. Therefore, the above string will return the login, password and role information for the user, ben, when passed into the config.get() method. Similarly, the login, password and role information for user, mike, would be returned with this string:

security.providers.0.http-basic-auth.users.1.login

Next, let’s create a new class to our web server application, AppUser, that implements the SecureUserStore.User interface:

public class AppUser implements SecureUserStore.User {

    private String login;
    private char[] password;
    private Collection<String> roles;

    public AppUser(String login, char[] password, Collection<String> roles) {
        this.login = login;
        this.password = password;
        this.roles = roles;
        }
    
    @Override
    public String login() {
        return login;
        }

    @Override
    public boolean isPasswordValid(char[] chars) {
        return false;
        }

    @Override
    public Collection<String> roles() {
        return roles;
        }

    @Override
    public Optional<String> digestHa1(String realm, HttpDigest.Algorithm algorithm) {
        return Optional.empty();
        }
    }

We will use this class to build a map of roles to users, that is:

Map<String, AppUser> users = new HashMap<>();

To accomplish this, we add a new method, getUsers(), to our web server application that populates the map using the configuration from the http-basic-auth subsection of the configuration file.

private static Map<String, AppUser> getUsers(Config config) {
    Map<String, AppUser> users = new HashMap<>();

    ConfigValue<String> ben = config.get("security.providers.0.http-basic-auth.users.0.login").asString();
    ConfigValue<String> benPassword = config.get("security.providers.0.http-basic-auth.users.0.password").asString();
    ConfigValue<List<Config>> benRoles = config.get("security.providers.0.http-basic-auth.users.0.roles").asNodeList();

    ConfigValue<String> mike = config.get("security.providers.0.http-basic-auth.users.1.login").asString();
    ConfigValue<String> mikePassword = config.get("security.providers.0.http-basic-auth.users.1.password").asString();
    ConfigValue<List<Config>> mikeRoles = config.get("security.providers.0.http-basic-auth.users.1.roles").asNodeList();

    users.put("admin", new AppUser(ben.get(), benPassword.get().toCharArray(), Arrays.asList("user", "admin")));
    users.put("user", new AppUser(mike.get(), mikePassword.get().toCharArray(), Arrays.asList("user")));

    return users;
    }

Now that we have this new functionality built into our web server application, let’s update the startServer() method to add security with Helidon’s implementation of HTTP Basic Authentication:

private static void startServer() {
    Config config = Config.create();
    ServerConfiguration serverConfig = ServerConfiguration.create(config.get("server"));

    Map<String, AppUser> users = getUsers(config);
    displayAuthorizedUsers(users);

    SecureUserStore store = user -> Optional.ofNullable(users.get(user));

    HttpBasicAuthProvider provider = HttpBasicAuthProvider.builder()
            .realm(config.get("security.providers.0.http-basic-auth.realm").asString().get())
            .subjectType(SubjectType.USER)
            .userStore(store)
            .build();

    Security security = Security.builder()
            .config(config.get("security"))
            .addAuthenticationProvider(provider)
            .build();

    WebSecurity webSecurity = WebSecurity.create(security)
            .securityDefaults(WebSecurity.authenticate());

    Routing routing = Routing.builder()
            .register(webSecurity)
            .get("/", (request, response) -> response.send(config.get("app.greeting").asString().get() + "\n"))
            .get("/admin", (request, response) -> response.send("Greetings from the admin, " + users.get("admin").login() + "!\n"))
            .get("/user", (request, response) -> response.send("Greetings from the user, " + users.get("user").login() + "!\n"))
            .build();

    WebServer webServer = WebServer
            .create(serverConfig, routing)
            .start()
            .toCompletableFuture()
            .get(10, TimeUnit.SECONDS);

    System.out.println("INFO: Server started at: http://localhost:" + webServer.port() + "\n");
    }

As we did in the previous example, we will build the instance variables, config and serverConfig. We then build our map of roles to users, users, with the getUsers() method as shown above.

Using Optional for null type-safety, the store instance variable is built from the SecureUserStore interface as shown with the lambda expression. A secure user store is used for both HTTP Basic Authentication and HTTP Digest Authentication. Please keep in mind that HTTP Basic Authentication can be unsafe, even when used with SSL, as passwords are not required.

We are now ready to build an instance of HTTPBasicAuthProvider, one of the implementing classes of the SecurityProvider interface. The realm() method defines the security realm name that is sent to the browser (or any other client) when unauthenticated. Since we have a realm defined in our configuration file, it is passed into the method. The subjectType() method defines the principal type a security provider would extract or propagate. It accepts one of two SubjectType enumerations, namely USER or SERVICE. The userStore() method accepts the store instance variable we just built to validate users in our application.

With our provider instance variable, we can now build an instance of the Security class used to bootstrap security and integrate it with other frameworks. We use the config() and addAuthenticationProvider() methods to accomplish this. Please note that more than one security provider may be registered by chaining together additional addAuthenticationProvider() methods. For example, let’s assume we defined instance variables, basicProvider and digestProvider, to represent the HttpBasicAuthProvider and HttpDigestAuthProvider classes, respectively. Our security instance variable may be built as follows:

Security security = Security.builder()
        .config(config.get("security"))
        .addAuthenticationProvider(basicProvider)
        .addAuthenticationProvider(digestProvider)
        .build();

The WebSecurity class implements the Service interface which encapsulates a set of routing rules and related logic. The instance variable, webSecurity, is built using the create() method by passing in the security instance variable and the WebSecurity.authentic() method, passed into the securityDefaults() method, ensures the request will go through the authentication process.

Our familiar instance variable, routing, that we’ve built in the previous two examples looks much different now. It registers the webSecurity instance variable and defines the endpoints, ‘/’, ‘/admin’, and ‘/user’ by chaining together get() methods. Notice that the /admin and /user endpoints are tied to users, ben and mike, respectively.

Finally, our web server can be started! After all the machinery we just implemented, building the web server looks exactly like the previous example.

We can now build and run this version of our web server application using the same Maven and Java commands and execute the following curl commands:

$ curl -X GET http://localhost:8080/ will return “Greetings from the web server!

$ curl -X GET http://localhost:8080/admin will return “Greetings from the admin, ben!

$ curl -X GET http://localhost:8080/user will return “Greetings from the user, mike!

You can find a comprehensive server application that demonstrates all three versions of the startServer() method related to the three core Helidon SE components we just explored. You can also find more extensive Helidon security examples that will show you how to implement some of the other security providers.

Helidon MP

Built on top of Helidon SE, Helidon MP is a small, declarative style API that is an implementation of the MicroProfile specification, a platform that optimizes enterprise Java for a microservices architecture for building microservices-based applications. The MicroProfile initiative, formed in 2016 as a collaboration of IBM, Red Hat, Payara and Tomitribe, specified three original APIs - CDI (JSR 365), JSON-P (JSR 374) and JAX-RS (JSR-370) - considered the minimal amount of APIs for creating a microservices application. Since then, MicroProfile has grown to 12 core APIs along with four standalone APIs to support reactive streams and GraphQL. MicroProfile 3.3, released in February 2020, is the latest version.

Helidon MP currently supports MicroProfile 3.2. For Java EE/Jakarta EE developers, Helidon MP is an excellent choice due to its familiar declarative approach with use of annotations. There is no deployment model and no additional Java EE packaging required.

Let’s take a look at the declarative style of Helidon MP with this very simple example on starting the Helidon web server and how it compares to the functional style with Helidon SE.

public class GreetService {
  @GET
  @Path("/greet")
  public String getMsg() {
    return "Hello World!";
    }
  }

Notice the difference in this style compared to the Helidon SE functional style.

Helidon Architecture

Now that you have been introduced to Helidon SE and Helidon MP, let’s see how they fit together. Helidon’s architecture can be described in the diagram shown below. Helidon MP is built on top of Helidon SE and the CDI extensions, explained in the next section, extend the cloud-native capabilities of Helidon MP.

CDI Extensions

Helidon ships with portable Context and Dependency Injection (CDI) extensions that support integration of various data sources, transactions and clients to extend the cloud-native functionality of Helidon MP applications. The following extensions are provided:

Helidon Quick Start Guides

Helidon provides quick start guides for both Helidon SE and Helidon MP. Simply visit these pages and follow the instructions. For example, you can quickly build a Helidon SE application by simply executing the following Maven command in your terminal window:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion=1.4.4 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se

This will generate a small, yet working application in the folder, helidon-quickstart-se, that includes a test and configuration files for the application (application.yaml), logging (logging.properties), building a native image with GraalVM (native-image.properties), containerizing the application with Docker (Dockerfile and Dockerfile.native) and orchestrating with Kubernetes (app.yaml).

Similarly, you can quickly build a Helidon MP application:

$ mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-mp \
    -DarchetypeVersion=1.4.4 \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-mp \
    -Dpackage=io.helidon.examples.quickstart.mp

This is a great starting point for building more complex Helidon applications as we will discuss in the next section.

Movie Application

Using a generated Helidon MP quickstart application, additional classes - a POJO, a resource, a repository, a custom exception, and an implementation of ExceptionMapper - were added to build a complete movie application that maintains a list of Quentin Tarantino movies. The HelidonApplication class, shown below, registers the required classes.

@ApplicationScoped
@ApplicationPath("/")
public class HelidonApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> set = new HashSet<>();
        set.add(MovieResource.class);
        set.add(MovieNotFoundExceptionMapper.class);
        return Collections.unmodifiableSet(set);
        }
    }

You can clone the GitHub repository to learn more about the application.

GraalVM

Helidon supports GraalVM, a polyglot virtual machine and platform, that converts applications to native executable code. GraalVM, created by Oracle Labs, is comprised of Graal, a just-in-time compiler written in Java, SubstrateVM, a framework that allows ahead-of-time compilation of Java applications into executable images, and Truffle, an open-source toolkit and API for building language interpreters. The latest version is 20.1.0.

You can convert Helidon SE applications to native executable code using GraalVM’s native-image utility that is a separate installation using GraalVM’s gu utility:

$ gu install native-image
$ export
GRAALVM_HOME=/usr/local/bin/graalvm-ce-java11-20.1.0/Contents/Home

Once installed, you can return to the helidon-quickstart-se directory and execute the following command:

$ mvn package -Pnative-image

This operation will take a few minutes, but once complete, your application will be converted to native code. The executable file will be found in the /target directory.

The Road to Helidon 2.0

Helidon 2.0.0 is scheduled to be released in the late Spring 2020 with Helidon 2.0.0.RC1 available to developers at this time. Significant new features include support for GraalVM on Helidon MP applications, new Web Client and DB Client components, a new CLI tool, and implementations of the standalone MicroProfile Reactive Messaging and Reactive Streams Operators APIs.

Until recently, only Helidon SE applications were able to take advantage of GraalVM due to the use of reflection in CDI 2.0 (JSR 365), a core MicroProfile API. However, due to customer demand, Helidon 2.0.0 will support Helidon MP applications to be converted to a native image. Oracle has created this demo application for the Java community to preview this new feature.

To complement the original three core Helidon SE APIs - Web Server, Configuration and Security - a new Web Client API completes the set for Helidon SE. Building an instance of the WebClient interface allows you to process HTTP requests and responses related to a specified endpoint. Just like the Web Server API, Web Client may also be configured via a configuration file.

You can learn more details on what developers can expect in the upcoming GA release of Helidon 2.0.0.

About the Author

Michael Redlich is a Senior Research Technician at ExxonMobil Research & Engineering in Clinton, New Jersey (views are his own) with experience in developing custom scientific laboratory and web applications for the past 30 years. He also has experience as a Technical Support Engineer at Ai-Logix, Inc. (now AudioCodes) where he provided technical support and developed telephony applications for customers. His technical expertise includes object-oriented design and analysis, relational database design and development, computer security, C/C++, Java, Python, and other programming/scripting languages. His latest passions include MicroProfile, Jakarta EE, Helidon, Micronaut and MongoDB.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Is your profile up-to-date? Please take a moment to review and update.

Note: If updating/changing your email, a validation request will be sent

Company name:
Company role:
Company size:
Country/Zone:
State/Province/Region:
You will be sent an email to validate the new email address. This pop-up will close itself in a few moments.
午夜电影_免费高清视频|高清电视|高清电影_青苹果影院