Overview

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

Our declarative approach has the following advantages:

  • Uses Helidon SE imperative code to implement features (i.e. performance is same as "pure" imperative application)

  • Generates all the necessary code at build-time, to avoid reflection and bytecode manipulation at runtime

  • It is based on Helidon Injection

  • Declarative features are in the same modules as Helidon SE features (i.e. does not require additional dependencies)

Note

Helidon Declarative is an incubating feature. The APIs shown here are subject to change. These APIs will be finalized in a future release of Helidon.

Usage

To create a declarative application, use the annotations provided in our Helidon SE modules (details under Features), and the maven plugin described in Injection: Startup to generate the binding.

In addition, the following section must be added to the build of the Maven pom.xml to enable annotation processors that generate the necessary code:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <annotationProcessorPaths>
                <path>
                    <groupId>io.helidon.bundles</groupId>
                    <artifactId>helidon-bundles-apt</artifactId>
                    <version>${helidon.version}</version>
                </path>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>
Copied

Features

The following features are currently implemented:

A Helidon Declarative application should be started using the generated application binding, to ensure no lookup and no reflection. The call to ServiceRegistryManager.start ensures that all services with a defined RunLevel are started, including Helidon WebServer, Scheduled services etc.

Example of a declarative main class
@Service.GenerateBinding // generated binding to bypass discovery and runtime binding
public static class Main {
    public static void main(String[] args) {
        // configure logging
        LogConfig.configureRuntime();

        // start the "container"
        ServiceRegistryManager.start(ApplicationBinding.create());
    }
}
Copied

Configuration

Configuration can be injected as a whole into any service, or a specific configuration option can be injected using @Configuration.Value. Default values can be defined using annotations in @Default

Services available for injection:

Annotations:

Example of usage can be seen below in HTTP Server Endpoint example.

HTTP Server Endpoint

To create an HTTP endpoint, simply annotate a class with @RestServer.Endpoint, and add at least one method annotated with one of the HTTP method annotations, such as @Http.GET.

Services available for injection:

N/A

Supported method parameters (no annotation required):

Annotations on endpoint type:

Annotations on endpoint methods:

Annotations on method parameters:

Example of an HTTP Server Endpoint
@RestServer.Endpoint // identifies this class as a server endpoint
@Http.Path("/greet") // serve this endpoint on /greet context root (path)
@Service.Singleton   // a singleton service (single instance within a service registry)
static class GreetEndpoint {
    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of());
    private final String greeting;

    // inject app.greeting configuration value, use "Hello" if not configured
    GreetEndpoint(@Configuration.Value("app.greeting") @Default.Value("Hello") String greeting) {
        this.greeting = greeting;
    }

    @Http.GET   // HTTP GET endpoint
    @Http.Produces(MediaTypes.APPLICATION_JSON_VALUE) // produces entity of application/json media type
    public JsonObject getDefaultMessageHandler() {
        // build the JSON object (requires `helidon-http-media-jsonp` on classpath)
        return JSON.createObjectBuilder()
                .add("message", greeting + " World!")
                .build();
    }
}
Copied

Typed HTTP Client

To create a typed HTTP client, create an interface annotated with RestClient.Endpoint, and at least one method annotated with one fo the HTTP method annotations, such as @Http.GET. Methods can only have parameters annotated with one of the Http qualifiers.

Annotations on endpoint type:

Annotations on endpoint methods:

Annotations on method parameters:

Example of a Typed HTTP Client
@RestClient.Endpoint("${greet-service.client.uri:http://localhost:8080}")
@RestClient.Header(name = HeaderNames.USER_AGENT_NAME, value = "my-client")
interface GreetClient {
    @Http.GET
    @Http.Produces(MediaTypes.APPLICATION_JSON_VALUE)
    JsonObject getDefaultMessageHandler();
}
Copied

Fault Tolerance

Fault tolerance annotation allow adding features to methods on services. The annotations can be added to any method that supports interception (i.e. methods that are not private).

Method Annotations:

Example of Fault Tolerance Fallback
@Service.Singleton
static class AlgorithmService {
    @Ft.Fallback(value = "fallbackAlgorithm", applyOn = IOException.class)
    String algorithm() throws IOException {
        // may throw an exception
        return "some-algorithm";
    }

    // method that would be called if #algorithm fails with an IOException
    String fallbackAlgorithm()  {
        return "default";
    }
}
Copied

Scheduling

Scheduling allows service methods to be invoked periodically.

Method annotations:

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