Declarative

Helidon Declarative

Overview

Helidon declarative programming model allows inversion of control style programming with all the performance benefits of Helidon.

Our declarative approach has the following advantages:

  • Uses Helidon 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 features (i.e. does not require additional dependencies)
Helidon Declarative is a preview feature. It is ready for production use. Its APIs will remain backward compatible within a major version, but may change without the usual deprecation process in a new major version.

Usage

To create a declarative application, use the annotations provided in our Helidon 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:

pom.xml
<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>

When using declarative gRPC server endpoints or typed gRPC clients, add the Helidon gRPC API dependency to the project’s compile classpath:

pom.xml
<dependency>
  <groupId>io.helidon.grpc</groupId>
  <artifactId>helidon-grpc-api</artifactId>
</dependency>

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());
    }
}

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

If an endpoint method uses ServerResponse directly, generated response metadata such as status, content type, and declarative response headers is configured before the method is invoked. This is equivalent to imperative code configuring the same ServerResponse with status(...), headers().contentType(...), and header(...) before handling the response.

The metadata belongs to the current response and is not automatically cleared if the method later calls next(), calls reroute(...), or throws an exception; later route or error handling can change it before sending the response. When using ServerResponse.outputStream(), close the returned stream before leaving the endpoint method.

For void endpoint methods, the generated handler only sends the response automatically if the method did not already handle it. Methods that return a value still send the returned entity.

Annotations on endpoint type:

Annotations on endpoint methods:

For a QUERY endpoint, use @Http.QUERY with @Http.Entity for the query content and @Http.Consumes for its required media type. The application still defines the query format and validates that the content matches that media type.

Annotations on method parameters:

Http.FormParam is backed by the request entity and should be used with @Http.Consumes(MediaTypes.APPLICATION_FORM_URLENCODED_VALUE). It cannot be combined with Http.Entity on the same declarative method. Each endpoint method parameter may have at most one supported request parameter annotation.

Http.RequestParams is supported only for record parameter types. This is a restriction on the parameter types supported by declarative code generation. Each record component must either be a supported typed parameter that does not require an annotation, such as ServerRequest, or have one of these component annotations: Http.HeaderParam, Http.CookieParam, Http.QueryParam, Http.FormParam, Http.PathParam, or Http.Entity. Components must not combine supported request parameter annotations. At most one Http.Entity component is supported, and Http.Entity cannot be combined with Http.FormParam components.

For declarative server endpoints, Http.Entity supports both a direct entity type and Optional<T>. A direct entity is mandatory, and the request fails if the entity is missing. An optional entity is Optional.empty() when the request has no entity. This behavior applies both to endpoint method parameters and to Http.RequestParams record components.

The named value annotations include Http.HeaderParam, Http.CookieParam, Http.QueryParam, Http.FormParam, and Http.PathParam. They support scalar values, Optional<T>, List<T>, and Optional<List<T>>. For server endpoints, List<T> is mandatory and the request fails if the named value is missing. Optional<List<T>> is optional and is empty when the named value is missing. If the named value is present but has no values, the injected list is empty. Cookies are an exception: cookies are not represented as Parameters, so a named cookie cannot be present with no values. Cookie parameter names are validated during declarative code generation. Server path parameters are obtained from the routed request path.

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)
@Http.Produces(MediaTypes.APPLICATION_JSON_VALUE) // default response media type for all endpoint methods
@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
    public JsonObject getDefaultMessageHandler() {
        // build the JSON object (requires `helidon-http-media-jsonp` on classpath)
        return JSON.createObjectBuilder()
                .add("message", greeting + " World!")
                .build();
    }
}

Example of Grouped HTTP Server Parameters:

@RestServer.Endpoint
@Http.Path("/messages")
@Service.Singleton
static class MessageEndpoint {
    @Http.GET
    @Http.Path("/{id}")
    @Http.Produces(MediaTypes.TEXT_PLAIN_VALUE)
    String message(@Http.RequestParams MessageParams params) {
        return params.id() + ":" + params.language() + ":" + params.theme();
    }

