Skip to content

Latest commit

 

History

History
 
 

se-restful-webservice

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

The RESTful Web Service Guide

What you will learn

You’ll learn how to use Helidon quickly to create a RESTful web service that accepts these HTTP requests:

Method and URL Result

GET localhost:8080/greet

Returns a generic but friendly greeting

GET localhost:8080/greet/Joe

Returns a personalized greeting for the specified person

PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' localhost:8080/greet/greeting

Changes the greeting used in subsequent responses

You’ll create the app in three main steps:

  1. Use the Helidon Maven archetype to create a basic Helidon SE app that responds to the HTTP requests.

  2. Add code to perform a simple app-specific health check.

  3. Add code to record a simple app-specific metric.

As you develop the app, this guide helps you understand what each part of the code does. If you prefer to download the finished code for this example, follow the instructions at Download the example source.

What you need

About 15 minutes

An IDE or text editor

JDK 1.8 or later

Maven 3.5 or later

Develop your application

Generate the Maven project using the Helidon archetype

Helidon provides a Maven archetype you can use to create a new Helidon project that includes sample code.

  1. cd to a directory that is not already a Maven project.

  2. Run this command:

    Creating a new Helidon SE project
    mvn archetype:generate -DinteractiveMode=false \
        -DarchetypeGroupId=io.helidon.archetypes \
        -DarchetypeArtifactId=helidon-quickstart-se \
        -DarchetypeVersion={helidon-version} \
        -DgroupId=io.helidon.guides \
        -DartifactId=se-restful-webservice \
        -Dpackage=io.helidon.guides.se.restfulwebservice

    Running the archetype this way creates a subdirectory se-restful-webservice (using the artifactId setting from the archetype invocation) that contains a new Maven project for a Helidon service.

Browse the generated source

pom.xml

The input you provided to the archetype determines the project’s Maven coordinates:

<artifactId>se-restful-webservice</artifactId>

In the <dependency-management> section, note the dependency on the helidon-bom POM:

