Contents
Overview
Helidon declarative programming model allows inversion of control style programming with all the performance benefits of Helidon SE.
Our declarative approach has the following advantages:
Uses Helidon SE imperative code to implement features (i.e. performance is same as "pure" imperative application)
Generates all the necessary code at build-time, to avoid reflection and bytecode manipulation at runtime
It is based on Helidon Injection
Declarative features are in the same modules as Helidon SE features (i.e. does not require additional dependencies)
Note
Helidon Declarative is an incubating feature. The APIs shown here are subject to change. These APIs will be finalized in a future release of Helidon.
Usage
To create a declarative application, use the annotations provided in our Helidon SE modules (details under Features), and the maven plugin described in Injection: Startup to generate the binding.
In addition, the following section must be added to the build of the Maven pom.xml to enable annotation processors that generate the necessary code:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.helidon.bundles</groupId>
<artifactId>helidon-bundles-apt</artifactId>
<version>${helidon.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>Features
The following features are currently implemented:
A Helidon Declarative application should be started using the generated application binding, to ensure no lookup and no reflection. The call to ServiceRegistryManager.start ensures that all services with a defined RunLevel are started, including Helidon WebServer, Scheduled services etc.
@Service.GenerateBinding // generated binding to bypass discovery and runtime binding
public static class Main {
public static void main(String[] args) {
// configure logging
LogConfig.configureRuntime();
// start the "container"
ServiceRegistryManager.start(ApplicationBinding.create());
}
}Configuration
Configuration can be injected as a whole into any service, or a specific configuration option can be injected using @Configuration.Value. Default values can be defined using annotations in @Default
Services available for injection:
Annotations:
io.helidon.config.Configuration.Value- define the configuration key to inject, on constructor parameterAnnotations defined in
io.helidon.common.Default- define a default typed value, on the same constructor parameter
Example of usage can be seen below in HTTP Server Endpoint example.
HTTP Server Endpoint
To create an HTTP endpoint, simply annotate a class with @RestServer.Endpoint, and add at least one method annotated with one of the HTTP method annotations, such as @Http.GET.
Services available for injection:
N/A
Supported method parameters (no annotation required):
io.helidon.common.security.SecurityContextio.helidon.security.SecurityContext- in casehelidon-securitymodule is on the classpath
Annotations on endpoint type:
io.helidon.webserver.http.RestServer.Endpoint- required annotationio.helidon.webserver.http.RestServer.Listener- to define the named listener this should be served on (named port/socket)io.helidon.webserver.http.RestServer.Header- header to return with each response from this endpointio.helidon.webserver.http.RestServer.ComputedHeader- computed header to return with each response from this endpointio.helidon.http.Http.Path- path (context) this endpoint will be available on
Annotations on endpoint methods:
io.helidon.webserver.http.RestServer.Header- header to return with each response from this methodio.helidon.webserver.http.RestServer.ComputedHeader- computed header to return with each response from this methodio.helidon.webserver.http.RestServer.Status- status to return (if a custom one is required)io.helidon.http.Http.Path- path (context) this method will be available on (subpath of the endpoint path)io.helidon.http.Http.GET(and other methods) - definition of HTTP method this method will serveio.helidon.http.Http.HttpMethod- for custom HTTP method names (mutually exclusive with above)io.helidon.http.Http.Produces- what media type this method produces (return entity content type)io.helidon.http.Http.Consumes- what media type this method accepts (request entity content type)
Annotations on method parameters:
io.helidon.http.Http.Entity- Request entity, a typed parameter is expected, will use HTTP media type modules to coerce into the correct typeio.helidon.http.Http.HeaderParam- Typed HTTP request header valueio.helidon.http.Http.QueryParam- Typed HTTP query valueio.helidon.http.Http.PathParam- Typed parameter from path template
@RestServer.Endpoint // identifies this class as a server endpoint
@Http.Path("/greet") // serve this endpoint on /greet context root (path)
@Service.Singleton // a singleton service (single instance within a service registry)
static class GreetEndpoint {
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());
private final String greeting;
// inject app.greeting configuration value, use "Hello" if not configured
GreetEndpoint(@Configuration.Value("app.greeting") @Default.Value("Hello") String greeting) {
this.greeting = greeting;
}
@Http.GET // HTTP GET endpoint
@Http.Produces(MediaTypes.APPLICATION_JSON_VALUE) // produces entity of application/json media type
public JsonObject getDefaultMessageHandler() {
// build the JSON object (requires `helidon-http-media-jsonp` on classpath)
return JSON.createObjectBuilder()
.add("message", greeting + " World!")
.build();
}
}Typed HTTP Client
To create a typed HTTP client, create an interface annotated with RestClient.Endpoint, and at least one method annotated with one fo the HTTP method annotations, such as @Http.GET. Methods can only have parameters annotated with one of the Http qualifiers.
Annotations on endpoint type:
io.helidon.webclient.api.RestClient.Endpoint- required annotationio.helidon.http.Http.Path- path (context) the server listens onio.helidon.webclient.api.RestClient.Header- header to include in every request to the serverio.helidon.webclient.api.RestClient.ComputedHeader- header to compute and include in every request to the server
Annotations on endpoint methods:
io.helidon.webclient.api.RestClient.Header- header to include in every request to the serverio.helidon.webclient.api.RestClient.ComputedHeader- header to compute and include in every request to the serverio.helidon.http.Http.Path- path (context) the server serves this endpoint method onio.helidon.http.Http.GET(and other methods) - definition of HTTP method this method will invokeio.helidon.http.Http.HttpMethod- for custom HTTP method names (mutually exclusive with above)io.helidon.http.Http.Produces- what media type this method produces (content type of entity from the server)io.helidon.http.Http.Consumes- what media type this method accepts (request entity content type)
Annotations on method parameters:
io.helidon.http.Http.Entity- Request entity, a typed parameter is expected, will use HTTP media type modules to write to the requestio.helidon.http.Http.HeaderParam- Typed HTTP header value to sendio.helidon.http.Http.QueryParam- Typed HTTP query value to sendio.helidon.http.Http.PathParam- Typed parameter from path template to construct the request URI
@RestClient.Endpoint("${greet-service.client.uri:http://localhost:8080}")
@RestClient.Header(name = HeaderNames.USER_AGENT_NAME, value = "my-client")
interface GreetClient {
@Http.GET
@Http.Produces(MediaTypes.APPLICATION_JSON_VALUE)
JsonObject getDefaultMessageHandler();
}Fault Tolerance
Fault tolerance annotations allow adding features to methods on services. The annotations can be added to any method that supports interception (i.e. methods that are not private).
Method Annotations:
io.helidon.faulttolerance.Ft.Retry- allow retriesio.helidon.faulttolerance.Ft.Fallback- fallback to another method that providesio.helidon.faulttolerance.Ft.Async- invoke method asynchronouslyio.helidon.faulttolerance.Ft.Timeout- invoke method with a timeoutio.helidon.faulttolerance.Ft.Bulkhead- use bulkheadio.helidon.faulttolerance.Ft.CircuitBreaker- use circuit breaker
@Service.Singleton
static class AlgorithmService {
@Ft.Fallback(value = "fallbackAlgorithm", applyOn = IOException.class)
String algorithm() throws IOException {
// may throw an exception
return "some-algorithm";
}
// method that would be called if #algorithm fails with an IOException
String fallbackAlgorithm() {
return "default";
}
}Scheduling
Scheduling allows service methods to be invoked periodically.
Method annotations:
io.helidon.scheduling.Scheduling.Cron- execute with schedule defined by a CRON expressionio.helidon.scheduling.Scheduling.FixedRate- execute with a fixed interval
@Service.Singleton
static class CacheService {
@Scheduling.FixedRate("PT5S")
void checkCache() {
// do something every 5 seconds
}
}The following annotation values can use configuration expressions:
Scheduling.Cron#value()Scheduling.Fixed#delayBy()Scheduling.FixedRate#value()
Configuration expressions is a reference to a configuration key, with optional default value:
${config.key:default-value}
Validation
Validation provides an ability to validate service method parameters and return types. This is achieved through constraint annotations and type validation.
To use validation, the proper dependency must be added to your pom.xml, and an annotation processor must be configured to code generate the required classes. The annotation processor is part of the bundle mentioned in Helidon Declarative introduction above.
Helidon validation module:
<dependency>
<groupId>io.helidon.validation</groupId>
<artifactId>helidon-validation</artifactId>
</dependency>Constraint Annotations
A "Constraint Annotation" is any annotation directly annotated with io.helidon.validation.Validation. Helidon Validation provides a set of built-in validation constraints, though custom constraints can be created, or existing constraints can be combined.
Existing constraints:
Constraints for any type:
io.helidon.validation.Validation.NotNull- must not be nullio.helidon.validation.Validation.Null- must be null
Constraints for String and CharSequence:
io.helidon.validation.Validation.Email- matches an e-mail structure (basic check only)io.helidon.validation.Validation.String.NotBlank- must not be blank (empty or only white-space characters)io.helidon.validation.Validation.String.NotEmpty- must not be empty (i.e. length is0)io.helidon.validation.Validation.String.Length- check for maximal (and optionally minimal) lengthio.helidon.validation.Validation.String.Pattern- check against a regular expression
Constraints for types that extend java.lang.Number. These constraints accept any such type, though all types are eventually converted to a BigDecimal and the checks are done against the result. Byte is always converted as an unsigned number, i.e. its values are from 0 to 255 inclusive.
io.helidon.validation.Validation.Number.Negative- the value must be negative (< 0)io.helidon.validation.Validation.Number.NegativeOrZero- the value must be negative or zero (<= 0)io.helidon.validation.Validation.Number.Positive- the value must be positive (> 0)io.helidon.validation.Validation.Number.PositiveOrZero- the value must be positive or zero (>= 0)io.helidon.validation.Validation.Number.Min- the value must be at least the specified minimal value (>= min), value is defined as aStringio.helidon.validation.Validation.Number.Max- the value must be at most the specified maximal value (<= max), value is defined as aStringio.helidon.validation.Validation.Number.Digits- the number must have at most the specified number of integer and fractional digitsio.helidon.validation.Validation.Number.MultipleOf- the value must be evenly divisible by the specified positive factor, which is defined as aString
Constraints for Integer data types. These constraints accept int, long, byte, char, short and their boxed counterparts. byte is always converted as an unsigned number, i.e. its values are from 0 to 255 inclusive. These are convenience annotation that use int data type:
io.helidon.validation.Validation.Integer.Min- the value must be at least the specified minimal value (>= min)io.helidon.validation.Validation.Integer.Max- the value must be at most the specified maximal value (<= max)io.helidon.validation.Validation.Integer.MultipleOf- the value must be evenly divisible by the specified positive integer factor
Constraints for Long and long data types. No other type is supported:
io.helidon.validation.Validation.Long.Min- the value must be at least the specified minimal value (>= min)io.helidon.validation.Validation.Long.Max- the value must be at most the specified maximal value (<= max)io.helidon.validation.Validation.Long.MultipleOf- the value must be evenly divisible by the specified positive long factor
Constraints for Boolean and boolean data type. No other type is supported:
io.helidon.validation.Validation.Boolean.True- the value must betrueio.helidon.validation.Validation.Boolean.False- the value must befalse
Constraints for collection and map data types:
io.helidon.validation.Validation.Collection.Size- the size of the collection or map must be between the minimal and maximal sizes
Constraints for calendar/time data types. Behavior depends on the specific type - for example for Year data type, past is previous year, future is the next year, and present is the current year, regardless of which month it is. When using Instant, past is already the last millisecond.
io.helidon.validation.Validation.Calendar.Future- the value must be in the futureio.helidon.validation.Validation.Calendar.FutureOrPresent- the value must be in the future or nowio.helidon.validation.Validation.Calendar.Past- the value must be in the pastio.helidon.validation.Validation.Calendar.PastOrPresent- the value must be in the past or now
Supported types for calendar/time validations:
java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate
Type Validation
A type annotated with @Validation.Validated will have validation code generated. Usage of that type can be marked with @Validation.Valid - if such an annotation is present, and it is on a field of another validated type, or it is a parameter, return type, or a type argument of a parameter/return type of a service method, the object instance will be validated using a generated interceptor.
Usage
@Validation.Validated
record MyType(@Validation.String.Pattern(".*valid.*") @Validation.NotNull String validString,
@Validation.Integer.Min(42) int validInt) {
}@Service.Singleton
static class ValidatedService {
@Validation.String.NotBlank // validates the response
String process(@Validation.Valid @Validation.NotNull MyType myType) {
// result of the logic
return "some result";
}
}A custom "compound" annotation can be created to simplify usage.
@Validation.NotNull
@Validation.String.NotBlank
public @interface NonNullNotBlank {
}A custom constraint annotation can be created (and act as a compound annotation as well).
@Validation.NotNull // will add not-null constraint as well
@Validation.Constraint
public @interface CustomConstraint {
}For each constraint annotation, there MUST be a service that validates it.
@Service.Singleton
@Service.NamedByType(CustomConstraint.class)
static class CustomConstraintValidatorProvider implements ConstraintValidatorProvider {
@Override
public ConstraintValidator create(TypeName typeName, Annotation constraintAnnotation) {
// we could Validation the type here, but we don't need to - depends on constraint
return new CustomValidator(constraintAnnotation);
}
private static class CustomValidator implements ConstraintValidator {
private final Annotation annotation;
private CustomValidator(Annotation annotation) {
this.annotation = annotation;
}
@Override
public ValidatorResponse check(ValidatorContext context, Object value) {
if (value == null) {
// we leave the `not-null` Validation to the "meta-annotation" on CustomConstraint
return ValidatorResponse.create();
}
// if string, and the value is "good", it is OK
if (value instanceof String str) {
if (str.equals("good")) {
return ValidatorResponse.create();
}
}
return ValidatorResponse.create(annotation, "Must be \"good\" string", value);
}
}
}Security
Security provides protection of WebServer endpoints.
Identity propagation (when using a WebClient) depends on configuration of the client and configuration of security. We currently do not have a declarative way of modifying client behavior.
Supported annotations:
io.helidon.security.annotations.Authenticated- mark an endpoint or a method as requiring authenticationio.helidon.security.annotations.Authorized- mark an endpoint or a method as requiring authorizationio.helidon.security.annotations.Audited- mark an endpoint or a method as requiring audit loggingio.helidon.security.abac.role.RoleValidator.PermitAll- annotated method does not require any authentication or authorization (even if endpoint does)jakarta.annotation.security.PermitAll- same asRoleValidator.PermitAlljakarta.annotation.security.DenyAll- annotated method will not be callable with any kind of authentication or authorizationio.helidon.security.abac.role.RoleValidator.Roles- provide a set of roles that can access a resource, implies authentication is requiredjakarta.annotation.security.RolesAllowed- same as above (RoleValidator.Roles)
Metrics
Add support for the following meters:
Counter
Timer
Gauge
Method annotations:
io.helidon.metrics.api.Metrics.Counted- adds a counter metric to the metric registry for method executionsio.helidon.metrics.api.Metrics.Timed- adds a timer metric to the metric registry for method executionsio.helidon.metrics.api.Metrics.Gauge- marks a method that returns a number as a gauge
In addition, we can use io.helidon.metrics.api.Metrics.Tag annotation on a type, method, or as a tags property of an annotation to add tags to the metric. Tags from type definition will be added to all metrics on the type, tags on methods on all metrics on the method, and tags in the metric annotation will only be used by that metric.
The example below shows additional tags. The counter on method counted will have the following tags: service=Metered;method=counted (and of course the scope tag that is always added).
@Service.Singleton
@Metrics.Tag(key = "service", value = "Metered")
static class MeteredService {
@Metrics.Counted(tags = @Metrics.Tag(key = "method", value = "counted"))
void counted() {
// whenever invoked through service interface, counter is incremented
}
}A gauge is a method that returns a Number, and is invoked by the metrics implementation to obtain a value. Example below shows a definition of a Gauge. Note that a unit is mandatory for gauges.
@Service.Singleton
static class ServiceWithAGauge {
private volatile int percentage = 0;
@Metrics.Gauge(unit = "percent")
int gauge() {
return this.percentage;
}
}Tracing
Add support for tracing of methods. This feature will add a new span for each annotated method (or all methods on an annotated type).
Annotations:
io.helidon.tracing.Tracing.Traced- all methods on the annotated type are will be traced, or the annotated method will be tracedio.helidon.tracing.Tracing.ParamTag- the annotated method parameter will be added as a tag to the span
Notes on defaults:
if a
kindis defined to other value thanINTERNAL, it will be used unless akindother thanINTERNALis defined on a method annotation (i.e. it is not possible to haveSERVERon type, andINTERNALon method)span name defaults to
fully-qualified-class-name.method-name
The following example shows annotation on a type. This would make all methods traced with span kind of SERVER, and with a tag service with value TracedService.
@Service.Singleton
@Tracing.Traced(tags = @Tracing.Tag(key = "service", value = "TracedService"),
kind = Span.Kind.SERVER)
static class TracedService {A traced method with an explicit span name, adding a tag with a constant value, and a tag with a value from annotated parameter. The tag name defaults to parameter name (userAgent in this case).
@Http.GET
@Http.Path("/greet")
@Tracing.Traced(value = "explicit-name", tags = @Tracing.Tag(key = "custom", value = "customValue"))
String greet(@Http.HeaderParam("User-Agent") @Tracing.ParamTag String userAgent) {
return "Hello!";
}WebSocket Server
To create a WebSocket endpoint, simply annotate a class with @WebSocketServer.Endpoint, and add at least one method annotated with one of the WebSocket method annotations, such as @WebSocket.OnMessage.
Services available for injection:
N/A
Supported method parameters (no annotation required):
booleanin a method annotated with@WebSocket.OnMessage- indicator of "last" message (if not present, the message will be combined before delivery)java.lang.String(@WebSocket.OnMessage) - the message delivered (text)java.io.Reader(@WebSocket.OnMessage) - the message delivered (text)io.helidon.common.buffers.BufferData(@WebSocket.OnMessage) - the message delivered (binary)java.nio.ByteBuffer(@WebSocket.OnMessage) - the message delivered (binary)java.io.InputStream(@WebSocket.OnMessage) - the message delivered (binary)io.helidon.http.HttpPrologue(`@WebSocket.OnHttpUpgrade) - the HTTP prologue (method, path, protocol version)io.helidon.http.Headers(`@WebSocket.OnHttpUpgrade) - the request headersint(@WebSocket.OnClose) - the close codejava.lang.String(@WebSocket.OnClose) - the close reasonjava.lang.Throwable(@WebSocket.OnError) - the throwable thrown
Annotations on endpoint type:
io.helidon.webserver.websocket.WebSocketServer.Endpoint- required annotationio.helidon.webserver.websocket.WebSocketServer.Listener- to define the named listener this should be served on (named port/socket)io.helidon.http.Http.Path- path (context) this endpoint will be available on
Annotations on endpoint methods:
io.helidon.websocket.WebSocket.OnMessage- receives either a binary or a text messageio.helidon.websocket.WebSocket.OnHttpUpgrade- invoked during HTTP upgrade, the method may returnHeadersto be sent during the upgrade responseio.helidon.websocket.WebSocket.OnOpen- invoked when the WebSocket connection is established (after upgrade)io.helidon.websocket.WebSocket.OnClose- invoked when the WebSocket connection is closedio.helidon.websocket.WebSocket.OnError- invoked when an error occurs when invoking other methods
Annotations on method parameters:
io.helidon.http.Http.PathParam- Typed parameter from path template
@WebSocketServer.Endpoint
@Http.Path("/websocket/echo")
@Service.Singleton
static class EchoEndpoint {
@WebSocket.OnMessage
void onMessage(WsSession session, String message) {
session.send(message, true);
}
}WebSocket Client
To create a WebSocket client endpoint, simply annotate a class with @WebSocketClient.Endpoint, and add at least one method annotated with one of the WebSocket method annotations, such as @WebSocket.OnMessage.
Services available for injection:
a factory for the endpoint (generated), if endpoint is named
EchoEndpoint, anEchoEndpointFactorywill be generated with methods to connect to remote server
Supported method parameters (no annotation required):
booleanin a method annotated with@WebSocket.OnMessage- indicator of "last" message (if not present, the message will be combined before delivery)java.lang.String(@WebSocket.OnMessage) - the message delivered (text)java.io.Reader(@WebSocket.OnMessage) - the message delivered (text)io.helidon.common.buffers.BufferData(@WebSocket.OnMessage) - the message delivered (binary)java.nio.ByteBuffer(@WebSocket.OnMessage) - the message delivered (binary)java.io.InputStream(@WebSocket.OnMessage) - the message delivered (binary)int(@WebSocket.OnClose) - the close codejava.lang.String(@WebSocket.OnClose) - the close reasonjava.lang.Throwable(@WebSocket.OnError) - the throwable thrown
Annotations on endpoint type:
io.helidon.webclient.websocket.WebSocketClient- required annotationio.helidon.http.Http.Path- path (context) this endpoint will be available on
Annotations on endpoint methods:
io.helidon.websocket.WebSocket.OnMessage- receives either a binary or a text messageio.helidon.websocket.WebSocket.OnHttpUpgrade- invoked during HTTP upgrade, the method may returnHeadersto be sent during the upgrade responseio.helidon.websocket.WebSocket.OnOpen- invoked when the WebSocket connection is established (after upgrade)io.helidon.websocket.WebSocket.OnClose- invoked when the WebSocket connection is closedio.helidon.websocket.WebSocket.OnError- invoked when an error occurs when invoking other methods
Annotations on method parameters:
io.helidon.http.Http.PathParam- Typed parameter from path template defined by@Http.Pathon the class
// will use `ws.connection` configuration key, and if not present, default to http://localhost:8080
@WebSocketClient.Endpoint("${ws.connection:http://localhost:8080}")
@Http.Path("/echo/{count}")
@Service.Singleton
static class EchoClient {
@WebSocket.OnMessage
void onMessage(WsSession session, String message, @Http.PathParam("count") int count) {
// do something with the message
}
}@Service.Singleton
static class EchoClientUser {
private final EchoClientFactory clientFactory;
@Service.Inject
EchoClientUser(EchoClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
void handle(int count) {
// the clientFactory and the method we are invoking are code generated
// this will start the websocket session (the method returns once the session is initiated)
clientFactory.connect(count);
}
}WebServer CORS
CORS can be configured through Helidon Config, the root key is cors.
To add an explicit CORS (Cross-origin resource sharing) configuration to an endpoint method, you may annotate it with one of the annotations in the Cors class, such as @Cors.Defaults.
Annotations on endpoint method (must be an OPTIONS method):
io.helidon.webserver.cors.Cors.Defaults- support all methods, all origins, do not combine with other annotations fromCorsclassio.helidon.webserver.cors.Cors.AllowOrigins- configure allowed origins, either as a exact string, or regular expression (if the value contains\,*or{, it is considered a regular expression)io.helidon.webserver.cors.Cors.AllowMethods- set of allowed methods that the different origin script can useio.helidon.webserver.cors.Cors.AllowHeaders- set of allowed headers sent from the different origin scriptio.helidon.webserver.cors.Cors.ExposeHeaders- set of headers from response exposed to the different origin scriptio.helidon.webserver.cors.Cors.AllowCredentials- whether to add credentials (such as Cookie) to requests from the different origin scriptio.helidon.webserver.cors.Cors.MaxAgeSeconds- maximal number of seconds the pre-flight is considered valid
@Service.Singleton
@Http.Path("/cors")
static class CorsEndpoint {
@Http.OPTIONS
@Cors.AllowOrigins("${app.cors.allow-origins:http://foos.bar,http://bars.foo}")
@Cors.AllowHeaders({"X-foo", "X-bar"})
@Cors.AllowMethods({Method.DELETE_NAME, Method.PUT_NAME, "LIST"})
@Cors.MaxAgeSeconds(180)
void options() {
}
}- Configure origins that can be overridden using config key
app.cors.allow-originswith the provided default values (comma separated) - Configure headers the script can send to this host
- Configure allowed methods for CORS requests
- Configure max age to be 3 minutes
Health Checks
To add a declarative health check, create a service that implements io.helidon.health.HealthCheck or produces an instance of it. The WebServer health observer discovers all such services and uses them to contribute to the health response. Because the lookup is performed only once, you must not use the @Service.PerRequest scope. The recommended scope is @Service.Singleton.