    @Http.POST
    @Http.Path("/form")
    @Http.Consumes(MediaTypes.APPLICATION_FORM_URLENCODED_VALUE)
    @Http.Produces(MediaTypes.TEXT_PLAIN_VALUE)
    String submit(@Http.FormParam("message") String message,
                  @Http.CookieParam("session") String session) {
        return session + ":" + message;
    }
}

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:

For a QUERY client method, use @Http.QUERY, annotate its query content with @Http.Entity, and declare the content media type using @Http.Consumes.

Annotations on method parameters:

Http.FormParam is backed by the request entity and should be used with @Http.Consumes(MediaTypes.APPLICATION_FORM_URLENCODED_VALUE). It cannot be combined with Http.Entity on the same declarative method. Each client method parameter may have at most one supported request parameter annotation.

Http.RequestParams is supported only for record parameter types. This is a restriction on the parameter types supported by declarative code generation. Each client record component must use one of these component annotations: Http.HeaderParam, Http.CookieParam, Http.QueryParam, Http.FormParam, Http.PathParam, or Http.Entity. Components must not combine supported request parameter annotations. At most one Http.Entity component is supported, and Http.Entity cannot be combined with Http.FormParam components.

The Http.HeaderParam, Http.CookieParam, Http.QueryParam, and Http.FormParam annotations support scalar values, Optional<T>, List<T>, and Optional<List<T>>. Declarative clients send every value from List<T>, and send Optional<List<T>> only when the optional is present. Declarative client named-value parameters must not be null; use Optional.empty() to omit optional values. List values must not contain null elements. The same nullability rule applies to named-value components of Http.RequestParams records, and the request-params argument itself must not be null. Cookie parameter names are validated during declarative code generation. Declarative clients send cookie parameters as a single Cookie header and do not escape cookie values; each cookie parameter value must already be a valid cookie-octet value. Cookies are not represented as Parameters, so a named cookie cannot be present with no values. A mandatory client List<T> cookie parameter must contain at least one value.

Declarative clients use Http.PathParam values as URI path text when constructing the request URI. They do not encode each value as a single path segment, so reserved path characters such as / keep their WebClient URI meaning. Path parameter values must not be null.

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")
@Http.Produces(MediaTypes.APPLICATION_JSON_VALUE)
interface GreetClient {
    @Http.GET
    JsonObject getDefaultMessageHandler();
}

Example of Grouped HTTP Client Parameters:

record MessageParams(
        @Http.PathParam("id") String id,
        @Http.QueryParam("lang") String language,
        @Http.CookieParam("theme") String theme) {
}

@RestClient.Endpoint("${message-service.client.uri:http://localhost:8080}")
@Http.Path("/messages")
interface MessageClient {
    @Http.GET
    @Http.Path("/{id}")
    @Http.Produces(MediaTypes.TEXT_PLAIN_VALUE)
    String message(@Http.RequestParams MessageParams params);

    @Http.POST
    @Http.Path("/form")
    @Http.Consumes(MediaTypes.APPLICATION_FORM_URLENCODED_VALUE)
    @Http.Produces(MediaTypes.TEXT_PLAIN_VALUE)
    String submit(@Http.FormParam("message") String message,
                  @Http.CookieParam("session") String session);
}

gRPC Server Endpoint

Declarative gRPC server endpoints are service registry services annotated with io.helidon.webserver.grpc.RpcServer.Endpoint and io.helidon.grpc.api.Grpc.GrpcService. The generated binding contributes a GrpcRouteRegistration service that registers the endpoint with the gRPC server feature.

Annotations on endpoint type:

  • io.helidon.webserver.grpc.RpcServer.Endpoint - required annotation to generate a declarative gRPC server endpoint
  • io.helidon.grpc.api.Grpc.GrpcService - required, non-blank gRPC service name; use the fully-qualified service name when the proto declares a package
  • io.helidon.grpc.api.Grpc.ProtoDescriptor - generated protocol buffer class with a static getDescriptor() method returning com.google.protobuf.Descriptors.FileDescriptor
  • io.helidon.service.registry.Service.Singleton - normal service registry scope for the endpoint implementation