<dependency>
    <groupId>io.helidon</groupId>
    <artifactId>helidon-bom</artifactId>
    <version>${helidon.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

Later, in the <dependencies> section, you will see declarations for various Helidon components:

  • web server

  • config support for YAML

  • health check

  • metrics

The helidon-bom dependency adds <dependency-management> declarations for all the Helidon components. You can add Helidon dependencies to your project without having to specify the version. For example, see the declaration for config YAML support:

<dependency>
    <groupId>io.helidon.config</groupId>
    <artifactId>helidon-config-yaml</artifactId>
</dependency>

src/main/resources/application.yaml: a config resource file for the application

Your app uses Helidon config to initialize the greeting and set up the HTTP listener.

src/main/resources/application.yaml
app:
  greeting: "Hello" # (1)

server:             # (2)
  port: 8080
  host: 0.0.0.0
  1. Sets the initial greeting text for responses from the service

  2. Sets how the service will listen for requests

src/main/resources/logging.properties

This file controls logging. .src/main/resources/logging.properties

# Send messages to the console
handlers=java.util.logging.ConsoleHandler

# Global default logging level. Can be overriden by specific handlers and loggers
.level=INFO

# Helidon Web Server has a custom log formatter that extends SimpleFormatter.
# It replaces "!thread!" with the current thread name
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=io.helidon.webserver.WebServerLogFormatter
java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n

GreetService.java: the greeting service for the app

In general, your application can implement multiple services, each tied to its own URL path. The example includes just one service: the greeting service in src/main/java/io/helidon/guides/se/restfulwebservice/GreetService.java.

  1. Note these import statements.

    import javax.json.Json;
    import javax.json.JsonBuilderFactory;
    import javax.json.JsonObject;
    
    import io.helidon.common.http.Http;
    import io.helidon.config.Config;
    import io.helidon.webserver.Routing;
    import io.helidon.webserver.ServerRequest;
    import io.helidon.webserver.ServerResponse;
    import io.helidon.webserver.Service;

    These imports are necessary for JSON and config support and for the key parts of the web server.

  2. The GreetService class implements io.helidon.webserver.Service.

  3. Its constructor accepts a Config object to control its behavior:

    GreetService(Config config) {
        this.greeting = config.get("app.greeting").asString().orElse("Ciao"); //(1)
    }

    Here the code looks up the initial greeting from the configuration object and saves it.

  4. The update method updates the routing rules in the web server to link the service’s methods with the corresponding URL paths:

    @Override
    public void update(Routing.Rules rules) {
        rules
            .get("/", this::getDefaultMessageHandler) //(1)
            .get("/{name}", this::getMessageHandler) //(2)
            .put("/greeting", this::updateGreetingHandler); //(3)
    }
    1. Handle GET requests that contain no extra path using getDefaultMessage.

    2. Handle GET requests that contain a name using getMessage, which personalizes the response using the name provided as the path suffix.

    3. Handle PUT requests to the greeting path using updateGreeting, interpreting the greeting value in the JSON payload as the new greeting string.

  5. The following methods respond to the three types of request.

    1. Returning the default greeting:

      private void getDefaultMessageHandler(ServerRequest request,
                                     ServerResponse response) {
          sendResponse(response, "World"); //(1)
      }
      1. The default message ends with "World!" — that is, without personalizing the message with the user’s name.

    2. Returning a personalized greeting:

      private void getMessageHandler(ServerRequest request,
                              ServerResponse response) {
          String name = request.path().param("name"); //(1)
          sendResponse(response, name); //(2)
      }
      1. Gets the name from the URL path in the request.

      2. Includes the user’s name in building the response.

    3. Updating the greeting:

      private void updateGreetingHandler(ServerRequest request,
                                         ServerResponse response) {
          request.content().as(JsonObject.class) // (1)
                  .thenAccept(jo -> updateGreetingFromJson(jo, response));
      }
      1. Compose the JSON response to confirm the new setting for greeting.

      This method delegates to updateGreetingFromJson:

          private void updateGreetingFromJson(JsonObject jo, ServerResponse response) {
      
              if (!jo.containsKey("greeting")) { // (1)
                  JsonObject jsonErrorObject = JSON.createObjectBuilder()
                          .add("error", "No greeting provided")
                          .build();
                  response.status(Http.Status.BAD_REQUEST_400)
                          .send(jsonErrorObject);
                  return;
              }
      
              greeting = jo.getString("greeting"); // (2)
      
              response.status(Http.Status.NO_CONTENT_204) // (3)
                      .send();
          }
      1. Rejects the request if the JSON payload does not include the greeting setting.

      2. Extracts the new greeting from the JSON object and saves it.

      3. Sends the "no content" response, acknowledging that the new greeting has been set.

Main.java

The job of Main is to create and start the web server. It uses the configuration in the config file to initialize the server, registering the greeting service with it. The startServer method does most of the work.

  1. Create and configure the server.

    // By default this will pick up application.yaml from the classpath
    Config config = Config.create(); //(1)
    
    // Get webserver config from the "server" section of application.yaml
    ServerConfiguration serverConfig = //(2)
            ServerConfiguration.create(config.get("server"));
    
    WebServer server = WebServer.create(serverConfig, createRouting(config)); //(3)
    1. Loads configuration from application.yaml.

    2. Creates the ServerConfiguration object from the relevant part of the Config object just loaded.

    3. Creates the server using the config and the updated routing rules (see below).

  2. Start the server.

    // Try to start the server. If successful, print some info and arrange to
    // print a message at shutdown. If unsuccessful, print the exception.
    server.start() //(1)
        .thenAccept(ws -> { //(2)
            System.out.println(
                    "WEB server is up! http://localhost:" + ws.port() + "/greet");
            ws.whenShutdown().thenRun(() //(3)
                -> System.out.println("WEB server is DOWN. Good bye!"));
            })
        .exceptionally(t -> { //(4)
            System.err.println("Startup failed: " + t.getMessage());
            t.printStackTrace(System.err);
            return null;
        });
    
    // Server threads are not daemon. No need to block. Just react.
    1. Starts the server.

    2. When the startup completes successfully, prints a message.

    3. Prints a message when the server is shut down. The CompletionStage returned from server.whenShutdown() completes when some other code invokes server.shutdown(). The current example does not invoke that method (except from a test), so in this example server the CompletionStage will never complete and so the message will not be printed. This code does show how easy it is to detect and respond to an orderly shutdown if you trigger one from your app.

    4. Report a failed startup.

  3. Create routing rules for the app.

    private static Routing createRouting(Config config) {
    
        MetricsSupport metrics = MetricsSupport.create();
        GreetService greetService = new GreetService(config);
        HealthSupport health = HealthSupport.builder()
                .add(HealthChecks.healthChecks())   // Adds a convenient set of checks
                .build(); //(1)
        return Routing.builder() //(2)
                .register(JsonSupport.create())
                .register(health)                   // Health at "/health"
                .register(metrics)                  // Metrics at "/metrics"
                .register("/greet", greetService)
                .build();
    }
    1. Sets up several built-in health checks (deadlock, disk space, heap memory) for the server.

    2. Builds the Routing instance by registering the JSON, health, metrics, and the app’s own greeting service.

    Later steps in this guide show how to add your own, app-specific health check and metric.

Build and run

You can use your IDE’s features to build and run the project directly.

Or, to use Maven outside the IDE, build your app this way:

mvn package

and run it like this:

java -jar target/se-restful-webservice.jar

Once you have started your app, from another command window run these commands to access its functions:

Command Result Function
curl -X GET http://localhost:8080/greet
{"message":"Hello World!"}

Returns a greeting with no personalization

curl -X GET http://localhost:8080/greet/Joe
{"message":"Hello Joe!"}

Returns the personalized greeting

curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting

(no response payload)

Changes the greeting

curl -X GET http://localhost:8080/greet/Jose
{"message":"Hola Jose!"}

Shows that the greeting change took effect

Add an app-specific health check

A well-behaved microservice reports on its own health. Two common approaches for checking health, often used together, are:

  • readiness - a simple verification that the service has been started, has initialized itself, and is ready to respond to requests; and

  • liveness - often a more thorough assessment of whether and how well the service can do its job.

For example, Kubernetes can ping your service’s readiness endpoint after it starts the pod containing the service to determine when the service is ready to accept requests, withholding traffic until the readiness endpoint reports success. Kubernetes can use the liveness endpoint to find out if the service considers itself able to function, attempting a pod restart if the endpoint reports a problem.

In general a liveness check might assess:

  • service health - whether the service itself can do its job correctly

  • host health - if the host has sufficient resources (for example, disk space) for the service to operate

  • health of other, dependent services - if other services on which this service depends are themselves OK.

We will add an app-specific liveness check. Our greeting service does not depend on any host resources (like disk space) or any other services. So for this example we define our service as "alive" in a very trivial way: if the greeting text has been assigned and is not empty when trimmed of leading or trailing white space. Otherwise we consider the service to be unhealthy, in which case the service will still respond but its answers might not be what we want.

Normally we would write our service to make sure that a newly-assigned greeting is non-empty before accepting it. But omitting that validation lets us create an easy health check that we can use by simply setting the greeting to blank from a curl command.

Revise GreetService.java

  1. Add health-related imports.

    import org.eclipse.microprofile.health.HealthCheckResponse;
    import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
  2. Add a liveness check method.

    The new method returns a HealthCheckResponse. This will make it very easy to add our custom health check to the built-in ones already in the code.

        HealthCheckResponse checkAlive() {
            HealthCheckResponseBuilder builder = HealthCheckResponse.builder()
                    .name("greetingAlive"); //(1)
            if (greeting == null || greeting.trim().length() == 0) { //(2)
                builder.down() //(3)
                       .withData("greeting", "not set or is empty");
            } else {
                builder.up(); //(4)
            }
            return builder.build(); //(5)
        }
    1. Use a builder to assemble the response, giving the health check a human-readable name.

    2. Enforce that the greeting be non-empty and non-null in order for the greeting service to be considered alive.

    3. For a null or empty greeting the response indicates that the service is down, in this case adding an explanation.

    4. For a valid greeting the response says the service is up.

    5. Either way, have the builder build the response.

Revise Main.java

We need to modify the createRouting method slightly to add our custom health check to the HealthSupportBuilder.

                .add(greetService::checkAlive)

Here’s the revised method after this change:

private static Routing createRouting(Config config) {

    MetricsSupport metrics = MetricsSupport.create();
    GreetService greetService = new GreetService(config);
    HealthSupport health = HealthSupport.builder()
            .add(HealthChecks.healthChecks())   // Adds a convenient set of checks
            .add(greetService::checkAlive)
            .build(); //(1)
    return Routing.builder() //(2)
            .register(JsonSupport.create())
            .register(health)                   // Health at "/health"
            .register(metrics)                  // Metrics at "/metrics"
            .register("/greet", greetService)
            .build();
}
  1. The health instance now includes the greet service liveness check.

  2. The returned routing refers to the previously-instantiated and saved GreetService instance.

Stop, rebuild and rerun your service

  1. Stop any running instance of your app.

  2. Rebuild the app and then run it.

Check the server’s health

Run this command:

curl -X GET http://localhost:8080/health | json_pp

You should see output as shown in this example:

{
    "checks": [
        {
            "name": "deadlock",
            "state": "UP"
        },
        {
            "data": {
                "free": "179.37 GB",
                "freeBytes": 192597303296,
                "percentFree": "38.51%",
                "total": "465.72 GB",
                "totalBytes": 500068036608
            },
            "name": "diskSpace",
            "state": "UP"
        },
        {
            "name": "greetingAlive",
            "state": "UP"
        },
        {
            "data": {
                "free": "255.99 MB",
                "freeBytes": 268422144,
                "max": "4.00 GB",
                "maxBytes": 4294967296,
                "percentFree": "98.73%",
                "total": "308.00 MB",
                "totalBytes": 322961408
            },
            "name": "heapMemory",
            "state": "UP"
        }
    ],
    "outcome": "UP"
}

The JSON output conveys various health indicators because the generated code included HealthChecks.healthChecks() in the HealthSupport.builder. The item labeled outcome describes the overall health of the server based on all the other indicators. The state of all the indicators is UP. So the outcome field shows UP. You should also see our app-specific liveness check in the output (bolded above).

Arrange for an unhealthy report

Recall that our simple rule for liveness is that the greeting be non-null and non-empty. We can easily force our server to report an unhealthy state.

  1. Set the greeting to a blank.

    curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : " "}' http://localhost:8080/greet/greeting

    Our code to update the greeting accepts this and saves it as the new greeting.

  2. Ping the health check endpoint again with the same command as before.

    curl -X GET http://localhost:8080/health | python -m json.tool

    This time you should see these two parts of the output indicating that something is wrong:

            {
                "data": {
                    "greeting": "not set or is empty"
                },
                "name": "greetingAlive",
                "state": "DOWN"
            }
    ...
        "outcome": "DOWN"

    If you add -i to the curl command and remove the pipe, the output includes the status 503 "Service Unavailable" report:

    curl -i -X GET http://localhost:8080/health
    HTTP/1.1 503 Service Unavailable
    Content-Type: application/json
    Date: Tue, 5 Feb 2019 08:09:22 -0600
    transfer-encoding: chunked
    connection: keep-alive
    ...
  3. Set the greeting back to "Hello", so that the service is healthy again.

    curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hello"}' http://localhost:8080/greet/greeting
  4. Check the health again.

    curl -X GET http://localhost:8080/health | python -m json.tool

    This time the outcome and greetingAlive values will be back to UP.

