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>
Copied

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.

Example of a declarative main class
@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());
    }
}
Copied

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:

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):

Annotations on endpoint type:

Annotations on endpoint methods:

Annotations on method parameters:

Example of an HTTP Server Endpoint
@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();
    }
}
Copied

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:

Annotations on endpoint methods:

Annotations on method parameters:

Example of a Typed HTTP Client
@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();
}
Copied

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:

Example of Fault Tolerance Fallback
@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";
    }
}
Copied

Scheduling

Scheduling allows service methods to be invoked periodically.

Method annotations:

Example of a fixed rate scheduled method
@Service.Singleton
static class CacheService {
    @Scheduling.FixedRate("PT5S")
    void checkCache() {
        // do something every 5 seconds
    }
}
Copied

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>
Copied

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:

Constraints for String and CharSequence:

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.

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:

Constraints for Long and long data types. No other type is supported:

Constraints for Boolean and boolean data type. No other type is supported:

Constraints for collection and map data types:

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.

Supported types for calendar/time validations:

  • java.util.Date

  • java.util.Calendar

  • java.time.Instant

  • java.time.LocalDate

  • java.time.LocalDateTime

  • java.time.LocalTime

  • java.time.MonthDay

  • java.time.OffsetDateTime

  • java.time.OffsetTime

  • java.time.Year

  • java.time.YearMonth

  • java.time.ZonedDateTime

  • java.time.chrono.HijrahDate

  • java.time.chrono.JapaneseDate

  • java.time.chrono.MinguoDate

  • java.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

Example of a validated type
@Validation.Validated
record MyType(@Validation.String.Pattern(".*valid.*") @Validation.NotNull String validString,
              @Validation.Integer.Min(42) int validInt) {
}
Copied
Example of a validated method call using a validated type
@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";
    }
}
Copied

A custom "compound" annotation can be created to simplify usage.

Example of a compound annotation
@Validation.NotNull
@Validation.String.NotBlank
public @interface NonNullNotBlank {
}
Copied

A custom constraint annotation can be created (and act as a compound annotation as well).

Example of a custom constraint annotation
@Validation.NotNull // will add not-null constraint as well
@Validation.Constraint
public @interface CustomConstraint {
}
Copied

For each constraint annotation, there MUST be a service that validates it.

Example of constraint validation provider
@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);
        }
    }
}
Copied

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 authentication

  • io.helidon.security.annotations.Authorized - mark an endpoint or a method as requiring authorization

  • io.helidon.security.annotations.Audited - mark an endpoint or a method as requiring audit logging

  • io.helidon.security.abac.role.RoleValidator.PermitAll - annotated method does not require any authentication or authorization (even if endpoint does)

  • jakarta.annotation.security.PermitAll - same as RoleValidator.PermitAll

  • jakarta.annotation.security.DenyAll - annotated method will not be callable with any kind of authentication or authorization

  • io.helidon.security.abac.role.RoleValidator.Roles - provide a set of roles that can access a resource, implies authentication is required

  • jakarta.annotation.security.RolesAllowed - same as above (RoleValidator.Roles)

Metrics

Add support for the following meters:

  • Counter

  • Timer

  • Gauge

Method annotations:

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).

Example of a counted method with type tags and counter tags
@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
    }
}
Copied

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.

Example of a gauge
@Service.Singleton
static class ServiceWithAGauge {
    private volatile int percentage = 0;

    @Metrics.Gauge(unit = "percent")
    int gauge() {
        return this.percentage;
    }
}
Copied

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:

Notes on defaults:

  • if a kind is defined to other value than INTERNAL, it will be used unless a kind other than INTERNAL is defined on a method annotation (i.e. it is not possible to have SERVER on type, and INTERNAL on 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.

Example of traced type
@Service.Singleton
@Tracing.Traced(tags = @Tracing.Tag(key = "service", value = "TracedService"),
                kind = Span.Kind.SERVER)
static class TracedService {
Copied

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).

Annotated traced method
@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!";
}
Copied

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):

  • io.helidon.websocket.WsSession

  • boolean in 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 headers

  • int (@WebSocket.OnClose) - the close code

  • java.lang.String (@WebSocket.OnClose) - the close reason

  • java.lang.Throwable (@WebSocket.OnError) - the throwable thrown

Annotations on endpoint type:

Annotations on endpoint methods:

Annotations on method parameters:

Example of a WebSocket Server Endpoint
@WebSocketServer.Endpoint
@Http.Path("/websocket/echo")
@Service.Singleton
static class EchoEndpoint {
    @WebSocket.OnMessage
    void onMessage(WsSession session, String message) {
        session.send(message, true);
    }
}
Copied

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, an EchoEndpointFactory will be generated with methods to connect to remote server

Supported method parameters (no annotation required):

  • io.helidon.websocket.WsSession

  • boolean in 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 code

  • java.lang.String (@WebSocket.OnClose) - the close reason

  • java.lang.Throwable (@WebSocket.OnError) - the throwable thrown

Annotations on endpoint type:

Annotations on endpoint methods:

Annotations on method parameters:

Example of a WebSocket Client Endpoint
// 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
    }
}
Copied
Example of a component connecting the websocket
@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);
    }
}
Copied

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):

Example of a CORS protected endpoint
@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() {
    }
}
Copied
  • Configure origins that can be overridden using config key app.cors.allow-origins with 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.