io.helidon.service.registry.Service.PerRequest is not supported for declarative gRPC endpoints because gRPC calls do not participate in the WebServer HTTP request scope. Use io.helidon.service.registry.Service.Singleton or io.helidon.service.registry.Service.PerLookup instead.

The endpoint must declare exactly one proto descriptor source: either @Grpc.ProtoDescriptor on the type or one @Grpc.Proto method. The referenced generated protocol buffer class must provide a public static getDescriptor() method returning com.google.protobuf.Descriptors.FileDescriptor. An @Grpc.Proto method may be static or an endpoint instance method; it must be non-private, have no parameters or checked exceptions, and return com.google.protobuf.Descriptors.FileDescriptor.

Each request and response type must implement com.google.protobuf.Message and declare public static no-argument getDescriptor() and getDefaultInstance() methods returning com.google.protobuf.Descriptors.Descriptor and the message type, respectively. At runtime, the generated registration verifies that these descriptors match the input and output descriptors of the named proto method.

Annotations on endpoint methods:

Each endpoint method must declare exactly one gRPC method annotation and must not declare checked exceptions. The Java annotation type must match the streaming cardinality declared by the proto method.

Supported server method signatures:

  • Unary: Res method(Req) or void method(Req, StreamObserver<Res>)
  • Server streaming: Stream<Res> method(Req) or void method(Req, StreamObserver<Res>)
  • Client streaming: Res method(Stream<Req>)
  • Bidirectional streaming: Stream<Res> method(Stream<Req>) or StreamObserver<Req> method(StreamObserver<Res>)

Declarative streaming methods use resource-owning Stream instances with transport backpressure and cancellation. Endpoint implementations consume request streams, while the generated runtime owns and closes both request streams supplied to an endpoint and response streams returned by an endpoint. Endpoint implementations transfer ownership of response streams to the runtime and must not close a response stream before returning it.

Generated registrations use the fully-qualified gRPC service name, including the proto package when present. Route registration is enabled by default and can be disabled with server.features.grpc-route-registration.enabled=false. Annotate an endpoint with @RpcServer.Listener("admin") to register it on a named listener. Custom GrpcRouteRegistration implementations can return a named socket. If that socket is not configured and socketRequired() returns false, the registration falls back to the default socket. Generated declarative registrations require the named listener when @RpcServer.Listener is used.

Validation annotations on declarative gRPC server methods are enforced by generated entry point interceptors. To map a ValidationException to gRPC INVALID_ARGUMENT, add helidon-webserver-grpc-validation to the server runtime. The validation gRPC server service is discovered from the classpath and enabled by default. Configure it under server.protocols.grpc.grpc-services.validation; set server.protocols.grpc.grpc-services.validation.enabled=false to disable the status mapping.

Security annotations on declarative gRPC server methods require the gRPC security module. Add helidon-webserver-grpc-security and configure normal Helidon security. The gRPC security service is discovered from the classpath and enabled by default; set server.protocols.grpc.grpc-services.security.enabled=false to disable it.

Typed gRPC Client

To create a typed gRPC client, create an interface annotated with RpcClient.Endpoint and Grpc.GrpcService, and at least one method annotated with one of the gRPC method annotations.

Annotations on endpoint type:

The endpoint must declare exactly one proto descriptor source: either @Grpc.ProtoDescriptor on the type or one @Grpc.Proto method. The referenced generated protocol buffer class must provide a public static getDescriptor() method returning com.google.protobuf.Descriptors.FileDescriptor. An @Grpc.Proto method may be static or a default interface method; it must be non-private, have no parameters or checked exceptions, and return com.google.protobuf.Descriptors.FileDescriptor.

Each request and response type must implement com.google.protobuf.Message and declare public static no-argument getDescriptor() and getDefaultInstance() methods returning com.google.protobuf.Descriptors.Descriptor and the message type, respectively. At runtime, the generated client verifies that these descriptors match the input and output descriptors of the named proto method.

Annotations on endpoint methods:

Each abstract non-default client method must declare exactly one gRPC method annotation and must not declare checked exceptions.