Add metrics support

As a simple illustration of using metrics, we revise our greeting service to count how many times a client sends a request to the app.

Review default metrics

The generated Main class already instantiates and registers MetricsSupport in the createRouting method. As a result, the system automatically collects and reports a number of measurements related to CPU, threads, memory, and request traffic. Use curl -X GET http://localhost:8080/metrics to get the metrics data.

Revise GreetService.java to add an app-specific metric

  1. Add metrics-related imports.

    import io.helidon.metrics.RegistryFactory;
    import org.eclipse.microprofile.metrics.Counter;
    import org.eclipse.microprofile.metrics.MetricRegistry;
  2. Register a metric in GreetService.java.

    Add these declarations as private fields:

        private final MetricRegistry registry = RegistryFactory.getInstance()
                .getRegistry(MetricRegistry.Type.APPLICATION); // (1)
        private final Counter greetCounter = registry.counter("accessctr"); // (2)
    1. Refers to the application-scoped metrics registry.

    2. Declares a metric of type counter with name accessctr.

  3. Create a method to display which method is handling a request.

    Add this method:

        private void displayThread() {
            String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
            System.out.println("Method=" + methodName + " " + "Thread=" + Thread.currentThread().getName());
        }
  4. Create a request handler to update the counter.

    Add this method:

        private void counterFilter(final ServerRequest request,
                                   final ServerResponse response) {
            displayThread(); // (1)
            greetCounter.inc(); // (2)
            request.next(); // (3)
        }
    1. Shows which method is handling the request.

    2. Updates the counter metric.

    3. Lets the next handler process the same request.

  5. Register the filter to count requests.

    To the update method add this line immediately before the existing get invocations.

    .any(this::counterFilter) // (1)
    1. Invokes counterFilter for any incoming request.

