Create and build a RESTful web service as your first Helidon SE application.
You’ll learn how to use Helidon quickly to create a RESTful web service that accepts these HTTP requests:
Method and URL | Result |
---|---|
|
Returns a generic but friendly greeting |
|
Returns a personalized greeting for the specified person |
|
Changes the greeting used in subsequent responses |
You’ll create the app in three main steps:
-
Use the Helidon Maven archetype to create a basic Helidon SE app that responds to the HTTP requests.
-
Add code to perform a simple app-specific health check.
-
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.
Helidon provides a Maven archetype you can use to create a new Helidon project that includes sample code.
-
cd
to a directory that is not already a Maven project. -
Run this command:
Creating a new Helidon SE projectmvn 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 theartifactId
setting from the archetype invocation) that contains a new Maven project for a Helidon service.
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>
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
-
Sets the initial greeting text for responses from the service
-
Sets how the service will listen for requests
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
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
.
-
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.
-
The
GreetService
class implementsio.helidon.webserver.Service
. -
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.
-
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) }
-
Handle
GET
requests that contain no extra path usinggetDefaultMessage
. -
Handle
GET
requests that contain a name usinggetMessage
, which personalizes the response using the name provided as the path suffix. -
Handle
PUT
requests to thegreeting
path usingupdateGreeting
, interpreting thegreeting
value in the JSON payload as the new greeting string.
-
-
The following methods respond to the three types of request.
-
Returning the default greeting:
private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) { sendResponse(response, "World"); //(1) }
-
The default message ends with "World!" — that is, without personalizing the message with the user’s name.
-
-
Returning a personalized greeting:
private void getMessageHandler(ServerRequest request, ServerResponse response) { String name = request.path().param("name"); //(1) sendResponse(response, name); //(2) }
-
Gets the name from the URL path in the request.
-
Includes the user’s name in building the response.
-
-
Updating the greeting:
private void updateGreetingHandler(ServerRequest request, ServerResponse response) { request.content().as(JsonObject.class) // (1) .thenAccept(jo -> updateGreetingFromJson(jo, response)); }
-
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(); }
-
Rejects the request if the JSON payload does not include the
greeting
setting. -
Extracts the new greeting from the JSON object and saves it.
-
Sends the "no content" response, acknowledging that the new greeting has been set.
-
-
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.
-
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)
-
Loads configuration from
application.yaml
. -
Creates the
ServerConfiguration
object from the relevant part of theConfig
object just loaded. -
Creates the server using the config and the updated routing rules (see below).
-
-
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.
-
Starts the server.
-
When the startup completes successfully, prints a message.
-
Prints a message when the server is shut down. The
CompletionStage
returned fromserver.whenShutdown()
completes when some other code invokesserver.shutdown()
. The current example does not invoke that method (except from a test), so in this example server theCompletionStage
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. -
Report a failed startup.
-
-
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(); }
-
Sets up several built-in health checks (deadlock, disk space, heap memory) for the server.
-
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.
-
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 |
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.
-
Add health-related imports.
import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
-
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) }
-
Use a builder to assemble the response, giving the health check a human-readable name.
-
Enforce that the greeting be non-empty and non-null in order for the greeting service to be considered alive.
-
For a null or empty greeting the response indicates that the service is down, in this case adding an explanation.
-
For a valid greeting the response says the service is up.
-
Either way, have the builder build the response.
-
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();
}
-
The
health
instance now includes the greet service liveness check. -
The returned routing refers to the previously-instantiated and saved
GreetService
instance.
-
Stop any running instance of your app.
-
Rebuild the app and then run it.
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).
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.
-
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.
-
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 thecurl
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 ...
-
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
-
Check the health again.
curl -X GET http://localhost:8080/health | python -m json.tool
This time the
outcome
andgreetingAlive
values will be back toUP
.
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.
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.
-
Add metrics-related imports.
import io.helidon.metrics.RegistryFactory; import org.eclipse.microprofile.metrics.Counter; import org.eclipse.microprofile.metrics.MetricRegistry;
-
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)
-
Refers to the application-scoped metrics registry.
-
Declares a metric of type
counter
with nameaccessctr
.
-
-
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()); }
-
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) }
-
Shows which method is handling the request.
-
Updates the counter metric.
-
Lets the next handler process the same request.
-
-
Register the filter to count requests.
To the
update
method add this line immediately before the existingget
invocations..any(this::counterFilter) // (1)
-
Invokes
counterFilter
for any incoming request.
-
Follow the same steps as before, remembering to stop any instance of your application that is still running.
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 |
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.
Instead of generating and then enhancing the application as described in this guide, you can download it.
-
Clone the Helidon repository:
Using sshgit clone [email protected]:oracle/helidon.git
or
Using HTTPSgit clone https://github.com/oracle/helidon.git
-
cd
to thehelidon/examples/guides/se-restful-webservice
directory. -
Run:
mvn package java -jar target/se-restful-webservice.jar