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)
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:
<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:
<dependency>
<groupId>io.helidon.grpc</groupId>
<artifactId>helidon-grpc-api</artifactId>
</dependency>
Features
The following features are currently implemented:
- Configuration
- HTTP Server Endpoint
- Typed HTTP Client
- gRPC Server Endpoint
- Typed gRPC Client
- Fault Tolerance
- Scheduling
- Validation
- Security
- Metrics
- Tracing
- WebSocket Server
- WebSocket Client
- GraphQL Server
- WebServer CORS
- Health Checks
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:
io.helidon.config.Configuration.Value- define the configuration key to inject, on constructor parameter- Annotations 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.webserver.http.ServerRequestio.helidon.webserver.http.ServerResponseio.helidon.common.context.Contextio.helidon.common.security.SecurityContextio.helidon.security.SecurityContext- in casehelidon-securitymodule is on the classpath
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:
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.Produces- default media types produced by endpoint methodsio.helidon.http.Http.Consumes- default media types consumed by endpoint methods with request entitiesio.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, includingio.helidon.http.Http.QUERY) - definition of HTTP method this method will serveio.helidon.http.Http.HttpMethod- for custom HTTP method names (mutually exclusive with above); the name is case-sensitive and is preserved exactlyio.helidon.http.Http.Produces- what media type this method produces (return entity content type), replacing the endpoint default; an empty array clears itio.helidon.http.Http.Consumes- what media type this method accepts (request entity content type), replacing the endpoint default; an empty array clears it
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:
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.CookieParam- Typed HTTP request cookie valueio.helidon.http.Http.QueryParam- Typed HTTP query valueio.helidon.http.Http.FormParam- Typed URL-encoded form value from the request entityio.helidon.http.Http.PathParam- Typed parameter from path templateio.helidon.http.Http.RequestParams- Grouped request parameters in a record type
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:
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 serverio.helidon.http.Http.Produces- default media types produced by the serverio.helidon.http.Http.Consumes- default media types consumed by the server for methods with request entities
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, includingio.helidon.http.Http.QUERY) - definition of HTTP method this method will invokeio.helidon.http.Http.HttpMethod- for custom HTTP method names (mutually exclusive with above); the name is case-sensitive and is preserved exactlyio.helidon.http.Http.Produces- what media type this method produces (content type of entity from the server), replacing the client type default; an empty array clears itio.helidon.http.Http.Consumes- what media type this method accepts (request entity content type), replacing the client type default; an empty array clears it
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:
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.CookieParam- Typed HTTP cookie value to sendio.helidon.http.Http.QueryParam- Typed HTTP query value to sendio.helidon.http.Http.FormParam- Typed URL-encoded form value to send in the request entityio.helidon.http.Http.PathParam- Typed parameter from path template to construct the request URIio.helidon.http.Http.RequestParams- Grouped request parameters in a record type
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 endpointio.helidon.grpc.api.Grpc.GrpcService- required, non-blank gRPC service name; use the fully-qualified service name when the proto declares a packageio.helidon.grpc.api.Grpc.ProtoDescriptor- generated protocol buffer class with a staticgetDescriptor()method returningcom.google.protobuf.Descriptors.FileDescriptorio.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:
io.helidon.grpc.api.Grpc.Unary- unary gRPC methodio.helidon.grpc.api.Grpc.ServerStreaming- server-streaming gRPC methodio.helidon.grpc.api.Grpc.ClientStreaming- client-streaming gRPC methodio.helidon.grpc.api.Grpc.Bidirectional- bidirectional streaming gRPC method
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)orvoid method(Req, StreamObserver<Res>) - Server streaming:
Stream<Res> method(Req)orvoid method(Req, StreamObserver<Res>) - Client streaming:
Res method(Stream<Req>) - Bidirectional streaming:
Stream<Res> method(Stream<Req>)orStreamObserver<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:
io.helidon.webclient.grpc.RpcClient.Endpoint- required annotationio.helidon.grpc.api.Grpc.GrpcService- required, non-blank gRPC service name; use the fully-qualified service name when the proto declares a packageio.helidon.grpc.api.Grpc.ProtoDescriptor- generated protocol buffer class with a staticgetDescriptor()method returningcom.google.protobuf.Descriptors.FileDescriptor
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:
io.helidon.grpc.api.Grpc.Unary- unary gRPC methodio.helidon.grpc.api.Grpc.ServerStreaming- server-streaming gRPC methodio.helidon.grpc.api.Grpc.ClientStreaming- client-streaming gRPC methodio.helidon.grpc.api.Grpc.Bidirectional- bidirectional streaming gRPC method
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)orvoid method(Req, StreamObserver<Res>) - Server streaming:
Stream<Res> method(Req)orvoid method(Req, StreamObserver<Res>) - Client streaming:
Res method(Stream<Req>)orStreamObserver<Req> method(StreamObserver<Res>) - Bidirectional streaming:
Stream<Res> method(Stream<Req>)orStreamObserver<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:
| Key | Default | Description |
|---|---|---|
client | none | Configuration 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:
- If
<configKey>.clientexists, create a dedicatedGrpcClientfrom that configuration and applyRpcClient.Endpoint.value()as its base URI. - Otherwise, if
RpcClient.Endpoint.clientName()is set and a matching namedGrpcClientexists in the service registry, use that registry client. - Otherwise, if
RpcClient.Endpoint.clientName()is set and no matching namedGrpcClientexists, create a newGrpcClientusing the endpoint URI fromRpcClient.Endpoint.value(). - Otherwise, if an unnamed
GrpcClientexists in the service registry, use that registry client. - Otherwise, create a new
GrpcClientusing the endpoint URI fromRpcClient.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:
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
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:
io.helidon.scheduling.Scheduling.Cron- execute with schedule defined by a CRON expressionio.helidon.scheduling.Scheduling.FixedRate- execute with a fixed interval
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:
<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:
<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:
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)
- the value must be negative or zero (
io.helidon.validation.Validation.Number.Positive- the value must be positive (> 0)Validation.Number.PositiveOrZero- the value must be positive or zero (
>= 0)
- the value must be positive or zero (
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
Yeardata type, past is previous year, future is the next year, and present is the current year, regardless of which month it is. When usingInstant, past is already the last millisecond. io.helidon.validation.Validation.Calendar.Future- the value must be in the futureValidation.Calendar.FutureOrPresent- the value must be in the future or now
io.helidon.validation.Validation.Calendar.Past- the value must be in the pastValidation.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. 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 authenticationio.helidon.security.annotations.Authorized- mark an endpoint or a method as requiring authorization;explicit = trueis not supported for declarative gRPCio.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
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:
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.
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.WsSessionbooleanin 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
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:
WebSocketServer.Endpoint- required annotation
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
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, anEchoEndpointFactorywill be generated with methods to connect to remote server
Supported method parameters (no annotation required):
io.helidon.websocket.WsSessionbooleanin 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
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):
graphql.schema.DataFetchingEnvironmentio.helidon.common.context.Contextio.helidon.graphql.server.ExecutionContextio.helidon.security.SecurityContext- ifhelidon-securityis on the classpath
Annotations on endpoint type:
io.helidon.webserver.graphql.GraphQlServer.Endpoint- required annotationio.helidon.webserver.graphql.GraphQlServer.Listener- defines the named listener this should be served onio.helidon.webserver.graphql.GraphQlServer.Context- GraphQL web context, defaults to/graphqlio.helidon.webserver.graphql.GraphQlServer.SchemaUri- schema endpoint URI under the GraphQL web context
Annotations on resolver methods:
io.helidon.graphql.GraphQl.Query- top-level query fieldio.helidon.graphql.GraphQl.Mutation- top-level mutation fieldio.helidon.webserver.graphql.GraphQlServer.Field- child field resolver
Annotations on schema types, fields, and resolver parameters:
io.helidon.graphql.GraphQl.Entity- required marker for Java classes, records, interfaces, and enums that contribute SDLio.helidon.graphql.GraphQl.Argument- GraphQL resolver argument nameio.helidon.graphql.GraphQl.Name- GraphQL name overrideio.helidon.graphql.GraphQl.Description- GraphQL descriptionio.helidon.graphql.GraphQl.DefaultValue- GraphQL default value literal for an argumentio.helidon.graphql.GraphQl.NonNull- non-null schema elementio.helidon.graphql.GraphQl.Ignore- exclude an output Java member from SDLio.helidon.graphql.GraphQl.Scalar- custom scalar Java typeio.helidon.webserver.graphql.GraphQlServer.Source- child resolver source object
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):
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
Example of a CORS protected endpoint
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.