Supported client method signatures:

  • Unary: Res method(Req) or void method(Req, StreamObserver<Res>)
  • Server streaming: Stream<Res> method(Req) or void method(Req, StreamObserver<Res>)
  • Client streaming: Res method(Stream<Req>) or StreamObserver<Req> method(StreamObserver<Res>)
  • Bidirectional streaming: Stream<Res> method(Stream<Req>) or StreamObserver<Req> method(StreamObserver<Res>)

Returned streams own the RPC and must be closed when the caller stops before normal exhaustion. The client consumes and closes request streams on normal completion, cancellation, or failure; a transferred request stream must not be reused. The calling thread consumes a client-streaming request stream. If producing elements can block, closing the stream must unblock production so an early peer termination can return promptly. After a response arrives, GrpcClientProtocolConfig.nextRequestWaitTime() bounds how long the call waits for the caller to request it; expiration closes the call with gRPC status CANCELLED.

To inject a typed gRPC client, use the annotated interface with the @RpcClient.Client qualifier.

@Service.Inject
MyService(@RpcClient.Client GreetingClient client) {
}

The RpcClient.Endpoint.value() defines the target URI for generated backing clients and supports configuration expressions, such as ${grpc.service.uri:http://localhost:8080}. When declarative code generation creates a new backing GrpcClient, it applies this URI after any client configuration. Registry-provided clients keep their own base URI.

The base of configuration for a declarative gRPC client is the fully-qualified name of the annotated interface. This key can be modified using the configKey property of the @RpcClient.Endpoint annotation.

Configuration options under this key:

KeyDefaultDescription
clientnoneConfiguration options of Helidon GrpcClient. If this node exists, the generated client creates a dedicated GrpcClient instance and does not use a registry-provided client.

Client resolution order:

  1. If <configKey>.client exists, create a dedicated GrpcClient from that configuration and apply RpcClient.Endpoint.value() as its base URI.
  2. Otherwise, if RpcClient.Endpoint.clientName() is set and a matching named GrpcClient exists in the service registry, use that registry client.
  3. Otherwise, if RpcClient.Endpoint.clientName() is set and no matching named GrpcClient exists, create a new GrpcClient using the endpoint URI from RpcClient.Endpoint.value().
  4. Otherwise, if an unnamed GrpcClient exists in the service registry, use that registry client.
  5. Otherwise, create a new GrpcClient using the endpoint URI from RpcClient.Endpoint.value().

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-level fault tolerance annotations can also be declared on methods inherited from service contracts and typed HTTP client interfaces. Type-level fault tolerance annotations on interfaces are not inherited by service methods. Contracts provided by registry-managed service factories are not included.

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";
    }
}

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

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:

pom.xml
<dependency>
  <groupId>io.helidon.validation</groupId>
  <artifactId>helidon-validation</artifactId>
</dependency>

For declarative gRPC server endpoints, add the gRPC validation runtime module when validation failures should be returned as gRPC INVALID_ARGUMENT status responses:

pom.xml
<dependency>
  <groupId>io.helidon.webserver</groupId>
  <artifactId>helidon-webserver-grpc-validation</artifactId>
</dependency>

The gRPC validation status mapper is enabled by default under server.protocols.grpc.grpc-services.validation; set server.protocols.grpc.grpc-services.validation.enabled=false to disable it.

Constraint Annotations

A "Constraint Annotation" is any annotation directly annotated with io.helidon.validation.Validation.Constraint. 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

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. Type-use validation is supported on nested Optional, Collection, List, Set, Map key/value types, array component types, and wildcard bounds.

Cascaded validation is skipped when a value annotated with @Validation.Valid is null. The annotation does not make the value required; add @Validation.NotNull when null must be rejected.

Usage

Example of a validated type

@Validation.Validated
record MyType(@Validation.String.Pattern(".*valid.*") @Validation.NotNull String validString,
              @Validation.Integer.Min(42) int validInt) {
}

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";
    }
}

