All features are implemented in a way that can use no reflection, mostly through code generating required handling classes.
Helidon Service Registry includes:
- Dependency Injection
- Lifecycle Support
- Factories and Services
- Aspect Oriented Programming (interceptors)
- Events
- Programmatic Lookup
- Startup
- Other
- Glossary
Provides a replacement for Java ServiceLoader with basic inversion of control mechanism. Each service may have constructor parameters that expect instances of other services.
The constructor dependency types can be as follows (Contract is used as the contract the service implements)
Contract- simply get an instance of another serviceOptional<Contract>- get an instance of another service, the other service may not be availableList<Contract>- get instances of all services that are availableSupplier<Contract>,Supplier<Optional<Contract>>,Supplier<List<Contract>>- equivalent methods but the value is resolved whenSupplier.get()is called, to allow more control
Equivalent behavior can be achieved programmatically through io.helidon.service.registry.ServiceRegistry instance. This can be
obtained from a ServiceRegistryManager.
Use one of the io.helidon.service.registry.Service.Scope annotations, such as
io.helidon.service.registry.Service.Singleton or io.helidon.service.registry.Service.PerLookup,
on your service provider type (implementation of a contract). Service factories implemented as
java.util.function.Supplier use the same scope annotations.
Use io.helidon.service.registry.Service.Descriptor to create a hand-crafted service descriptor (see below "Behind the scenes")
Service example:
import io.helidon.service.registry.Service;
@Service.Singleton
class MyService implements MyContract {
MyService() {
}
@Override
public String message() {
return "MyService";
}
}Service with dependency example:
import io.helidon.service.registry.Service;
@Service.Singleton
class MyService2 implements MyContract2 {
private final MyContract dependency;
MyService2(MyContract dependency) {
this.dependency = dependency;
}
@Override
public String message() {
return dependency.message();
}
}Service with java.util.function.Supplier as a contract example:
import java.util.function.Supplier;
import io.helidon.service.registry.Service;
@Service.PerLookup
// the type must be fully qualified, as it is code generated
class MyService3 implements Supplier<Optional<com.foo.bar.MyContract3>> {
MyService3() {
}
@Override
public Optional<MyContract3> get() {
return Optional.of(MyContract3.builder().message("MyService3").build());
}
}To use Service registry code generator, you need to add the Helidon annotation processor and the service registry code generator to your annotation processor path.
For Maven:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.helidon.codegen</groupId>
<artifactId>helidon-codegen-apt</artifactId>
<version>${helidon.version}</version>
</path>
<path>
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-codegen</artifactId>
<version>${helidon.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>Additional options can be configured to customize the behavior. For example the default approach is that all contracts
are auto-discovered. We can switch contract discovery to annotated only, in such a case the following annotations are available:
Use io.helidon.service.registry.Service.Contract on your contract interface (if not annotated, such an interface would not be
considered a contract and will not be discoverable using the registry - configurable).
Use io.helidon.service.registry.Service.ExternalContracts on your service provider type to
add other types as contracts, even if not annotated with Contract (i.e. to support third party libraries).
There is also an option to exclude specific types from being contracts (such as Closeable could be excluded).
To enable this (Maven):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<!-- Disable automatic adding of contracts -->
<arg>-Ahelidon.registry.autoAddNonContractInterfaces=false</arg>
<!-- Add contract exclusion (not needed if above is set to true) -->
<arg>-Ahelidon.registry.nonContractTypes=java.io.Serializable,java.lang.AutoCloseable,java.io.Closeable</arg>
</compilerArgs>
<!-- Annotation processor setup etc. -->
</configuration>
</plugin>For each service, Helidon generates a service descriptor (ServiceProvider__ServiceDescriptor).
This descriptor is discovered at runtime and used to instantiate a service without the need to use reflection.
Reflection is used only to obtain an instance of the service descriptor (by using its public INSTANCE singleton field). As both
the descriptor and the INSTANCE field are always public, there is no need to add opens to module-info.java.
Support for GraalVM native image is handled in Helidon native image extension, by registering all service descriptors for
reflection (the class, and the field).
The service registry uses a service-registry.json file in META-INF/helidon directory to store the main metadata of
the service. This is to allow proper ordering of services (Service weight is one of the information stored) and
lazy loading of services (which is the approach chosen in the core service registry).
The format is as follows (using // to comment sections, not part of the format):
// root is an array of modules (we always generate a single module, but this allows a combined array, i.e. when using shading
[
{
// version of the metadata file, defaults to 1 (and will always default to 1)
"version": 1,
// name of the module
"module": "io.helidon.example",
// all services in this module
"services": [
{
// version of the service descriptor, defaults to 1 (and will always default to 1)
"version": 1,
// core (Service registry) or inject (Service Injection), defaults to core
"type": "inject",
// weight, defaults to 100
"weight": 91.4,
// class of the service descriptor - generated type that contains public constant INSTANCE
"descriptor": "io.helidon.example.ServiceImpl__ServiceDescriptor",
// all contracts this service implements
"contracts": [
"io.helidon.example.ServiceApi"
]
}
]
}
]The basic building stone for inversion of control, dependency injection provides a mechanism to obtain an instance of a service at runtime, from the service registry, rather than constructing service instances through a constructor or a factory method.
When using dependency injection, we can separate the concerns of "how to create a service instance" from "how to use the contract". The consumer of the contract is not burdened with the details of how to obtain a valid instance, and the provider of the service is not burdened with providing an API to build/setup a service instance. In some cases such interaction would be quite cumbersome, as we would need to carry a shared instance through constructors to reach the correct place where we want to create a service instance.
One of the advantages of such an approach is the capability to exchange the service that implements a contract without the need to modify the consumers of such a contract.
In Helidon, dependency injection can be done in the following ways:
- Through a constructor annotated with
@Service.Inject- each parameter is considered an injection point; this is the recommended way of injecting dependencies (as it can be unit tested easily, and fields can be declaredprivate final) - Through field(s) annotated with
@Service.Inject- each field is considered an injection point; this is not recommended, as the fields must be accessible (at least package local), and cannot be declared asfinal
An injection point is satisfied by a service with the highest weight implementing the requested contract.
Services are:
- Java classes annotated with one of the
Service.Scopeannotations, such as@Service.Singleton- up to one instance exists in the service registry@Service.PerLookup- an instance is created each time a lookup is done (injecting into an injection point is considered a lookup as well)@Service.PerRequest- up to one instance exists in the service registry per request (what is a request is not defined in the injection framework itself, but it matches concepts such as HTTP request/response interaction, or consuming of a messaging message)
- Any class with
@Service.Injectannotation that does not have a scope annotation. In such a case, the service will be@Service.PerLookup. Only services can have Injection points.
Any annotation "meta-annotated" with @Service.Qualifier is considered a qualifier.
Qualifier annotations can be used to "qualify" injection points and services.
If an injection points is qualified (it has one or more qualifiers), it will only be satisfied with services that match all the specified qualifiers.
One qualifier is provided out-of-the-box - the @Service.Named (and @Service.NamedByType which does the same thing,
only the name is the fully qualified class name of the provided class).
Named instances are used by some features of Helidon Service Registry itself.
The service registry manages lifecycle of services.
To manage lifecycle, you can use the following annotations:
@Service.PostConstruct- a method annotated with this annotation will be invoked after the instance is constructed and fully injected@Service.PreDestroy- a method annotated with this annotation will be invoked after the service is no longer used by the registry
The behavior depends on the scope of the bean as follows:
@Service.PerLookup- only "post construct" lifecycle method is invoked, as we do not control the instance after is is injected- Any other scope - the "pre destroy" lifecycle method is invoked when the scope is deactivated (Singletons on registry shutdown or JVM shutdown)
Let's consider we have a contract named MyContract.
The simple case is that we have a class that implements the contract, and that is a service, such as:
@Service.Singleton
class MyImpl implements MyContract {
}This means the service instance itself is an implementation of the contract, and when this service is used to satisfy a dependency injection point, we will get an instance of MyImpl.
But such an approach is only feasible if the contract is an interface, and we are fine with doing a full implementation. There may be cases, where this is not sufficient:
- we need to provide an instance created by somebody else
- the provided contract is not an interface
- the provided instance may not be created at all (i.e. it is optional)
This can be done by implementing one of the factory interfaces Helidon Service Registry supports:
java.util.function.Supplier- a factory that supplies a single instance (can also beSupplier<Optional<MyContract>>)io.helidon.service.registry.Service.ServicesFactory- a factory that creates zero or more contract implementationsio.helidon.service.registry.Service.InjectionPointFactory- a factory that provides zero or more instances for each injection pointio.helidon.service.registry.Service.QualifiedFactory- a factory that provides zero or more instances for a specific qualifier and contract
The factory interfaces above should provide enough tooling to implement any injection use case.
Interception provides capability to intercept call to a constructor or a method (even to fields when used as injection points).
Interception is (by default) only enabled for elements annotated with an annotation that is a Interception.Intercepted.
Annotation processor configuration allows for creating interception "plumbing" for any annotation, or to disable it altogether.
Interception works "around" the invocation, so it can:
- do something before actual invocation
- modify invocation parameters
- do something after actual invocation
- modify response
- handle exceptions
Annotation type: io.helidon.service.registry.Interception
Annotations:
| Annotation class | Description |
|---|---|
Intercepted |
Marker for annotations that should trigger interception |
Delegate |
Marks a class as supporting interception delegation. Classes are not good candidates for delegation, as you need to create an instance that delegates to another instance, opening space for side-effects. To use a class, it must have an accessible no-arg constructor, and it should be designed not to have side-effects from construction |
ExternalDelegate |
Add this to a service provider that provides a class that requires delegation, if the class is not part of your current project (i.e. you cannot annotate it with Delegate |
Interfaces:
| Interface class | Description |
|---|---|
Interceptor |
A service implementing this interface, and named with the annotation type (maybe using NamedByType) will be used as interceptor of methods annotated with that annotation. Interceptor must call proceed method to handle the interception chain |
Events allow in-application communication between services, by providing a mechanism to emit an event, and to create a consumer/consumers of events.
One event can be delivered to (0..n) consumers.
Basic terminology:
Event Producer- a service that calls an emitter (origin of an event)Event Emitter- a service that emits an event to the event systemEvent Object- an arbitrary object that is sent around as an eventEvent Observer- a service that receives events, and has a method annotated withio.helidon.service.registry.Event.ObserverQualified Event- event published by an emitter providing a qualifier (annotation annotated withService.Qualifier)
Event Emitters are code generated by Helidon. To create an Event Producer, simply inject the emitter.
Event producers can be in any scope, the generated event emitter is always in Service.Singleton scope.
A simple singleton service that injects an event emitter for event object of type MyEventObject.
@Service.Singleton
class MyService {
private final Event.Emitter<MyEventObject> emitter;
@Service.Inject
MyService(Event.Emitter<MyEventObject> emitter) {
this.emitter = emitter;
}
}To emit an event, you simply call emitter.emit(myEventObjectInstance).
The method will return once all event observers were notified (unless they are asynchronous - see below).
In case any of the observers throws an exception, an EventDispatchException will be thrown with all exceptions caught added as
suppressed (i.e. we will invoke all observers, even after we catch an exception).
Event emitters are code generated for each Event Producer, so we may end up with more than one in the system. As all of them provide the exact same function, this is not an issue.
Explanation of the above statement: we cannot code generate classes into packages that do not belong to the current module, so we always code generate the emitter to the same package as the service that needs the emitter. Even though this may duplicate code, it is the only safe way we can do during annotation processing (where we do not have access to the classpath of the application)
An event can be consumed by declaring an observer method.
Event consumers can only be in Service.Singleton or Service.PerLookup scopes. The lookup is done exactly once, and all
events are delivered to the same instance for the lifetime of the service registry.
Helidon code generates an EventObserverRegistration service, which is used by the event manager to gather all observers for
event handling.
To create an event observer:
- create an observer method, with a single parameter of the event type you want to observe
- annotate the method with
Event.Observer
Example:
@Event.Observer
void event(MyEventObject eventObject) {
// do something with the event
}Events can be emitted asynchronously, and event observer can be asynchronous.
Executor service for asynchronous events can be provided via service registry, as a service that implements contract
java.util.concurrent.ExecutorService, and is named io.helidon.service.registry.EventManager.
If none is provided, the service will use a thread per task executor with Virtual threads, thread names will be prefixed with
inject-event-manager-.
Rules of asynchronous event producing:
- Method
Event.Emitter.emitAsync(..)returns aCompletionStage<MyEventType> - All synchronous Event Consumer are submitted to an executor service, and the returned completion stage will provide either
success (the event object itself), or will provide an exception, which will have
EventDispatchExceptionas a cause - The method returns once all the event observers are submitted to the executor service (there is no guarantee that anything has been delivered - we may have delivered 0 to n events (where n is number of synchronous observers))
- All asynchronous Event Observer are invoked outside of the returned completion stage
Asynchronous observer methods are invoked from separate threads (through the executor service mentioned above), and their results
are ignored by the Event Emitter; if there is an exception thrown from the observer method, it is logged with WARNING log level
into logger named io.helidon.service.registry.EventManager.
To declare an asynchronous observer use annotation Event.AsyncObserver instead of Event.Observer.
Example:
@Event.AsyncObserver
void event(MyEventObject eventObject) {
// handle event
}A Qualified Event is only delivered to Event Consumers that use the same qualifier.
A qualified event can be produced with two options:
- The injection point of
Event.Emitter(the constructor parameter, or field) is annotated with a qualifier annotation - The
Event.Emitter.emit(..)method is called with explicit qualifier(s), note that if combined, the qualifier specified by the injection point will always be present!
Example (combination of both):
import io.helidon.service.registry.Qualifier;
// class declaration
private static final Qualifier BLUE = Qualifier.create(Blue.class);
@Service.Inject
EventEmitter(@Black Event.Emitter<EventObject> event) {
// the event producer will implicitly have Black qualifier added
this.event = event;
}
void emit(MyEventObject eventObject) {
// the event will be emitted with both Blue and Black qualifiers
this.event.emit(eventObject, BLUE);
}To consume a qualified event, observer method must be annotated with the correct qualifier(s).
Example:
@Service.Singleton
class EventObserver {
@Event.Observer
@Black
void event(MyEventObject eventObject) {
// handle event that is qualified with Black (and none other)
}
}As usual with Helidon, what can be done via automation (dependency injection in this case) can also be done programmatically.
The service registry can be used and handled "from outside" - you can create a registry instance, lookup services, call methods on them.
It can also be used "from inside" - you can inject an ServiceRegistry into your services. In case this approach is done, we
cannot work around lookup costs as we can when only dependency injection is used.
To create a registry instance:
// create an instance of a registry manager - can be configured and shut down
var registryManager = ServiceRegistryManager.create();
// get the associated service registry
var registry = registryManager.registry();Note that all instances are created lazily, so the registry will do "nothing" by default. If a service does something during construction or post construction, you must lookup an instance from the registry first.
Special registry operations:
List<ServiceInfo> lookupServices(Lookup lookup)- get all service descriptors that match the lookupOptional<T> get(ServiceInfo)- get an instance for the provided service descriptor
The common registry operations are grouped by method name. Acceptable parameters are described below.
Registry methods:
T get(...)- immediately get an instance of a contract from the registry; throws if implementation not availableOptional<T> first(...)- immediately get an instance of a contract from the registry; there may not be an implementation availableList<T> all(...)- immediately get all instances of a contract from the registry; result may be emptySupplier<T> supply(...)- get a supplier of an instance; the service may be instantiated only whengetis calledSupplier<Optional<T>> supplyFirst(...)- get a supplier of an optional instanceSupplier<List<T>> supplyAll(...)- get a supplier of all instances
Lookup parameter options:
Class<?>- the contract we are looking forTypeName- the same, but using Helidon abstraction of type names (may have type arguments)Lookup- a full search criteria for a registry lookup
Helidon provides a Maven plugin (io.helidon.service:helidon-service-maven-plugin, goal create-application) to generate
build time bindings, that can be used to start the service registry without any classpath discovery and reflection.
Default name is ApplicationBinding (customizable)
Methods that accept the bindings are on io.helidon.service.registry.ServiceRegistryManager:
start(Binding)- starts the service registry with the generated binding, initializing all singleton and per-lookup services annotated with a@RunLevelannotation (i.e.start(ApplicationBinding.create()))start(Binding, ServiceRegistryConfig)- same as above, allows for customization of configuration, if used, do not forget to set discovery tofalseto prevent automated discovery from the classpath
All options to start a Helidon application that uses service registry:
- A custom Main method using
ServiceRegistryManager.start(...)methods, orServiceRegistryManager.create(...)methods - A generated
ApplicationMain- optional feature of the Maven plugin, requires propertygenerateMainto be set totrue - The Helidon startup class
io.helidon.Main, which will start the registry manager and initialize allRunLevelservices, though it uses service discover (which in turn must use reflection to get service descriptor instances)
Helidon Service Maven plugin must be configured to generate binding. Binding is only used by the application (i.e. not by library modules).
Example of Maven plugin configuration:
<plugin>
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-maven-plugin</artifactId>
<version>${helidon.version}</version>
<executions>
<execution>
<id>create-application</id>
<goals>
<goal>create-application</goal>
</goals>
</execution>
</executions>
</plugin>A class ApplicationBinding is generated with
- all dependency injection point bindings (i.e. which services satisfy injection points, to bypass analysis at startup)
- registration of all service descriptor with config (to avoid discovery and reflection at runtime)
- all run levels to initialize (to avoid service registry lookup that returns all services at runtime)
The generated binding can be used to start a service registry as mentioned above, via ServiceRegistryManager.
Helidon Service Maven plugin must be configured to generate a Main class. Main class is only used by the application (i.e. not by library modules).
Example of Maven plugin configuration (generateMain is false by default):
<plugin>
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-maven-plugin</artifactId>
<version>${helidon.version}</version>
<executions>
<execution>
<id>create-application</id>
<goals>
<goal>create-application</goal>
</goals>
</execution>
</executions>
<configuration>
<generateMain>true</generateMain>
</configuration>
</plugin>The generated class will use the generated binding to start service registry.
For details on how to configure your build, see Maven Plugin.
Annotation type: io.helidon.service.registry.Injection
Annotations:
| Annotation class | Description |
|---|---|
Inject |
Marks element as an injection point; although we prefer constructor injection, field and method injection works as well |
Qualifier |
Marker for annotations that are qualifiers |
Named |
A qualifier that provides a name |
NamedByType |
An equivalent of Named, that uses the fully qualified class name of the configured class as name |
Scope |
Marker for annotations that are scopes |
PerLookup |
Service instance is created per lookup (either for injection point, or via registry lookup) |
Singleton |
Singleton scope - a service registry will create zero or one instances of this service (instantiation is lazy) |
PerRequest |
Request scope - a service registry will create zero or one instance of this service per request scope instance |
RunLevel |
A "layer" in which this service should be instantiated; not executed by injection, will be used when starting application |
PerInstance |
Create a service instance for each instance of the configured contract available in registry (usually for named) |
InstanceName |
Parameter or field that will be injected with the name this service instance is created for (see PerInstance) |
Describe |
Create a descriptor for a type that is not a service itself, but an instance would be provided at scope creation time |
Interfaces:
| Interface class | Description |
|---|---|
ServicesFactory |
A service factory that creates zero or more qualified service instances at runtime |
InjectionPointFactory |
A service factory that creates values for specific injection points |
QualifiedFactory |
A service factory to resolve qualified injection points of any type (used for example by config value injection |
QualifiedInstance |
Used as a return type of some of the interfaces above, not to be implemented by users |
ScopeHandler |
Extension point to support additional scopes |
An injection point may have the following forms (Contract stands for a contract interface, or class):
Instance based:
Contract- injects an instance of the contract with the highest weight from the registryOptional<Contract>- same as previous, the contract may not have an implementation available in registryList<Contract>- a list of all available instances in the registry
Supplier based (to break cyclic dependency, and to create instances as late as possible):
Supplier<Contract>Supplier<Optional<Contract>>Supplier<List<Contract>>
Service instance based (to obtain registry metadata in addition to the instance):
ServiceInstance<Contract>Optional<ServiceInstance<Contract>>List<ServiceInstance<Contract>>
We do not support dependencies of types List<Supplier<Contract>> and Optional<Supplier<Contract>>, even though other frameworks do have similar concepts.
The reason is that we resolve all injection points as late as possible, and injecting these types would require an early resolution of instances.
As we support optional factories (i.e. Supplier<Optional<Contract>>), and the concept of ServicesFactory<Contract>, we
do not know how many instances (and if any) are available at the time of injecting to a service.
So if we supported List<Supplier<Contract>>, we would still need to resolve all the instances, so just use List<Contract>
for this purpose. If a supplier is need to break dependency cycle, use Supplier<List<Contract>>, and you will get instances
resolved only once you call get() on the supplier.
| Term | Description |
|---|---|
| Contract | A class extended by a service, or an interface implemented by a service, can be used to lookup instances |
| Dependency | A service constructor parameter (type must be another service or a "Contract") |
| Service | A class annotated with one of the scope annotations, or with @Service.Inject and no scope annotation |
| Factory | A Service that implements one of the factory interfaces |
| Injection Point | Field annotated with @Service.Inject, or a constructor parameter of a constructor used for injection (either the only accessible constructor, or the only constructor annotated with @Service.Inject) |