Rebuild and rerun your application

Follow the same steps as before, remembering to stop any instance of your application that is still running.

Send some requests

Use the same curl commands from the beginning to send requests to the server:

Command Server Output
curl -X GET http://localhost:8080/greet
Method=counterFilter Thread=nioEventLoopGroup-3-1
curl -X GET http://localhost:8080/greet/Joe
Method=counterFilter Thread=nioEventLoopGroup-3-2
curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/greet/greeting
Method=counterFilter Thread=nioEventLoopGroup-3-3
curl -X GET http://localhost:8080/greet/Jose
Method=counterFilter Thread=nioEventLoopGroup-3-4

Retrieve metrics

Run this curl command to retrieve the collected metrics:

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

You should see a long response. Note two items:

Output Meaning
 application:accessctr 4

The counter we added to the app

vendor:requests_count 7

The total number of HTTP requests that the Helidon web server received

The requests count is higher because the access to /metrics to retrieve the monitoring data is not handled by our app’s rules and filters but by the metrics infrastructure.

(Optional) Download the example source

Instead of generating and then enhancing the application as described in this guide, you can download it.

  1. Clone the Helidon repository:

    Using ssh
    git clone [email protected]:oracle/helidon.git

    or

    Using HTTPS
    git clone https://github.com/oracle/helidon.git
  2. cd to the helidon/examples/guides/se-restful-webservice directory.

  3. Run:

    mvn package
    java -jar target/se-restful-webservice.jar