Constraint annotations and @Validation.Valid can also be declared on non-private instance methods of service interfaces. They are applied when the service instance is obtained from the service registry. Constraints declared on matching service interface and implementation methods are combined. Implementation constraints do not replace or loosen constraints declared by the service interface, even when both methods use the same constraint type with different values. Service interfaces are discovered using the service registry contract rules, including @Service.Contract, implicit contracts when enabled, and @Service.ExternalContracts. The same applies to instances returned by registry-managed service factories, including Supplier, Service.ServicesFactory, Service.InjectionPointFactory, and Service.QualifiedFactory services. This can change runtime behavior for services that already declared interface validation: invalid calls through registry-obtained services may now fail with a ValidationException, while directly constructed objects are not intercepted.

Example of a validated service contract

@Service.Contract
interface ValidatedServiceContract {
    String process(@Validation.String.NotBlank String value);
}

@Service.Singleton
static class ContractValidatedService implements ValidatedServiceContract {
    @Override
    public String process(String value) {
        return value;
    }
}

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

Example of a compound annotation

@Validation.NotNull
@Validation.String.NotBlank
public @interface NonNullNotBlank {
}

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 {
}

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);
        }
    }
}

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.

Declarative gRPC server security annotations require helidon-webserver-grpc-security on the server runtime classpath and an enabled gRPC security service, for example server.protocols.grpc.grpc-services.security.enabled=true, in addition to normal Helidon security configuration.

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; explicit = true is not supported for declarative gRPC
  • 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:

Metrics method annotations can also be declared on methods inherited from service contracts and typed HTTP client interfaces. Contracts provided by registry-managed service factories are not included.

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.

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

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;
    }
}

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

Tracing annotations can also be declared on methods inherited from service contracts and typed HTTP client interfaces. A type-level tracing annotation on a contract interface applies only to methods declared by that contract. Contracts provided by registry-managed service factories are not included.

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 {

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!";
}

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

When a message is combined before delivery, the server and client each use a default buffering threshold configured as 1 MiB. Configure the server limit with server.protocols.websocket.max-buffered-message-size, and the client limit with protocol-config.max-buffered-message-size in WsClient configuration. If a combined message exceeds the applicable limit, that side closes the connection with WebSocket close code 1009. The server limit is independent of max-frame-length, which applies to each individual frame. Binary messages are measured exactly in bytes. Text message size is approximated using the number of UTF-16 code units in each decoded string fragment and may be smaller than the UTF-8 payload size. Methods that accept the trailing boolean fragment indicator, Reader, or InputStream consume fragments without whole-message buffering and are not limited by max-buffered-message-size.

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);
    }
}

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

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);
    }
}

GraphQL Server

To create a GraphQL endpoint, annotate a class with @GraphQlServer.Endpoint, add at least one method annotated with @GraphQl.Query, and annotate Java schema types with @GraphQl.Entity. Declarative GraphQL generates the SDL, GraphQL Java runtime wiring, and WebServer registration at build time.

Supported method parameters (no annotation required):

Annotations on endpoint type:

Annotations on resolver methods:

Annotations on schema types, fields, and resolver parameters:

GraphQL resolver methods are Service Registry entry points. Standard declarative entry-point features, including security annotations and validation interceptors, can participate in resolver invocation. The generated WebServer route is also wrapped as an HTTP entry point for route-level security.

Security annotations on the GraphQL endpoint type protect the generated WebServer route before GraphQL execution starts. Authentication and authorization failures in this phase use normal HTTP security responses, such as 401 or 403. Security annotations on resolver methods protect individual GraphQL fields. Resolver-level failures are returned as GraphQL field errors in the response body, so sibling fields can still complete and the HTTP response commonly remains 200.

See the GraphQL Declarative API for dependencies, startup, examples, custom scalars, and resolver parameter extension points.

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}")1
    @Cors.AllowHeaders({"X-foo", "X-bar"})2
    @Cors.AllowMethods({Method.DELETE_NAME, Method.PUT_NAME, "LIST"})3
    @Cors.MaxAgeSeconds(180)4
    void options() {
    }
}
  1. Configure origins that can be overridden using config key app.cors.allow-origins with the provided default values (comma separated)
  2. Configure headers the script can send to this host
  3. Configure allowed methods for CORS requests
  4. 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.

Copyright © 2018, 2026 Oracle and/or its affiliates.