Helidon project that uses both Helidon MP and Helidon SE microservices.
- Maven 3.5+
- Java 8+
- Optional - docker
Prepare a directory that will hold your projects. Once within this directory, use the following commands to generate the SE and MP Helidon projects.
The commands are the same ones as used in our quides: https://helidon.io/docs/latest/#/guides/01_overview
Helidon SE - Linux and MacOS
mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=1.3.0 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-quickstart-se \
-Dpackage=io.helidon.examples.quickstart.se
Helidon MP - Linux and MacOS
mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-mp \
-DarchetypeVersion=1.3.0 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-quickstart-mp \
-Dpackage=io.helidon.examples.quickstart.mp
On Windows, please remove the backslashes and end of lines
You can verify the projects are correctly created as follows:
cd helidon-quickstart-se
mvn clean package
java -jar target/helidon-quickstart-se.jar
# Now you can excercise endpoints from a browser or curl
# Afterwards Ctrl-c to end the program
cd ../helidon-quickstart-mp
mvn clean package
java -jar target/helidon-quickstart-mp.jar
# Now you can excercise endpoints from a browser or curl
# Afterwards Ctrl-c to end the program
curl
commands can be found in README.md
in each project
Open the project directory in your favorite IDE and add the two pom.xml
maven files as modules (depends on IDE used).
When the projects are created, both run on port 8080
. As we want to run
both in parallel, let's modify the listen port of the MP service to 8081
.
In our example, we use the MP specific configuration file
helidon-quickstart-mp/src/main/resources/META-INF/microprofile-config.properties
.
Modify the server.port
property to value 8081
Now we can run both project in parallel without conflicts.
As we have seen, MP uses the microprofile-config.properties
.
In addition we can use YAML
configuration files, as we have the module on
classpath (explicitly in our SE application):
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml</artifactId>
</dependency>
This dependency means that our applications will read application.yaml
files from
the classpath (see SE configuration file helidon-quickstart-se/src/main/resources/application.yaml
).
We now want to add additional files to be able to override configuration on each environment.
Let's add conf/se.yaml
and conf/mp.yaml
files as
configuration sources.
As we want to read these as files, we either must configure the path as absolute, or start the application from the correct directory.
Let's add a buildConfig
method to the Main
class of Helidon SE.
The source code defines:
- the file (relative path)
pollinStrategy
- watching the file for changes, application can listen on such changesoptional
- the startup sequence will not fail if file is missing- we have also added a
se-test.yaml
optional configuration to allow unit tests to override configuration (such as security), so our unit tests can run on a different port and use different security
import io.helidon.config.PollingStrategies;
import static io.helidon.config.ConfigSources.classpath;
import static io.helidon.config.ConfigSources.file;
//...
private static Config buildConfig() {
return Config.builder()
.sources(
classpath("se-test.yaml").optional(),
file("../conf/se.yaml")
.pollingStrategy(PollingStrategies::watch)
.optional(),
classpath("application.yaml"))
.build();
}
Now we need to modify the configuration used. The line
Config config = Config.create();
must be changed to
Config config = buildConfig();
Now if we start our application, nothing is changed.
Let's create the conf/se.yaml
file with the following content:
app:
greeting: "Hallo"
Now after restart, the message should be changed.
If the application is started from the helidon-quickstart-se
folder, the
configuration is correctly located.
Let's add a buildConfig
method to the Main
class of Helidon MP.
The source code defines:
- the file (relative path)
pollinStrategy
- watching the file for changes, application can listen on such changesoptional
- the startup sequence will not fail if file is missing- the
application.yaml
is also defined as optional, as we do not use it (yet)
import io.helidon.config.PollingStrategies;
import static io.helidon.config.ConfigSources.classpath;
import static io.helidon.config.ConfigSources.file;
//...
private static Config buildConfig() {
return Config.builder()
.sources(
file("../conf/mp.yaml")
.pollingStrategy(PollingStrategies::watch)
.optional(),
classpath("application.yaml").optional(),
classpath("META-INF/microprofile-config.properties"))
.build();
}
Now we need to modify the configuration used by Server. The line:
return Server.create().start()
must be changed to:
return Server.builder()
.config(buildConfig())
.build()
.start();
Let's create the conf/mp.yaml
file with the following content:
app:
greeting: "MP Hallo"
Validate that the configuration was used by our MP application.
If the application is started from the helidon-quickstart-mp
folder, the
configuration is correctly located.
Let us modify our SE application to react on changed configuration.
Go to constructor of GreetService
and change its code:
Config greetingConfig = config.get("app.greeting");
// initial value
greeting.set(greetingConfig.asString().orElse("Ciao"));
// on change listener
greetingConfig.onChange((Consumer<Config>) cfg -> greeting.set(cfg.asString().orElse("Ciao")));
Now run the application and check the message (it should be "Hallo World!").
If you modify the se.yaml
file and change the greeting to "SE Hallo", the message
return will change to "SE Hallo World!"
On MacOs, please give it a few seconds.
Metrics are already enabled in both projects:
Let's add custom metrics to our applications.
To add a new metric in MP, simply annotate the JAX-RS resource with one of the annotations.
Let's modify GreetResource.getDefaultMessage
:
import org.eclipse.microprofile.metrics.annotation.Timed;
//...
@GET
@Produces(MediaType.APPLICATION_JSON)
@Timed
public JsonObject getDefaultMessage() {
return createResponse("World");
}
Restart the application and access the endpoint (http://localhost:8081/greet).
Then validate the metric is present:
curl -H "Accept: application/json" http://localhost:8081/metrics/application
Expected result is similar to this:
{
"io.helidon.examples.quickstart.mp.GreetResource.getDefaultMessage" : {
"oneMinRate" : 0.00821011325831546,
"p95" : 24025911,
"max" : 24025911,
"fifteenMinRate" : 0.00105986292046305,
"stddev" : 0,
"mean" : 24025911,
"p50" : 24025911,
"count" : 1,
"p75" : 24025911,
"p999" : 24025911,
"p98" : 24025911,
"meanRate" : 0.0141106839296138,
"min" : 24025911,
"fiveMinRate" : 0.00289306852357793,
"p99" : 24025911
}
}
In a similar way, we could use most of the metrics from MP Metrics specification:
Counted
Metered
Timed
In SE, there is no injection or annotation processing, so to add a metric, we need to do so by hand.
We will modify the constructor of our GreetService
again to create the metric. We will
also need to update the getDefaultMessageHandler
to use the metric.
// field
private final Timer defaultMessageTimer;
//...
GreetService(Config config) {
// our configuration code
// ...
RegistryFactory metricsRegistry = RegistryFactory.getInstance();
MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION);
this.defaultMessageTimer = appRegistry.timer("greet.default.timer");
}
//...
private void getDefaultMessageHandler(ServerRequest request,
ServerResponse response) {
Timer.Context timerContext = defaultMessageTimer.time();
sendResponse(response, "World");
response.whenSent()
.thenAccept(res -> timerContext.stop());
}
Restart the application and access the endpoint (http://localhost:8080/greet).
Then validate the metric is present:
curl -H "Accept: application/json" http://localhost:8080/metrics/application
Expected result is similar to this:
{
"greet.default.timer" : {
"count" : 4,
"max" : 99604487,
"p98" : 99604487,
"fiveMinRate" : 0.0127893407850335,
"p75" : 2421876,
"p95" : 99604487,
"min" : 1267507,
"mean" : 25662839.2921554,
"oneMinRate" : 0.0541447534553673,
"stddev" : 41846753.4348158,
"fifteenMinRate" : 0.0043831483778439,
"p50" : 2265093,
"p999" : 99604487,
"meanRate" : 0.159544963048957,
"p99" : 99604487
}
}
In a similar way, we could use most of the metrics from MP Metrics specification:
Timer
Counter
Meter
Health checks are already enabled in both projects.
Original MP Health endpoints (all healthchecks available):
New MP Health endpoints:
Readiness checks: http://localhost:8080/health/ready http://localhost:8081/health/ready
Liveness checks: http://localhost:8080/health/live http://localhost:8081/health/live
Let's add custom health check to our applications.
Adding a custom health check in MP utilizes CDI.
Simply create a new class GreetHealthcheck
with the following content:
package io.helidon.examples.quickstart.mp;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
@Liveness
@ApplicationScoped
public class GreetHealthcheck implements HealthCheck {
private GreetingProvider provider;
@Inject
public GreetHealthcheck(GreetingProvider provider) {
this.provider = provider;
}
@Override
public HealthCheckResponse call() {
String message = provider.getMessage();
return HealthCheckResponse.named("greeting")
.state("Hello".equals(message))
.withData("greeting", message)
.build();
}
}
After restarting the application and checking the health check endpoint, we should
see the application is DOWN
, as the greeting is "MP Hallo" instead of "Hello".
In MP we do not listen on configuration changes, so to fix the greeting, we can use
the update greeting endpoint (that changes the greeting in memory):
curl -i -X PUT -H "Content-Type: application/json" -d '{"greeting": "Hello"}' http://localhost:8081/greet/greeting
The next request to health endpoint should return UP
.
Our new health check also provides its result in the liveness checks on
http://localhost:8081/health/live
Custom health checks may be added when creating the HealthSupport
instance
in our SE Main
class in method createRouting
.
Let's add a health check that is always up and just sends the current time in millis:
HealthSupport health = HealthSupport.builder()
.addLiveness(HealthChecks.healthChecks()) // Adds a convenient set of checks
.addLiveness(() -> HealthCheckResponse.named("custom") // a custom (liveness) health check
.up()
.withData("timestamp", System.currentTimeMillis())
.build())
.build();
Restart the application and verify the health endpoint, that it contains the new health check. As we have added all the health checks to liveness, we can also see them in http://localhost:8080/health/live
We will use a JAX-RS client to connect from our MP service to the SE service.
Let's modify the GreetResource
.
Add a web target:
import javax.ws.rs.client.WebTarget;
import org.glassfish.jersey.server.Uri;
//...
@Uri("http://localhost:8080/greet")
private WebTarget target;
And a new resource method to handle the outbound call:
@GET
@Path("/outbound/{name}")
public JsonObject outbound(@PathParam("name") String name) {
return target.path(name)
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(JsonObject.class);
}
Now restart the MP application and call the endpoint:
curl -i http://localhost:8081/greet/outbound/jack
We should get:
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 10 Jul 2019 16:11:55 +0200
connection: keep-alive
content-length: 28
{"message":"SE Hallo jack!"}%
We have a choice for Helidon SE of using the HTTP client in Java (available since version 11), or any reactive/asynchronous HTTP client.
For our example we will use JAX-RS reactive client from Jersey.
This adds a few dependencies to our project.
Create a dependencyManagement
node in your pom.xml
:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>2.29.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
And then update the dependencies
node by adding the following dependencies to it:
These belong under dependencies
NOT under dependencyManagement/dependencies
<dependency>
<groupId>io.helidon.security.integration</groupId>
<artifactId>helidon-security-integration-jersey</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-jersey-client</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
Add a Client
and WebTarget
to the GreetService
:
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
//...
private static final Client JAX_RS_CLIENT = ClientBuilder.newClient();
private final WebTarget webTarget;
Update constructor to configure the WebTarget
:
webTarget = JAX_RS_CLIENT.target("http://localhost:8081/greet");
Let's add a new routing method to our GreetService
in update(Rules)
method:
rules
.get("/", this::getDefaultMessageHandler)
.get("/outbound", this::outbound)
.get("/{name}", this::getMessageHandler)
.put("/greeting", this::updateGreetingHandler);
And create the outbound method itself:
private void outbound(ServerRequest request, ServerResponse response) {
// and reactive jersey client call
webTarget.request()
.rx()
.get(String.class)
.thenAccept(response::send)
.exceptionally(throwable -> {
// process exception
response.status(Http.Status.INTERNAL_SERVER_ERROR_500);
response.send("Failed with: " + throwable);
return null;
});
}
To use tracing with Helidon, we need to connect the services to a tracer. Helidon supports "Zipkin" and "Jaeger" tracers. For our examples, we will use Zipkin server.
If this is the first time you use Zipkin:
docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin
If you already have the container ready:
docker start zipkin
The Zipkin UI is available on: http://localhost:9411/zipkin/
We need to add the integration library to pom.xml
:
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-zipkin</artifactId>
</dependency>
and we need to configure the tracing service name (let's add it to microprofile-config.properties
):
tracing.service=helidon-mp
We need to add the Zipkin integration libraries to pom.xml
(transitively depends on a tracer abstraction library):
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-zipkin</artifactId>
</dependency>
and configure the tracing service name (in application.yaml
):
tracing.service: "helidon-se"
As the last step, we need to configure the tracer with Helidon WebServer:
In SE Main.startServer()
:
ServerConfiguration serverConfig =
ServerConfiguration.builder(config.get("server"))
.tracer(TracerBuilder.create(config.get("tracing")).build())
.build();
Now we have both SE and MP service connected to Zipkin, we can invoke requests on each and see the traces.
To see the true power of tracing, invoke the outbound service:
curl -i http://localhost:8081/greet/outbound/jack
And see the trace in the tracer.
Fault tolerance is currently available in MP only, as it heavily depends on annotations.
To see the power of fault tolerance, let's shut down the SE service and invoke
our favorite outbound endpoint. You should get:
HTTP/1.1 500 Internal Server Error
, as the request with the client fails and
we do not have any error handling in place.
We can now add a Fallback
annotation to the GreetResource.outbound
method:
@Fallback(fallbackMethod = "outboundFailed")
and create the fallback method:
public JsonObject outboundFailed(String name) {
return Json.createObjectBuilder().add("Failed", name).build();
}
Note that the signature (parameters and response type) must be exactly the same as for the original method
Restart the MP service and try the call again. This time the response should be:
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 10 Jul 2019 16:35:00 +0200
connection: keep-alive
content-length: 17
{"Failed":"jack"}%
This Fault Tolerance annotation is one of many, you can use:
Fallback
CircuitBreaker
Bulkhead
Retry
Timeout
See the MP Fault Tolerance spec for details: https://github.com/eclipse/microprofile-fault-tolerance/releases/download/2.0/microprofile-fault-tolerance-spec-2.0.html
GraalVM provides a feature of ahead-of-time compilation into native code.
This is supported by Helidon SE (with some restrictions). The quickstart example
is capable of compilation using native-image
.
There are two options:
- Compile using local installation of GraalVM
- Compile using docker image into a docker image
We will use the second approach. Start in the directory of the SE service:
docker build -t helidon-quickstart-se-native -f Dockerfile.native .
The first build takes a bit longer, as it downloads necessary libraries from Maven central into the docker image. Further builds use the downloaded libraries.
The above command creates a docker image helidon-quickstart-se-native
.
To run it locally, shut down SE service and run:
docker run --rm -p 8080:8080 helidon-quickstart-se-native:latest
Recommended approach is to configure security in a configuration file. As security requires more complex configuration, using a yaml file is required (unless you prefer very cryptic files).
We will secure our services as follows:
MP Service
- Authentication: HTTP Basic authentication (NEVER use this in production)
- Authorization: Role based access control
- Identity propagation:
- HTTP Basic authentication (user)
- HTTP Signatures (service)
SE Service
- Authentication:
- HTTP Basic authentication (user)
- HTTP Signatures (service)
- Authorization: Role based access control
The common configuration (exactly the same in SE and MP) uses the ABAC and Basic authentication providers:
security:
providers:
# enable the "ABAC" security provider (also handles RBAC)
- abac:
# enabled the HTTP Basic authentication provider
- http-basic-auth:
realm: "helidon"
users:
- login: "jack"
password: "password"
roles: ["admin"]
- login: "jill"
password: "password"
roles: ["user"]
- login: "joe"
password: "password"
Once the above configuration is added to the mp.yaml
, we can try if security works.
Let's modify our GreetResource.outbound
method.
This method will be available to users in role user
or admin
@GET
@Path("/outbound/{name}")
@Fallback(fallbackMethod = "outboundFailed")
@RolesAllowed({"user", "admin"})
public JsonObject outbound(@PathParam("name") String name) {
If the application is restarted and you invoke the endpoint
curl -i http://localhost:8081/greet/outbound/jack
You get the following response:
HTTP/1.1 403 Forbidden
Content-Length: 0
Authorization itself does not imply authentication. Simple way to
enforce authentication is to annotate either class or method as @Authenticated
:
import io.helidon.security.annotations.Authenticated;
//...
@GET
@Path("/outbound/{name}")
@Fallback(fallbackMethod = "outboundFailed")
@RolesAllowed({"user", "admin"})
@Authenticated
public JsonObject outbound(@PathParam("name") String name) {
Now when we restart and re-request the endpoint, we get:
HTTP/1.1 401 Unauthorized
Content-Length: 0
...
Now we can request the endpoint as any user in user
or admin
role.
You can try the following commands to see the results:
curl -i -u jack:password http://localhost:8081/greet/outbound/Stuttgart
curl -i -u jill:password http://localhost:8081/greet/outbound/Stuttgart
curl -i -u joe:password http://localhost:8081/greet/outbound/Stuttgart
curl -i -u john:password http://localhost:8081/greet/outbound/Stuttgart
We should see that jack
and jill
get the response, joe
is forbidden (unauthorized)
and john
is unauthorized (meaning unauthenticated).
Also investigate the traces in Zipkin, as you should nicely see what happened.
Let's modify our method to use the username of the logged in user. We will
remove the path parameter and instead use the current username.
Note that you also need to update the outboundFailed
fallback method, as the signature changes.
Also we send the current security context, so security can be propagated.
import io.helidon.security.SecurityContext;
//...
@GET
@Path("/outbound")
@Fallback(fallbackMethod = "outboundFailed")
@RolesAllowed({"user", "admin"})
@Authenticated
public JsonObject outbound(@Context SecurityContext context) {
return target.path(context.userName())
.request()
.property(ClientSecurityFeature.PROPERTY_CONTEXT, context)
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(JsonObject.class);
}
public JsonObject outboundFailed(SecurityContext context) {
return Json.createObjectBuilder()
.add("Failed", context.userName())
.build();
}
You can try the following commands to see the results:
curl -i -u jack:password http://localhost:8081/greet/outbound
curl -i -u jill:password http://localhost:8081/greet/outbound
curl -i -u joe:password http://localhost:8081/greet/outbound
curl -i -u john:password http://localhost:8081/greet/outbound
Before connecting to SE, we need to add the following code to our Main
class of MP, to support security propagation:
// as we use default HTTP connection for Jersey client, we should set this as we set the Authorization header
// when propagating security
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
Dependencies:
<dependency>
<groupId>io.helidon.security.integration</groupId>
<artifactId>helidon-security-integration-webserver</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security.providers</groupId>
<artifactId>helidon-security-providers-abac</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security.providers</groupId>
<artifactId>helidon-security-providers-http-auth</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security.providers</groupId>
<artifactId>helidon-security-providers-http-sign</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-object-mapping</artifactId>
</dependency>
In SE, we need to explicitly add Security to configuration: please refer to the source code of this module to enable security in SE
Then we can add security to WebServer routing in Main
class, method createRouting
:
return Routing.builder()
.register(JsonSupport.create())
.register(WebSecurity.create(config.get("security")))
.register(health) // Health at "/health"
.register(metrics) // Metrics at "/metrics"
.register("/greet", greetService)
.build();
Helidon WebServer has the concept of named ports that can have routings assigned to them.
In Helidon MP, we can run our main application on the default port (all JAX-RS resources) and assign some of the
MP "management" endpoints to different ports.
The following configuration (you can add this to conf/mp.yaml
) will move metrics and health check endpoints to port
9081
(this is commented out in the file in this project, so previous examples work nicely)
server:
port: 8081
host: "localhost"
sockets:
admin:
port: 9081
bind-address: "localhost"
metrics:
routing: "admin"
health:
routing: "admin"
After restarting the MP server, you can find metrics and health on the following endpoints: http://localhost:9081/health http://localhost:9081/metrics