From dce5ae0b1fc22c18ad768cfa3097beb8b3f58788 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 6 Feb 2025 12:38:18 +0100 Subject: [PATCH 01/18] inject documentation first Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 471 ++++++++++++++++++ docs/src/main/asciidoc/se/introduction.adoc | 7 + .../helidon/docs/se/inject/BasicExample.java | 46 ++ .../helidon/docs/se/inject/EventsExample.java | 88 ++++ .../docs/se/inject/FactoryExample.java | 47 ++ .../se/inject/InterceptorDelegateExample.java | 61 +++ .../docs/se/inject/InterceptorExample.java | 64 +++ .../docs/se/inject/Qualifier2Example.java | 54 ++ .../docs/se/inject/QualifierExample.java | 58 +++ 9 files changed, 896 insertions(+) create mode 100644 docs/src/main/asciidoc/se/injection.adoc create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc new file mode 100644 index 00000000000..bd81b03e9b2 --- /dev/null +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -0,0 +1,471 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2025 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += Overview + +:description: Dependency Injection in Helidon SE +:keywords: helidon, se, injection +:h1Prefix: SE +:feature-name: Injection +:rootdir: {docdir}/.. + +include::{rootdir}/includes/se.adoc[] + +== Contents + +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> + +== Overview + +Injection is the basic building stone for inversion of control. Dependency injection provides a mechanism to get +an instance of a service at runtime, from the service registry, rather than constructing service instances through +a constructor or a factory method. + +include::{rootdir}/includes/dependencies.adoc[] + +[source,xml] +---- + + io.helidon.service + helidon-service-registry + +---- + +== Usage +To start using Helidon Inject, you need to create both: + +- A service that will be injected. +- An injection point, where the service will be injected. + +Let's begin by explaining what services are in Helidon Inject. + +=== What is a service +Services are: + +1. Java classes annotated with one of the `Service.Scope` annotations, 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) +- `@Service.PerInstance` - a service that has instances created for each named instance of the service it is driven by +2. Any class with `@Service.Inject` annotation that doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. +3. Any `core` service defined for Helidon Service Registry (using annotation `@Service.Provider`), the scope is `@Service.PerLookup` if the service implements a `Supplier`, and `@Singleton` otherwise; all dependencies are considered injection points + +Now, let's talk about an injection points. + +=== Injection points +In Helidon, dependency injection can be done into the injection point in the following ways: + +1. 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 declared private final) +2. 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 can’t be declared as final + +Injected services are picked by the highest weight and implementing the requested contract. +Only services can have Injection points. + +=== Contract vs. service +Contract and service can be the same thing, but also separate entities. For simplicity, you can imagine contract as what you’re injecting/searching service registry for and service is what is responsible for adding new instances to the service registry. + +=== Annotation processors +To make everything work, it is necessary to add the following annotation processors to +the compilation process of your application. + +For Maven: +[source,xml] +.Example annotation processor configuration in the Maven +---- + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + + + + +---- + +==== Why are these annotation processors needed? +Annotation processor `helidon-service-codegen` generates a service descriptor (`ServiceProvider__ServiceDescriptor`) for each discovered service. +This descriptor is discovered at runtime and used to instantiate a service without the need to use reflection. + +Reflection is used only to get an instance of the service descriptor (by using its public `INSTANCE` singleton field). + +=== Basic injection example + +Create a simple service class, which will be injected into another. + +[source,java] +.Creating simple Greeter service. +---- +include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_1, indent=0] +---- + +Once the Greeter service is created, an injection point for such a service is needed now. +Let's create another service, which injects Greeter service as its constructor parameter. + +[source,java] +.Create simple injection point +---- +include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_2, indent=0] +---- + +Now it just needs to be tested. The easiest way is to make a main method. The following piece of code +initializes Service registry. After that we search for our `GreetingInjectionService` and execute it +to print out `Hello Tomas!`. +[source,java] +.Lookup our created service and execute it manually +---- +include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_3, indent=0] +---- + +If everything went as expected, no problems occurred Service registry gave us fully initialized +and ready to use service. + +=== Service Lifecycle + +The service registry manages the lifecycle of services. To ensure a method is invoked at a specific lifecycle phase, you can use the following annotations: + +- `@Service.PostConstruct` – Invokes the annotated method after the instance has been created and fully injected. +- `@Service.PreDestroy` – Invokes the annotated method when the service is no longer in use by the registry. + +The lifecycle behavior depends on the bean scope: + +- `@Service.PerLookup` – Only the post-construct method is invoked since the instance is not managed after the injection. +- *Other scopes* – The pre-destroy method is invoked when the scope is deactivated (e.g. for singletons this happens during registry or JVM shutdown). + +=== Qualifiers +In dependency injection, a qualifier is a way to tell the framework which dependency to use when there are multiple options available. + +Annotations are considered qualifier if they’re "meta-annotated" with `@Service.Qualifier`. + +Helidon Inject comes with one qualifier provided out-of-the-box - the `@Service.Named` (and `@Service.NamedByType` which does the same thing, but uses class instead of a `String` name) + +==== Named service injection + +Services can be assigned names, allowing them to be specified by name during injection. +This ensures that the correct service is injected. To achieve this, we use the @Service.Named annotation. + +[source,java] +.Create named services +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_1, indent=0] +---- + +These named services can now be injected at specific injection points using their assigned names +[source,java] +.Create BlueCircle with Blue color injected +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_2, indent=0] +---- +[source,java] +.Create GreenCircle with Green color injected +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_3, indent=0] +---- + +==== Named by the type +Alternatively, instead of using string-based names for services, a specific class can be used to "name" them. For this purpose, we use the @Service.NamedByType annotation. + +[source,java] +.Named by type usage example +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_4, indent=0] +---- + +The way it is used on the injection point, it is the same as it was in case of the `@Service.Named`. + +==== Custom qualifiers +To make custom qualifiers, it is necessary to "meta-annotated" it with `@Service.Qualifier`. + +[source,java] +.Custom qualifier creation +---- +include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_1, indent=0] +---- +The `@HexCode` annotation serves as our new qualifier. It can now be used to qualify services in the same way as `@Service.Named`. + +[source,java] +.Custom qualifier HexCode usage +---- +include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_2, indent=0] +---- + +Once the services are created and qualified, they can be injected in the same way as before using the following approach + +[source,java] +.Custom qualifier usage on injection point +---- +include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_3, indent=0] +---- + +=== Factories +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: + +[source,java] +---- +@Service.Singleton +class MyImpl implements MyContract { +} +---- + +This means that the service instance itself serves as the implementation of the contract. When this service is injected into a dependency injection point, we receive an instance of MyImpl. + +However, this approach only works if the contract is an interface and we’re implementing it fully. In some cases, this may not be enough, such as when: + +- The instance needs to be provided by an external source. +- The contract is not an interface. +- The instance may not always be created (e.g., it is optional). + +These challenges can be addressed by implementing one of the factory interfaces supported by the Helidon Service Registry: + +- `java.util.function.Supplier` +- `io.helidon.service.registry.Service.ServicesFactory` +- `io.helidon.service.registry.Service.InjectionPointFactory` +- `io.helidon.service.registry.Service.QualifiedFactory` + +==== java.util.function.Supplier +A factory that supplies a single instance (it can also return `Supplier>`) +[source,java] +.Supplier factory +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_1, indent=0] +---- + +==== io.helidon.service.registry.Service.ServicesFactory +A factory that creates zero or more implementations of a given contract. + +[source,java] +.ServicesFactory example +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_2, indent=0] +---- + +If the created services need to be qualified, the relevant qualifier information must also be included in the factory annotations. + +Note: If one doesn’t want to list all the names provided by this factory, it is possible use `*` to cover all the possible names. + +[source,java] +.Qualified ServicesFactory example +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_3, indent=0] +---- + +==== io.helidon.service.registry.Service.InjectionPointFactory +A factory that provides zero or more instances for each injection point. + +==== io.helidon.service.registry.Service.QualifiedFactory +A factory that provides zero or more instances based on a specific qualifier and contract. + +=== Interceptors +Interception allows intercepting calls to constructors or methods, and even fields when used as injection points. + +By default, interception is enabled only for elements annotated with `Interception.Intercepted`. However, annotation processor configurations can enable interception for any annotation or disable it entirely. + +Interception wraps around the invocation, enabling it to: + +* Execute logic before the actual invocation +* Modify invocation parameters +* Execute logic after the actual invocation +* Modify the response +* Handle exceptions + +==== Intercepted annotation +The `@Interception.Intercepted` annotation is a marker used to indicate that an annotation should trigger interception. + +[source,java] +.Custom annotation for interception +---- +include::{sourcedir}/se/inject/InterceptorExample.java[tag=snippet_1, indent=0] +---- + +==== Interceptor interface +The `io.helidon.service.registry.Interception.Interceptor` interface defines an interceptor service that intercepts methods/constructors/fields annotated with the configured marker annotation. This supported marker annotation is specified using `@Service.NamedByType`. To properly handle the interception chain, the interceptor must always invoke the `proceed` method. + +[source,java] +.Sample Interceptor interface implementation +---- +include::{sourcedir}/se/inject/InterceptorExample.java[tag=snippet_2, indent=0] +---- + +<1> Binds this Interceptor to process annotated with `@Traced` +<2> Passing interceptor processing to another interceptor in the chain + +==== Delegate annotation +The `@Interception.Delegate` annotation enables interception for classes or interfaces that aren’t created through the service registry but are instead produced by a factory. To enable interception, this annotation must be present on the class that the factory produces. While it is not required on interfaces, it will still function correctly if applied there. + +Let's make the same `@Traced` annotation and Interceptor as in the previous examples + +[source,java] +.Custom annotation and interceptor +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_1, indent=0] +---- + +Now, let's create the factory of the service instance. + +[source,java] +.Instance producer +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_2, indent=0] +---- + +Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the `@Interception.Delegate` annotation. However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. Additionally, if a class is annotated with `@Interception.Delegate`, it must have a non-private default constructor. + +[source,java] +.Delegate used on the class +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_3, indent=0] +---- + +// ==== ExternalDelegate annotation +// The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes or interfaces that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes or interfaces. + +=== Events +Events enable in-application communication between services by providing a mechanism to emit events and register consumers to handle them. + +A single event can be delivered to zero or more consumers. + +Key Terminology: + +- *Event Object* – Any object that is sent as an event. +- *Event Emitter* – A service responsible for emitting events into the event system. +- *Event Producer* – A service that triggers an event by calling an emitter. +- *Event Observer* – A service that listens for events, with a method annotated using `io.helidon.service.registry.Event.Observer`. +- *Qualified Event* – An event emitted with a qualifier, using an annotation marked with `Service.Qualifier`. + +==== Event Object +To begin emitting events, the first step is to define the event type itself. + +[source,java] +.Create a desired event type +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] +---- + +==== Event Emitter +Event emitters are code generated by Helidon. So we don't need to make them ourselves. + +==== Event Producer +An event producer is a service that triggers the event by using the event emitter. + +To emit an event, inject the desired event emitter, construct the corresponding event object, and call the emit method on the emitter instance. + +[source,java] +.Event producer example +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_2, indent=0] +---- +The method returns only after all event observers have been notified. If any observer throws an exception, an `EventDispatchException` is thrown, with all caught exceptions added as suppressed. This ensures that all observers are invoked, even if an exception occurs. + +==== Event Observer +An event observer is a service which processes fired event. + +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` + +[source,java] +.Event observer example +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_3, indent=0] +---- + +=== Qualified Events + +A Qualified Event is only delivered to Event Consumers that use the same qualifier. + +==== Qualified Event Producer + +A qualified event can be produced with two options: + +1. The injection point of `Event.Emitter` (the constructor parameter, or field) is annotated with a qualifier annotation +2. 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! + +In this example below, we create event producer, which fires event only to observers qualified with name "id". +[source,java] +.Qualified event producer +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_6, indent=0] +---- + +==== Qualified Event Observers + +To consume a qualified event, observer method must be annotated with the correct qualifier(s). + +[source,java] +.Qualified event observer +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_7, indent=0] +---- + +=== Asynchronous Events +Events can be emitted asynchronously, and event observers can also operate asynchronously. + +An executor service for handling asynchronous events can be registered in the service registry by providing a service that implements the `java.util.concurrent.ExecutorService` contract and is named `io.helidon.service.registry.EventManager`. + +If no custom executor service is provided, the system defaults to a per-task executor using Virtual Threads, with thread names prefixed as `inject-event-manager-`. + +==== Asynchronous Event Producer +All asynchronous event producers must use the `Event.Emitter.emitAsync(..)` method instead of the synchronous `Event.Emitter.emit(..)`. + +The `emitAsync` method returns a `CompletionStage` instance. When executed, it completes once all event observers have been submitted to the executor service. However, there is no guarantee that any event has been delivered—it may have been sent to anywhere from 0 to n observers (where n represents the number of synchronous observers). + +[source,java] +.Asynchronous Event Producer Example +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_4, indent=0] +---- + +==== Asynchronous Observer +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`. + +[source,java] +.Asynchronous Event Observer Example +---- +include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_5, indent=0] +---- + diff --git a/docs/src/main/asciidoc/se/introduction.adoc b/docs/src/main/asciidoc/se/introduction.adoc index 217a05a16e6..87bc6ec0932 100644 --- a/docs/src/main/asciidoc/se/introduction.adoc +++ b/docs/src/main/asciidoc/se/introduction.adoc @@ -78,6 +78,13 @@ Build gRPC servers and clients. -- Expose health statuses of your applications. -- +//Injection +[CARD] +.Injection +[icon=message,link=injection.adoc] +-- +Use of the Helidon injection in your applications. +-- //Metrics [CARD] .Metrics diff --git a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java new file mode 100644 index 00000000000..4998ac861f0 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java @@ -0,0 +1,46 @@ +package io.helidon.docs.se.inject; + +import java.util.function.Function; + +import io.helidon.service.registry.Service; +import io.helidon.service.registry.ServiceRegistryManager; + +class BasicExample { + + // tag::snippet_1[] + @Service.Singleton + class Greeter { + + public String greet(String name) { + return "Hello %s!".formatted(name); + } + + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + class GreetingInjectionService { + + private final Greeter greeter; + + @Service.Inject + GreetingInjectionService(Greeter greeter) { + this.greeter = greeter; + } + + void printGreeting(String name) { + System.out.println(greeter.greet(name)); + } + } + // end::snippet_2[] + + // tag::snippet_3[] + public static void main(String[] args) { + var registry = ServiceRegistryManager.create().registry(); + var greetings = registry.get(GreetingInjectionService.class); + greetings.printGreeting("Tomas"); + } + // end::snippet_3[] + +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java new file mode 100644 index 00000000000..01737cec6f1 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java @@ -0,0 +1,88 @@ +package io.helidon.docs.se.inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import io.helidon.service.registry.Event; +import io.helidon.service.registry.Service; + +class EventsExample { + + // tag::snippet_1[] + /** + * A custom event payload. + * @param msg message + */ + record MyEvent(String msg) { + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + record MyEventProducer(Event.Emitter emitter) { + + void emit(String msg) { + emitter.emit(new MyEvent(msg)); + } + } + // end::snippet_2[] + + + // tag::snippet_3[] + @Service.Singleton + class MyEventObserver { + + @Event.Observer + void event(MyEvent event) { + //Do something with the event + } + } + // end::snippet_3[] + + + // tag::snippet_4[] + @Service.Singleton + record MyAsyncEmitter(Event.Emitter emitter) { + + void emit(String msg) { + CompletionStage completionStage = emitter.emitAsync(new MyEvent(msg)); + //Do something with the completion stage + } + } + // end::snippet_4[] + + // tag::snippet_5[] + @Service.Singleton + class MyEventAsyncObserver { + + @Event.AsyncObserver + void event(MyEvent event) { + //Do something with the event + } + } + // end::snippet_5[] + + // tag::snippet_6[] + @Service.Singleton + record MyIdProducer(@Service.Named("id") Event.Emitter emitter) { + + void emit(String msg) { + emitter.emit(msg); + } + } + // end::snippet_6[] + + // tag::snippet_7[] + @Service.Singleton + class MyIdObserver { + + @Event.Observer + @Service.Named("id") + void event(MyEvent event) { + //Do something with the event + } + } + // end::snippet_7[] + +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java new file mode 100644 index 00000000000..b98291b6e55 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -0,0 +1,47 @@ +package io.helidon.docs.se.inject; + +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.service.registry.Qualifier; +import io.helidon.service.registry.Service; + +class FactoryExample { + + record MyService() {} + + // tag::snippet_1[] + /** + * Supplier service factory. + */ + @Service.Singleton + class MyServiceProvider implements Supplier { + + @Override + public MyService get() { + return new MyService(); + } + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + class MyServiceFactory implements Service.ServicesFactory { + @Override + public List> services() { + return List.of(Service.QualifiedInstance.create(new MyService())); + } + } + // end::snippet_2[] + + // tag::snippet_3[] + @Service.Singleton + @Service.Named("test") + class MyQualifiedServiceFactory implements Service.ServicesFactory { + @Override + public List> services() { + return List.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("test"))); + } + } + // end::snippet_3[] +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java new file mode 100644 index 00000000000..b9dc96977c4 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java @@ -0,0 +1,61 @@ +package io.helidon.docs.se.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.service.registry.Interception; +import io.helidon.service.registry.InterceptionContext; +import io.helidon.service.registry.Service; + +class InterceptorDelegateExample { + + // tag::snippet_1[] + @Interception.Intercepted + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @interface Traced { + } + + @Service.Singleton + @Service.NamedByType(Traced.class) + static class MyServiceInterceptor implements Interception.Interceptor { + static final List INVOKED = new ArrayList<>(); + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + INVOKED.add("%s.%s: %s".formatted( + ctx.serviceInfo().serviceType().declaredName(), + ctx.elementInfo().elementName(), + Arrays.asList(args))); + return chain.proceed(args); + } + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + static class MyServiceProvider implements Supplier { + @Override + public MyService get() { + return new MyService(); + } + } + // end::snippet_2[] + + // tag::snippet_3[] + @Service.Contract + @Interception.Delegate + static class MyService { + + @Traced + String sayHello(String name) { + return "Hello %s!".formatted(name); + } + + } + // end::snippet_3[] + +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java new file mode 100644 index 00000000000..20653db04f3 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java @@ -0,0 +1,64 @@ +package io.helidon.docs.se.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.service.registry.Interception; +import io.helidon.service.registry.InterceptionContext; +import io.helidon.service.registry.Service; + +class InterceptorExample { + + // tag::snippet_1[] + /** + * An annotation to mark methods to be intercepted. + */ + @Interception.Intercepted + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @interface Traced { + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + @Service.NamedByType(Traced.class) //<1> + static class MyServiceInterceptor implements Interception.Interceptor { + static final List INVOKED = new ArrayList<>(); + + @Override + public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { + INVOKED.add("%s.%s: %s".formatted( + ctx.serviceInfo().serviceType().declaredName(), + ctx.elementInfo().elementName(), + Arrays.asList(args))); + return chain.proceed(args); //<2> + } + } + // end::snippet_2[] + + // tag::snippet_3[] + @Service.Singleton + static class MyServiceProvider implements Supplier { + @Override + public MyService get() { + return new MyService(); + } + } + + @Service.Contract + @Interception.Delegate + static class MyService { + + @Traced + String sayHello(String name) { + return "Hello %s!".formatted(name); + } + + } + // end::snippet_3[] + +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java new file mode 100644 index 00000000000..9a011500c21 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java @@ -0,0 +1,54 @@ +package io.helidon.docs.se.inject; + +import io.helidon.service.registry.Service; + +class Qualifier2Example { + + // tag::snippet_1[] + /** + * Custom Helidon Inject qualifier. + */ + @Service.Qualifier + public @interface HexCode { + String value(); + } + // end::snippet_1[] + + // tag::snippet_2[] + interface Color { + String name(); + } + + @HexCode("0000FF") + @Service.Singleton + static class BlueColor implements Color { + + @Override + public String name() { + return "blue"; + } + } + + @HexCode("008000") + @Service.Singleton + static class GreenColor implements Color { + + @Override + public String name() { + return "green"; + } + } + // end::snippet_2[] + + // tag::snippet_3[] + @Service.Singleton + record BlueCircle(@HexCode("0000FF") Color color) { + } + + @Service.Singleton + record GreenCircle(@HexCode("008000") Color color) { + } + // end::snippet_3[] + + +} diff --git a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java new file mode 100644 index 00000000000..d2d9f29a1a6 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java @@ -0,0 +1,58 @@ +package io.helidon.docs.se.inject; + +import io.helidon.service.registry.Service; + +class QualifierExample { + + + // tag::snippet_1[] + interface Color { + String hexCode(); + } + + @Service.Named("blue") + @Service.Singleton + public class Blue implements Color { + + @Override + public String hexCode() { + return "0000FF"; + } + } + + @Service.Named("green") + @Service.Singleton + public class Green implements Color { + + @Override + public String hexCode() { + return "008000"; + } + } + // end::snippet_1[] + + // tag::snippet_2[] + @Service.Singleton + record BlueCircle(@Service.Named("blue") Color color) { + } + // end::snippet_2[] + + // tag::snippet_3[] + @Service.Singleton + record GreenCircle(@Service.Named("green") Color color) { + } + // end::snippet_3[] + + // tag::snippet_4[] + @Service.NamedByType(GreenNamedByType.class) + @Service.Singleton + public class GreenNamedByType implements Color { + + @Override + public String hexCode() { + return "008000"; + } + } + // end::snippet_4[] + +} From f186a77a8842a45605784cfee5ede9fde4a90752 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 6 Feb 2025 13:29:39 +0100 Subject: [PATCH 02/18] lookup and start chapters added Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 73 ++++++++++++++++++- .../se/inject/ServiceRegistryExample.java | 16 ++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index bd81b03e9b2..c3231c39289 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -41,6 +41,8 @@ include::{rootdir}/includes/se.adoc[] - <> - <> - <> +- <> +- <> == Overview @@ -150,7 +152,7 @@ include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_2, indent=0] Now it just needs to be tested. The easiest way is to make a main method. The following piece of code initializes Service registry. After that we search for our `GreetingInjectionService` and execute it -to print out `Hello Tomas!`. +to print out `Hello Tomas!`. To find out more about this approach, please take a look into the <> chapter. [source,java] .Lookup our created service and execute it manually ---- @@ -384,10 +386,10 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] ---- ==== Event Emitter -Event emitters are code generated by Helidon. So we don't need to make them ourselves. +Event emitters are code generated by Helidon. So we don't make them ourselves. ==== Event Producer -An event producer is a service that triggers the event by using the event emitter. +An event producer is a service which triggers the event by using the event emitter. To emit an event, inject the desired event emitter, construct the corresponding event object, and call the emit method on the emitter instance. @@ -414,7 +416,7 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_3, indent=0] === Qualified Events -A Qualified Event is only delivered to Event Consumers that use the same qualifier. +A Qualified Event is only delivered to Event Observers that use the same qualifier. ==== Qualified Event Producer @@ -469,3 +471,66 @@ To declare an asynchronous observer use annotation `Event.AsyncObserver` instead include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_5, indent=0] ---- +=== Programmatic Lookup + +There are two primary ways to access services from the service registry: + +- *Dependency Injection* – Services are injected automatically at defined injection points. +- *Programmatic Lookup* – Services are retrieved manually by accessing the service registry directly. + +If you want to use programmatic lookup, there are two main ways to access a `ServiceRegistry` instance: + +- Create a new ServiceRegistry instance and search for the desired service. +- Inject the existing ServiceRegistry instance currently used by Helidon and retrieve services from it. + +To create a registry instance: + +[source,java] +---- +include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] +---- + +Note that all instances are created lazily, meaning the service registry does nothing by default. +If a service performs any actions during construction or post-construction, +you must first retrieve an instance from the registry to trigger its initialization. + +Special registry operations: + +- `List lookupServices(Lookup lookup)` - get all service descriptors that match the lookup +- `Optional 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 available +- `Optional first(...)` - immediately get an instance of a contract from the registry; there may not be an implementation +available +- `List all(...)` - immediately get all instances of a contract from the registry; result may be empty +- `Supplier supply(...)` - get a supplier of an instance; the service may be instantiated only when `get` is called +- `Supplier> supplyFirst(...)` - get a supplier of an optional instance +- `Supplier> supplyAll(...)` - get a supplier of all instances + +Lookup parameter options: + +- `Class` - the contract we are looking for +- `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) +- `Lookup` - a full search criteria for a registry lookup + +=== Startup + +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 `@RunLevel` annotation (i.e. `start(ApplicationBinding.create())`) +- `start(Binding, ServiceRegistryConfig)` - same as above, allows for customization of configuration, if used, do not forget to set discovery to `false` to 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, or `ServiceRegistryManager.create(...)` methods +- A generated `ApplicationMain` - optional feature of the Maven plugin, requires property `generateMain` to be set to `true` +- The Helidon startup class `io.helidon.Main`, which will start the registry manager and initialize all `RunLevel` services, though it uses service discover (which in turn must use reflection to get service descriptor instances) + diff --git a/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java new file mode 100644 index 00000000000..0a9532e2fab --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java @@ -0,0 +1,16 @@ +package io.helidon.docs.se.inject; + +import io.helidon.service.registry.ServiceRegistryManager; + +class ServiceRegistryExample { + + public void programmatic() { + // tag::snippet_1[] + // 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(); + // end::snippet_1[] + } + +} From 4939e1ba53c3fdfd9b4118cc4cf6c7ce67a792a0 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 6 Feb 2025 13:34:39 +0100 Subject: [PATCH 03/18] changed the title level structure Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 68 ++++++++++++------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index c3231c39289..bbbe409f6b1 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -68,7 +68,7 @@ To start using Helidon Inject, you need to create both: Let's begin by explaining what services are in Helidon Inject. -=== What is a service +== What is a service Services are: 1. Java classes annotated with one of the `Service.Scope` annotations, such as @@ -81,7 +81,7 @@ Services are: Now, let's talk about an injection points. -=== Injection points +== Injection points In Helidon, dependency injection can be done into the injection point in the following ways: 1. 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 declared private final) @@ -90,10 +90,10 @@ In Helidon, dependency injection can be done into the injection point in the fol Injected services are picked by the highest weight and implementing the requested contract. Only services can have Injection points. -=== Contract vs. service +== Contract vs. service Contract and service can be the same thing, but also separate entities. For simplicity, you can imagine contract as what you’re injecting/searching service registry for and service is what is responsible for adding new instances to the service registry. -=== Annotation processors +== Annotation processors To make everything work, it is necessary to add the following annotation processors to the compilation process of your application. @@ -125,13 +125,13 @@ For Maven: ---- -==== Why are these annotation processors needed? +=== Why are these annotation processors needed? Annotation processor `helidon-service-codegen` generates a service descriptor (`ServiceProvider__ServiceDescriptor`) for each discovered service. This descriptor is discovered at runtime and used to instantiate a service without the need to use reflection. Reflection is used only to get an instance of the service descriptor (by using its public `INSTANCE` singleton field). -=== Basic injection example +== Basic injection example Create a simple service class, which will be injected into another. @@ -162,7 +162,7 @@ include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_3, indent=0] If everything went as expected, no problems occurred Service registry gave us fully initialized and ready to use service. -=== Service Lifecycle +== Service Lifecycle The service registry manages the lifecycle of services. To ensure a method is invoked at a specific lifecycle phase, you can use the following annotations: @@ -174,14 +174,14 @@ The lifecycle behavior depends on the bean scope: - `@Service.PerLookup` – Only the post-construct method is invoked since the instance is not managed after the injection. - *Other scopes* – The pre-destroy method is invoked when the scope is deactivated (e.g. for singletons this happens during registry or JVM shutdown). -=== Qualifiers +== Qualifiers In dependency injection, a qualifier is a way to tell the framework which dependency to use when there are multiple options available. Annotations are considered qualifier if they’re "meta-annotated" with `@Service.Qualifier`. Helidon Inject comes with one qualifier provided out-of-the-box - the `@Service.Named` (and `@Service.NamedByType` which does the same thing, but uses class instead of a `String` name) -==== Named service injection +=== Named service injection Services can be assigned names, allowing them to be specified by name during injection. This ensures that the correct service is injected. To achieve this, we use the @Service.Named annotation. @@ -204,7 +204,7 @@ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_2, indent=0] include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_3, indent=0] ---- -==== Named by the type +=== Named by the type Alternatively, instead of using string-based names for services, a specific class can be used to "name" them. For this purpose, we use the @Service.NamedByType annotation. [source,java] @@ -215,7 +215,7 @@ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_4, indent=0] The way it is used on the injection point, it is the same as it was in case of the `@Service.Named`. -==== Custom qualifiers +=== Custom qualifiers To make custom qualifiers, it is necessary to "meta-annotated" it with `@Service.Qualifier`. [source,java] @@ -239,7 +239,7 @@ Once the services are created and qualified, they can be injected in the same wa include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_3, indent=0] ---- -=== Factories +== Factories 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: @@ -266,7 +266,7 @@ These challenges can be addressed by implementing one of the factory interfaces - `io.helidon.service.registry.Service.InjectionPointFactory` - `io.helidon.service.registry.Service.QualifiedFactory` -==== java.util.function.Supplier +=== java.util.function.Supplier A factory that supplies a single instance (it can also return `Supplier>`) [source,java] .Supplier factory @@ -274,7 +274,7 @@ A factory that supplies a single instance (it can also return `Supplier Binds this Interceptor to process annotated with `@Traced` <2> Passing interceptor processing to another interceptor in the chain -==== Delegate annotation +=== Delegate annotation The `@Interception.Delegate` annotation enables interception for classes or interfaces that aren’t created through the service registry but are instead produced by a factory. To enable interception, this annotation must be present on the class that the factory produces. While it is not required on interfaces, it will still function correctly if applied there. Let's make the same `@Traced` annotation and Interceptor as in the previous examples @@ -360,10 +360,10 @@ Method calls on an instance created this way can’t be intercepted. To enable i include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_3, indent=0] ---- -// ==== ExternalDelegate annotation +// === ExternalDelegate annotation // The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes or interfaces that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes or interfaces. -=== Events +== Events Events enable in-application communication between services by providing a mechanism to emit events and register consumers to handle them. A single event can be delivered to zero or more consumers. @@ -376,7 +376,7 @@ Key Terminology: - *Event Observer* – A service that listens for events, with a method annotated using `io.helidon.service.registry.Event.Observer`. - *Qualified Event* – An event emitted with a qualifier, using an annotation marked with `Service.Qualifier`. -==== Event Object +=== Event Object To begin emitting events, the first step is to define the event type itself. [source,java] @@ -385,10 +385,10 @@ To begin emitting events, the first step is to define the event type itself. include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] ---- -==== Event Emitter +=== Event Emitter Event emitters are code generated by Helidon. So we don't make them ourselves. -==== Event Producer +=== Event Producer An event producer is a service which triggers the event by using the event emitter. To emit an event, inject the desired event emitter, construct the corresponding event object, and call the emit method on the emitter instance. @@ -400,7 +400,7 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_2, indent=0] ---- The method returns only after all event observers have been notified. If any observer throws an exception, an `EventDispatchException` is thrown, with all caught exceptions added as suppressed. This ensures that all observers are invoked, even if an exception occurs. -==== Event Observer +=== Event Observer An event observer is a service which processes fired event. To create an event observer: @@ -414,11 +414,11 @@ To create an event observer: include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_3, indent=0] ---- -=== Qualified Events +== Qualified Events A Qualified Event is only delivered to Event Observers that use the same qualifier. -==== Qualified Event Producer +=== Qualified Event Producer A qualified event can be produced with two options: @@ -432,7 +432,7 @@ In this example below, we create event producer, which fires event only to obser include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_6, indent=0] ---- -==== Qualified Event Observers +=== Qualified Event Observers To consume a qualified event, observer method must be annotated with the correct qualifier(s). @@ -442,14 +442,14 @@ To consume a qualified event, observer method must be annotated with the correct include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_7, indent=0] ---- -=== Asynchronous Events +== Asynchronous Events Events can be emitted asynchronously, and event observers can also operate asynchronously. An executor service for handling asynchronous events can be registered in the service registry by providing a service that implements the `java.util.concurrent.ExecutorService` contract and is named `io.helidon.service.registry.EventManager`. If no custom executor service is provided, the system defaults to a per-task executor using Virtual Threads, with thread names prefixed as `inject-event-manager-`. -==== Asynchronous Event Producer +=== Asynchronous Event Producer All asynchronous event producers must use the `Event.Emitter.emitAsync(..)` method instead of the synchronous `Event.Emitter.emit(..)`. The `emitAsync` method returns a `CompletionStage` instance. When executed, it completes once all event observers have been submitted to the executor service. However, there is no guarantee that any event has been delivered—it may have been sent to anywhere from 0 to n observers (where n represents the number of synchronous observers). @@ -460,7 +460,7 @@ The `emitAsync` method returns a `CompletionStage` instance. When execu include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_4, indent=0] ---- -==== Asynchronous Observer +=== Asynchronous Observer 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`. @@ -471,7 +471,7 @@ To declare an asynchronous observer use annotation `Event.AsyncObserver` instead include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_5, indent=0] ---- -=== Programmatic Lookup +== Programmatic Lookup There are two primary ways to access services from the service registry: @@ -517,7 +517,7 @@ Lookup parameter options: - `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) - `Lookup` - a full search criteria for a registry lookup -=== Startup +== Startup 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. From 478f164102781cfbc056bcc60f9a80242f402363 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 6 Feb 2025 15:32:23 +0100 Subject: [PATCH 04/18] minor improvements to not finished parts and currently skipped ones Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 11 ++++++ .../se/inject/InterceptorDelegateExample.java | 34 ++++++++++++++----- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index bbbe409f6b1..06ee7a0850a 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -296,9 +296,13 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_3, indent=0] === io.helidon.service.registry.Service.InjectionPointFactory A factory that provides zero or more instances for each injection point. +//TODO + === io.helidon.service.registry.Service.QualifiedFactory A factory that provides zero or more instances based on a specific qualifier and contract. +//TODO + == Interceptors Interception allows intercepting calls to constructors or methods, and even fields when used as injection points. @@ -361,7 +365,14 @@ include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_3, in ---- // === ExternalDelegate annotation +// TODO Uncomment this, once this issue is fixed https://github.com/helidon-io/helidon/issues/9726 // The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes or interfaces that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes or interfaces. +// +// [source,java] +// .ExternalDelegate used for external class/interface +// ---- +// include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_4, indent=0] +// ---- == Events Events enable in-application communication between services by providing a mechanism to emit events and register consumers to handle them. diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java index b9dc96977c4..c0e01c58ed5 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java @@ -21,15 +21,11 @@ class InterceptorDelegateExample { @Service.Singleton @Service.NamedByType(Traced.class) - static class MyServiceInterceptor implements Interception.Interceptor { - static final List INVOKED = new ArrayList<>(); + class MyServiceInterceptor implements Interception.Interceptor { @Override public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { - INVOKED.add("%s.%s: %s".formatted( - ctx.serviceInfo().serviceType().declaredName(), - ctx.elementInfo().elementName(), - Arrays.asList(args))); + //Do something return chain.proceed(args); } } @@ -37,7 +33,7 @@ public V proceed(InterceptionContext ctx, Chain chain, Object... args) th // tag::snippet_2[] @Service.Singleton - static class MyServiceProvider implements Supplier { + class MyServiceProvider implements Supplier { @Override public MyService get() { return new MyService(); @@ -48,7 +44,7 @@ public MyService get() { // tag::snippet_3[] @Service.Contract @Interception.Delegate - static class MyService { + class MyService { @Traced String sayHello(String name) { @@ -58,4 +54,26 @@ String sayHello(String name) { } // end::snippet_3[] + + + // tag::snippet_4[] + /** + * Assume this is the class we have no control over. + */ + class SomeExternalClass { + String sayHello(String name) { + return "Hello %s!".formatted(name); + } + } + + @Service.Singleton + @Interception.ExternalDelegate(SomeExternalClass.class) + class SomeExternalClassProvider implements Supplier { + @Override + public SomeExternalClass get() { + return new SomeExternalClass(); + } + } + // end::snippet_4[] + } From 5e7f5ea066df94d06a6094b70e6b3de89632f6c0 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 6 Feb 2025 15:35:10 +0100 Subject: [PATCH 05/18] copyright added Signed-off-by: David Kral --- .../io/helidon/docs/se/inject/BasicExample.java | 17 +++++++++++++++-- .../helidon/docs/se/inject/EventsExample.java | 17 +++++++++++++++-- .../helidon/docs/se/inject/FactoryExample.java | 15 +++++++++++++++ .../se/inject/InterceptorDelegateExample.java | 15 +++++++++++++++ .../docs/se/inject/InterceptorExample.java | 15 +++++++++++++++ .../docs/se/inject/Qualifier2Example.java | 15 +++++++++++++++ .../docs/se/inject/QualifierExample.java | 15 +++++++++++++++ .../docs/se/inject/ServiceRegistryExample.java | 15 +++++++++++++++ 8 files changed, 120 insertions(+), 4 deletions(-) diff --git a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java index 4998ac861f0..255bc23b17f 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java @@ -1,7 +1,20 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; -import java.util.function.Function; - import io.helidon.service.registry.Service; import io.helidon.service.registry.ServiceRegistryManager; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java index 01737cec6f1..f3c403e301e 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java @@ -1,7 +1,20 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletionStage; import io.helidon.service.registry.Event; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java index b98291b6e55..3bc60cbdea7 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import java.util.List; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java index c0e01c58ed5..84acd9394d7 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import java.lang.annotation.ElementType; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java index 20653db04f3..49e89e56f91 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import java.lang.annotation.ElementType; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java index 9a011500c21..3d1367ee041 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import io.helidon.service.registry.Service; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java index d2d9f29a1a6..5c29f124d05 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import io.helidon.service.registry.Service; diff --git a/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java index 0a9532e2fab..c9f2a2ff899 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import io.helidon.service.registry.ServiceRegistryManager; From c3d2f44229fc0522a0d3ca11215c32945e4341be Mon Sep 17 00:00:00 2001 From: David Kral Date: Fri, 7 Feb 2025 13:39:32 +0100 Subject: [PATCH 06/18] Factory types clarified Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 59 +++++++++++++++---- .../docs/se/inject/FactoryExample.java | 45 +++++++++++++- .../docs/se/inject/InterceptorExample.java | 7 +-- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 06ee7a0850a..23e912e365c 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -283,25 +283,49 @@ A factory that creates zero or more implementations of a given contract. include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_2, indent=0] ---- -If the created services need to be qualified, the relevant qualifier information must also be included in the factory annotations. +If the created services need to be qualified, any relevant qualifier information must also be included in the factory annotations. -Note: If one doesn’t want to list all the names provided by this factory, it is possible use `*` to cover all the possible names. +Note: If one doesn’t want to list all the name qualifiers provided by this factory, it is possible use `*` to cover all the possible names. [source,java] -.Qualified ServicesFactory example ---- include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_3, indent=0] ---- +=== io.helidon.service.registry.Service.QualifiedFactory +Qualified factory is strictly bound to a specific qualifier type. +It will get executed only for injection points, +which are annotated with this selected qualifier and are intended for a selected contract. +It will be ignored for any other injection points. + +It receives a Lookup object as a parameter, which contains all necessary information about the injection point. +This allows the factory to create instances dynamically based on the injection point information. + +[source,java] +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_4, indent=0] +---- + === io.helidon.service.registry.Service.InjectionPointFactory -A factory that provides zero or more instances for each injection point. +This type of factory is very similar to `QualifiedFactory`, +but with one key difference—it is executed for each injection point and is not bound to a specific qualifier/s (unless specified). -//TODO +It receives a Lookup object as a parameter, which contains all necessary information about the injection point. +This allows the factory to create instances dynamically based on the injection point information. -=== io.helidon.service.registry.Service.QualifiedFactory -A factory that provides zero or more instances based on a specific qualifier and contract. +[source,java] +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_5, indent=0] +---- + +It is possible to restrict this factory to specific qualifiers by specifying them at the class level of the factory. -//TODO +This ensures that the factory is executed only for injection points that match the intended contract and qualifier. + +[source,java] +---- +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_6, indent=0] +---- == Interceptors Interception allows intercepting calls to constructors or methods, and even fields when used as injection points. @@ -338,7 +362,12 @@ include::{sourcedir}/se/inject/InterceptorExample.java[tag=snippet_2, indent=0] <2> Passing interceptor processing to another interceptor in the chain === Delegate annotation -The `@Interception.Delegate` annotation enables interception for classes or interfaces that aren’t created through the service registry but are instead produced by a factory. To enable interception, this annotation must be present on the class that the factory produces. While it is not required on interfaces, it will still function correctly if applied there. +The `@Interception.Delegate` annotation enables interception for classes +that aren’t created through the service registry +but are instead produced by a factory (More about factories can be found here - +<>). +To enable interception, this annotation must be present on the class that the factory produces. +While it is not required on interfaces, it will still function correctly if applied there. Let's make the same `@Traced` annotation and Interceptor as in the previous examples @@ -356,7 +385,13 @@ Now, let's create the factory of the service instance. include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_2, indent=0] ---- -Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the `@Interception.Delegate` annotation. However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. Additionally, if a class is annotated with `@Interception.Delegate`, it must have a non-private default constructor. +Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the `@Interception.Delegate` annotation. However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. + +If you need to enable interception for classes using delegation, you should: + +- The class must have accessible no-arg constructor (at least package local) +- The constructor should have no side effects, as the instance will act only as a wrapper for the delegate +- All invoked methods must be accessible (at least package local) [source,java] .Delegate used on the class @@ -366,10 +401,10 @@ include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_3, in // === ExternalDelegate annotation // TODO Uncomment this, once this issue is fixed https://github.com/helidon-io/helidon/issues/9726 -// The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes or interfaces that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes or interfaces. +// The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes. // // [source,java] -// .ExternalDelegate used for external class/interface +// .ExternalDelegate used for external class // ---- // include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_4, indent=0] // ---- diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java index 3bc60cbdea7..409121d723a 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -16,8 +16,11 @@ package io.helidon.docs.se.inject; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; +import io.helidon.common.GenericType; +import io.helidon.service.registry.Lookup; import io.helidon.service.registry.Qualifier; import io.helidon.service.registry.Service; @@ -51,12 +54,48 @@ public List> services() { // tag::snippet_3[] @Service.Singleton - @Service.Named("test") - class MyQualifiedServiceFactory implements Service.ServicesFactory { + @Service.Named("name") + class MyServiceFactoryWithQualifier implements Service.ServicesFactory { @Override public List> services() { - return List.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("test"))); + return List.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); } } // end::snippet_3[] + + // tag::snippet_4[] + @Service.Singleton + class QualifiedFactory implements Service.QualifiedFactory { + + @Override + public Optional> first(Qualifier qualifier, + Lookup lookup, + GenericType genericType) { + return Optional.of(Service.QualifiedInstance.create(new MyService())); + } + } + // end::snippet_4[] + + // tag::snippet_5[] + @Service.Singleton + class InjectionPointFactory implements Service.InjectionPointFactory { + + @Override + public Optional> first(Lookup lookup) { + return Optional.of(Service.QualifiedInstance.create(new MyService())); + } + } + // end::snippet_5[] + + // tag::snippet_6[] + @Service.Singleton + @Service.Named("name") + class InjectionPointFactoryWithQualifier implements Service.InjectionPointFactory { + + @Override + public Optional> first(Lookup lookup) { + return Optional.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); + } + } + // end::snippet_6[] } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java index 49e89e56f91..ab319152884 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java @@ -42,14 +42,9 @@ class InterceptorExample { @Service.Singleton @Service.NamedByType(Traced.class) //<1> static class MyServiceInterceptor implements Interception.Interceptor { - static final List INVOKED = new ArrayList<>(); - @Override public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { - INVOKED.add("%s.%s: %s".formatted( - ctx.serviceInfo().serviceType().declaredName(), - ctx.elementInfo().elementName(), - Arrays.asList(args))); + //Do something return chain.proceed(args); //<2> } } From 91531fd3b1d4551f2226752cadc0ba41c56f8ee2 Mon Sep 17 00:00:00 2001 From: David Kral Date: Mon, 10 Feb 2025 17:25:14 +0100 Subject: [PATCH 07/18] minor adjustments Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 47 +++++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 23e912e365c..55b645603be 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -75,7 +75,6 @@ Services are: - `@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) -- `@Service.PerInstance` - a service that has instances created for each named instance of the service it is driven by 2. Any class with `@Service.Inject` annotation that doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. 3. Any `core` service defined for Helidon Service Registry (using annotation `@Service.Provider`), the scope is `@Service.PerLookup` if the service implements a `Supplier`, and `@Singleton` otherwise; all dependencies are considered injection points @@ -90,9 +89,53 @@ In Helidon, dependency injection can be done into the injection point in the fol Injected services are picked by the highest weight and implementing the requested contract. Only services can have Injection points. +=== Injected dependency types + +Injected dependency types can be as follows: + +- `Contract` - simply get an instance of another service +- `Optional` - get an instance of another service, the other service may not be available. +If not available an empty optional is supplied +- `List` - get instances of all services that are available +- `Supplier`, `Supplier>`, `Supplier>` - equivalent methods but the value is resolved +when `Supplier.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`. + == Contract vs. service Contract and service can be the same thing, but also separate entities. For simplicity, you can imagine contract as what you’re injecting/searching service registry for and service is what is responsible for adding new instances to the service registry. +=== Contract +Contract can be concrete class or just an interface. + +[source,java] +.Contract example +---- +var registry = ServiceRegistryManager.create().registry(); +var serviceInstance = registry.get(MyContract.class); //<1> +---- + +<1> `MyContract.class` is a contract. It is the type we are searching service registry for + +=== Service +This can be either a concrete class, +which implements the contract (or is also a contract) OR it can be a factory/producer, +which creates a new instances to be registered into the service registry. + +[source,java] +.Service example +---- +@Service.Singleton +class MyService { + + public String greet(String name) { + return "Hello %s!".formatted(name); + } + +} +---- + == Annotation processors To make everything work, it is necessary to add the following annotation processors to the compilation process of your application. @@ -159,7 +202,7 @@ to print out `Hello Tomas!`. To find out more about this approach, please take a include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_3, indent=0] ---- -If everything went as expected, no problems occurred Service registry gave us fully initialized +If everything went as expected, no problems occurred and Service registry gave us fully initialized and ready to use service. == Service Lifecycle From dd3b00f8d7cc70da5e687e51574afc3f60ca742c Mon Sep 17 00:00:00 2001 From: David Kral Date: Mon, 10 Feb 2025 17:50:12 +0100 Subject: [PATCH 08/18] another improvements Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 40 +++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 55b645603be..c4729eb12e8 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -100,17 +100,34 @@ If not available an empty optional is supplied - `Supplier`, `Supplier>`, `Supplier>` - equivalent methods but the value is resolved when `Supplier.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`. +Equivalent behavior can be achieved programmatically through `io.helidon.service.registry.ServiceRegistry` instance. This can be obtained from a `ServiceRegistryManager`. See <> chapter. == Contract vs. service Contract and service can be the same thing, but also separate entities. For simplicity, you can imagine contract as what you’re injecting/searching service registry for and service is what is responsible for adding new instances to the service registry. +=== Service +This can be either a concrete class, +which implements the contract (or is also a contract) OR it can be a factory/producer, +which creates a new instances to be registered into the service registry. + +[source,java] +.Service example +---- +@Service.Singleton +class MyService { + + public String greet(String name) { + return "Hello %s!".formatted(name); + } + +} +---- + === Contract Contract can be concrete class or just an interface. [source,java] -.Contract example +.Contract usage example with programmatic lookup ---- var registry = ServiceRegistryManager.create().registry(); var serviceInstance = registry.get(MyContract.class); //<1> @@ -118,24 +135,23 @@ var serviceInstance = registry.get(MyContract.class); //<1> <1> `MyContract.class` is a contract. It is the type we are searching service registry for -=== Service -This can be either a concrete class, -which implements the contract (or is also a contract) OR it can be a factory/producer, -which creates a new instances to be registered into the service registry. - [source,java] -.Service example +.Contract usage example with injection ---- @Service.Singleton -class MyService { +class SomeOtherService { + private MyContract myContract; - public String greet(String name) { - return "Hello %s!".formatted(name); + @Service.Inject + SomeOtherService(MyContract myContract) { //<1> + this.myContract = myContract; } } ---- +<1> `MyContract` is here used as a type we are injecting + == Annotation processors To make everything work, it is necessary to add the following annotation processors to the compilation process of your application. From fdd1c9316e9fa66c138a47acd334cfa95dc1a183 Mon Sep 17 00:00:00 2001 From: David Kral Date: Mon, 10 Feb 2025 19:58:30 +0100 Subject: [PATCH 09/18] minor adjustments Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index c4729eb12e8..a9a0fcc68ed 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -89,9 +89,9 @@ In Helidon, dependency injection can be done into the injection point in the fol Injected services are picked by the highest weight and implementing the requested contract. Only services can have Injection points. -=== Injected dependency types +=== Injected dependency formats -Injected dependency types can be as follows: +Injected dependency formats can be as follows: - `Contract` - simply get an instance of another service - `Optional` - get an instance of another service, the other service may not be available. From 8686e42a789f9f57ced4bbd747e5d3c20561fad2 Mon Sep 17 00:00:00 2001 From: David Kral Date: Wed, 12 Feb 2025 09:33:04 +0100 Subject: [PATCH 10/18] some changes from the monday meeting added Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 172 ++++++++++-------- .../helidon/docs/se/inject/BasicExample.java | 2 +- .../docs/se/inject/Qualifier2Example.java | 18 +- .../docs/se/inject/QualifierExample.java | 10 +- 4 files changed, 114 insertions(+), 88 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index a9a0fcc68ed..9a840405ea7 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -31,10 +31,11 @@ include::{rootdir}/includes/se.adoc[] - <> - <> - <> -- <> +- <> +- <> - <> -- <> -- <> +- <> +- <> - <> - <> - <> @@ -64,57 +65,59 @@ include::{rootdir}/includes/dependencies.adoc[] To start using Helidon Inject, you need to create both: - A service that will be injected. -- An injection point, where the service will be injected. +- An injection point, where the service instance will be injected. -Let's begin by explaining what services are in Helidon Inject. +Let's begin by explaining some basic terms. -== What is a service -Services are: +== Basic terms -1. Java classes annotated with one of the `Service.Scope` annotations, 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) -2. Any class with `@Service.Inject` annotation that doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. -3. Any `core` service defined for Helidon Service Registry (using annotation `@Service.Provider`), the scope is `@Service.PerLookup` if the service implements a `Supplier`, and `@Singleton` otherwise; all dependencies are considered injection points +=== Declarative style of programming +In a declarative approach, you use annotations on classes, constructors, and constructor arguments to express your intent. -Now, let's talk about an injection points. +For example, instead of manually managing dependencies, +you declare that a class should be injectable using annotations like `@Service.Singleton` +(More about that later). -== Injection points -In Helidon, dependency injection can be done into the injection point in the following ways: +=== Service registry +A service registry is a tool that enables declarative programming by supporting inversion of control (IoC). -1. 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 declared private final) -2. 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 can’t be declared as final +It manages the lifecycle of services, handles dependency injection, +and ensures that the correct instances are provided where needed and without requiring manual instantiation. -Injected services are picked by the highest weight and implementing the requested contract. -Only services can have Injection points. +=== Inversion of control +Instead of manually creating an instance of a certain type, you can delegate its creation to the service registry. -=== Injected dependency formats +This allows the registry to handle the entire instantiation process and provide the instance when needed, +ensuring proper lifecycle management and dependency resolution. -Injected dependency formats can be as follows: +=== Contract +A contract is a type that defines what the service registry should provide. +It represents an API that will be used. -- `Contract` - simply get an instance of another service -- `Optional` - get an instance of another service, the other service may not be available. -If not available an empty optional is supplied -- `List` - get instances of all services that are available -- `Supplier`, `Supplier>`, `Supplier>` - equivalent methods but the value is resolved -when `Supplier.get()` is called, to allow more control +For simplicity, a contract can be thought of as an interface, +but it can also be an abstract class or even a concrete class. -Equivalent behavior can be achieved programmatically through `io.helidon.service.registry.ServiceRegistry` instance. This can be obtained from a `ServiceRegistryManager`. See <> chapter. +[source,java] +.Contract example +---- +interface GreetingContract { -== Contract vs. service -Contract and service can be the same thing, but also separate entities. For simplicity, you can imagine contract as what you’re injecting/searching service registry for and service is what is responsible for adding new instances to the service registry. + String greet(String name); + +} +---- === Service -This can be either a concrete class, -which implements the contract (or is also a contract) OR it can be a factory/producer, +This can be either a concrete class, which implements the contract +(or is contract itself if it was a concrete class), +or it can be a factory/producer (more about <>), which creates a new instances to be registered into the service registry. [source,java] .Service example ---- @Service.Singleton -class MyService { +class MyGreetingService implements GreetingContract { public String greet(String name) { return "Hello %s!".formatted(name); @@ -123,42 +126,55 @@ class MyService { } ---- -=== Contract -Contract can be concrete class or just an interface. +=== Contract vs. service +Contract and service can be the same thing, but also separate entities. -[source,java] -.Contract usage example with programmatic lookup ----- -var registry = ServiceRegistryManager.create().registry(); -var serviceInstance = registry.get(MyContract.class); //<1> ----- +== How are services defined +Services are defined by: -<1> `MyContract.class` is a contract. It is the type we are searching service registry for +1. Java classes annotated with one of the `Service.Scope` annotations (see <>) +2. Any class with `@Service.Inject` annotation even when it doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. -[source,java] -.Contract usage example with injection ----- -@Service.Singleton -class SomeOtherService { - private MyContract myContract; +Now, let's talk about an injection points. - @Service.Inject - SomeOtherService(MyContract myContract) { //<1> - this.myContract = myContract; - } +== Injection points +In Helidon, dependency injection can be done into the injection point in the following ways: -} ----- +1. 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 declared private final) +2. 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 can’t be declared as final + +Injected services are picked by the highest weight and implementing the requested contract. +Only services can have Injection points. + +=== Injected dependency formats + +Dependencies can be injected in different formats, depending on the required behavior: -<1> `MyContract` is here used as a type we are injecting +- `Contract` - Retrieves an instance of another service. +- `Optional` - Retrieves an instance, but if the service is unavailable, an empty optional is provided. +- `List` - Retrieves all available instances of a given service. +- `Supplier`, `Supplier>`, `Supplier>` - Similar to the above, but the value is only resolved when get() is called, allowing lazy evaluation for more control. -== Annotation processors -To make everything work, it is necessary to add the following annotation processors to -the compilation process of your application. +The same behavior can be achieved programmatically using the `io.helidon.service.registry.ServiceRegistry` instance. +To obtain a `ServiceRegistry`, you can retrieve it from a `ServiceRegistryManager`, +allowing manual lookup and management of services. See <> chapter. + +== Scopes +Helidon Inject provides three built-in scopes: + +- `@Service.Singleton` – A single instance exists in the service registry for the application's lifetime. +- `@Service.PerLookup` – A new instance is created each time a lookup occurs (including when injected into an injection point). +- `@Service.PerRequest` – A single instance per request exists in the service registry. The definition of a "request" is not enforced by the injection framework but aligns with concepts like an HTTP request-response cycle or message consumption in a messaging system. + +== Build time +To ensure everything functions correctly, +you need to add the following annotation processors to your application's compilation process. + +These processors generate the necessary metadata and wiring for dependency injection and service registration in Helidon Inject. For Maven: [source,xml] -.Example annotation processor configuration in the Maven +.Example annotation processor configuration in Maven ---- @@ -186,13 +202,11 @@ For Maven: === Why are these annotation processors needed? Annotation processor `helidon-service-codegen` generates a service descriptor (`ServiceProvider__ServiceDescriptor`) for each discovered service. -This descriptor is discovered at runtime and used to instantiate a service without the need to use reflection. - -Reflection is used only to get an instance of the service descriptor (by using its public `INSTANCE` singleton field). +This descriptor is discovered at runtime and is used to instantiate the service without relying on reflection, improving performance and reducing overhead during service creation. == Basic injection example -Create a simple service class, which will be injected into another. +To demonstrate how Helidon Inject works, let's create a simple working example where one service is injected into another. [source,java] .Creating simple Greeter service. @@ -200,8 +214,8 @@ Create a simple service class, which will be injected into another. include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_1, indent=0] ---- -Once the Greeter service is created, an injection point for such a service is needed now. -Let's create another service, which injects Greeter service as its constructor parameter. +Once the Greeter service is created, an injection point for this service is now required. +Let's create another service that injects the Greeter service as its constructor parameter. [source,java] .Create simple injection point @@ -211,7 +225,7 @@ include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_2, indent=0] Now it just needs to be tested. The easiest way is to make a main method. The following piece of code initializes Service registry. After that we search for our `GreetingInjectionService` and execute it -to print out `Hello Tomas!`. To find out more about this approach, please take a look into the <> chapter. +to print out `Hello David!`. To find out more about this manual approach, please take a look into the <> chapter. [source,java] .Lookup our created service and execute it manually ---- @@ -225,13 +239,10 @@ and ready to use service. The service registry manages the lifecycle of services. To ensure a method is invoked at a specific lifecycle phase, you can use the following annotations: -- `@Service.PostConstruct` – Invokes the annotated method after the instance has been created and fully injected. -- `@Service.PreDestroy` – Invokes the annotated method when the service is no longer in use by the registry. - -The lifecycle behavior depends on the bean scope: - -- `@Service.PerLookup` – Only the post-construct method is invoked since the instance is not managed after the injection. -- *Other scopes* – The pre-destroy method is invoked when the scope is deactivated (e.g. for singletons this happens during registry or JVM shutdown). +* `@Service.PostConstruct` – Invokes the annotated method after the instance has been created and fully injected. +* `@Service.PreDestroy` – Invokes the annotated method when the service is no longer in use by the registry. (Such as if the intended scope ends) +** `@Service.PerLookup` – PreDestroy annotated method is not invoked, since it is not managed by the service registry after the injection. +** *Other scopes* – The pre-destroy method is invoked when the scope is deactivated (e.g. for singletons this happens during registry or JVM shutdown). == Qualifiers In dependency injection, a qualifier is a way to tell the framework which dependency to use when there are multiple options available. @@ -274,18 +285,25 @@ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_4, indent=0] The way it is used on the injection point, it is the same as it was in case of the `@Service.Named`. +[source,java] +.Named by type injection point +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_5, indent=0] +---- + === Custom qualifiers To make custom qualifiers, it is necessary to "meta-annotated" it with `@Service.Qualifier`. [source,java] -.Custom qualifier creation +.Create Blue and Green custom qualifiers ---- include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_1, indent=0] ---- -The `@HexCode` annotation serves as our new qualifier. It can now be used to qualify services in the same way as `@Service.Named`. +The `@Blue` and `@Green` annotations serve as our new qualifiers. +It can now be used to qualify services in the same way as `@Service.Named`. [source,java] -.Custom qualifier HexCode usage +.Custom qualifier Blue and Green usage ---- include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_2, indent=0] ---- diff --git a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java index 255bc23b17f..f90397a0b2a 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java @@ -24,7 +24,7 @@ class BasicExample { @Service.Singleton class Greeter { - public String greet(String name) { + String greet(String name) { return "Hello %s!".formatted(name); } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java index 3d1367ee041..ad619aae5a7 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/Qualifier2Example.java @@ -20,12 +20,12 @@ class Qualifier2Example { // tag::snippet_1[] - /** - * Custom Helidon Inject qualifier. - */ @Service.Qualifier - public @interface HexCode { - String value(); + public @interface Blue { + } + + @Service.Qualifier + public @interface Green { } // end::snippet_1[] @@ -34,7 +34,7 @@ interface Color { String name(); } - @HexCode("0000FF") + @Blue @Service.Singleton static class BlueColor implements Color { @@ -44,7 +44,7 @@ public String name() { } } - @HexCode("008000") + @Green @Service.Singleton static class GreenColor implements Color { @@ -57,11 +57,11 @@ public String name() { // tag::snippet_3[] @Service.Singleton - record BlueCircle(@HexCode("0000FF") Color color) { + record BlueCircle(@Blue Color color) { } @Service.Singleton - record GreenCircle(@HexCode("008000") Color color) { + record GreenCircle(@Green Color color) { } // end::snippet_3[] diff --git a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java index 5c29f124d05..e012e4d6c19 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java @@ -59,7 +59,7 @@ record GreenCircle(@Service.Named("green") Color color) { // end::snippet_3[] // tag::snippet_4[] - @Service.NamedByType(GreenNamedByType.class) + @Service.NamedByType(Green.class) @Service.Singleton public class GreenNamedByType implements Color { @@ -70,4 +70,12 @@ public String hexCode() { } // end::snippet_4[] + // tag::snippet_5[] + @Service.Singleton + record GreenCircleType(@Service.NamedByType(Green.class) Color color) { + } + // end::snippet_5[] + + + } From dbc4346d5d6c94cca4feb78d5925ef9dee65c21d Mon Sep 17 00:00:00 2001 From: David Kral Date: Fri, 14 Feb 2025 14:08:06 +0100 Subject: [PATCH 11/18] Minor adjustments and wording updates Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 28 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 9a840405ea7..351c4cc9faf 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -71,6 +71,16 @@ Let's begin by explaining some basic terms. == Basic terms +=== Dependency Injection +Injection is a way to automatically provide instances of dependencies without having to create them manually. +Instead of a class creating an object itself, +something else (like a <>) hands it over when needed. +This makes code cleaner, easier to manage, and more flexible. + +For example, if a Car needs an Engine, instead of the Car making an Engine itself, it just asks for one, +and the system provides it. +This is called Dependency Injection (DI). + === Declarative style of programming In a declarative approach, you use annotations on classes, constructors, and constructor arguments to express your intent. @@ -135,6 +145,9 @@ Services are defined by: 1. Java classes annotated with one of the `Service.Scope` annotations (see <>) 2. Any class with `@Service.Inject` annotation even when it doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. +Keep in mind that if you create any service instance directly, it will not get its injection points resolved! +This works only when using service registry. + Now, let's talk about an injection points. == Injection points @@ -167,7 +180,7 @@ Helidon Inject provides three built-in scopes: - `@Service.PerRequest` – A single instance per request exists in the service registry. The definition of a "request" is not enforced by the injection framework but aligns with concepts like an HTTP request-response cycle or message consumption in a messaging system. == Build time -To ensure everything functions correctly, +To ensure everything works correctly, you need to add the following annotation processors to your application's compilation process. These processors generate the necessary metadata and wiring for dependency injection and service registration in Helidon Inject. @@ -232,7 +245,10 @@ to print out `Hello David!`. To find out more about this manual approach, please include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_3, indent=0] ---- -If everything went as expected, no problems occurred and Service registry gave us fully initialized +The last step is ensuring that everything necessary for your application to compile correctly with injection is included. +See <>. + +If everything went as expected, no problems occurred and a Service registry gave us fully initialized and ready to use service. == Service Lifecycle @@ -443,8 +459,6 @@ The `@Interception.Delegate` annotation enables interception for classes that aren’t created through the service registry but are instead produced by a factory (More about factories can be found here - <>). -To enable interception, this annotation must be present on the class that the factory produces. -While it is not required on interfaces, it will still function correctly if applied there. Let's make the same `@Traced` annotation and Interceptor as in the previous examples @@ -463,13 +477,17 @@ include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_2, in ---- Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the `@Interception.Delegate` annotation. However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. +To enable interception, this annotation must be present on the class that the factory produces. +While it is not required on interfaces, it will still work correctly if applied there. -If you need to enable interception for classes using delegation, you should: +If you need to enable interception for classes using delegation, you should make sure about the following: - The class must have accessible no-arg constructor (at least package local) - The constructor should have no side effects, as the instance will act only as a wrapper for the delegate - All invoked methods must be accessible (at least package local) +Therefore using `@Interception.Delegate` on the classes can be considered as acknowledgement of the rules above. + [source,java] .Delegate used on the class ---- From 115582fabef824b30045d68339d7d3f4bc8ee0e7 Mon Sep 17 00:00:00 2001 From: David Kral Date: Tue, 18 Feb 2025 12:53:19 +0100 Subject: [PATCH 12/18] The first part of example suggestions implemented Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 24 +++++++-------- .../helidon/docs/se/inject/BasicExample.java | 7 +++-- .../docs/se/inject/FactoryExample.java | 30 ++++++++----------- .../docs/se/inject/InterceptorExample.java | 23 +------------- .../se/inject/ServiceRegistryExample.java | 11 +++++++ 5 files changed, 40 insertions(+), 55 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 351c4cc9faf..9edb4a9e2bc 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -368,23 +368,13 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_1, indent=0] ---- === io.helidon.service.registry.Service.ServicesFactory -A factory that creates zero or more implementations of a given contract. +The ServicesFactory should be used only for its intended purpose — creating zero OR n instances at runtime, each assigned different qualifiers. This allows for dynamic and flexible service creation while ensuring proper differentiation between instances based on their qualifiers. [source,java] -.ServicesFactory example ---- include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_2, indent=0] ---- -If the created services need to be qualified, any relevant qualifier information must also be included in the factory annotations. - -Note: If one doesn’t want to list all the name qualifiers provided by this factory, it is possible use `*` to cover all the possible names. - -[source,java] ----- -include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_3, indent=0] ----- - === io.helidon.service.registry.Service.QualifiedFactory Qualified factory is strictly bound to a specific qualifier type. It will get executed only for injection points, @@ -396,7 +386,7 @@ This allows the factory to create instances dynamically based on the injection p [source,java] ---- -include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_4, indent=0] +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_3, indent=0] ---- === io.helidon.service.registry.Service.InjectionPointFactory @@ -408,7 +398,7 @@ This allows the factory to create instances dynamically based on the injection p [source,java] ---- -include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_5, indent=0] +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_4, indent=0] ---- It is possible to restrict this factory to specific qualifiers by specifying them at the class level of the factory. @@ -417,7 +407,7 @@ This ensures that the factory is executed only for injection points that match t [source,java] ---- -include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_6, indent=0] +include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_5, indent=0] ---- == Interceptors @@ -631,6 +621,12 @@ To create a registry instance: include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] ---- +Keep in mind, that ServiceRegistryManager needs to be shut down, once it is not needed +[source,java] +---- +include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_2, indent=0] +---- + Note that all instances are created lazily, meaning the service registry does nothing by default. If a service performs any actions during construction or post-construction, you must first retrieve an instance from the registry to trigger its initialization. diff --git a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java index f90397a0b2a..e86735cef86 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java @@ -50,9 +50,12 @@ void printGreeting(String name) { // tag::snippet_3[] public static void main(String[] args) { - var registry = ServiceRegistryManager.create().registry(); + var serviceRegistryManager = ServiceRegistryManager.create(); + var registry = serviceRegistryManager.registry(); var greetings = registry.get(GreetingInjectionService.class); - greetings.printGreeting("Tomas"); + greetings.printGreeting("David"); + + serviceRegistryManager.shutdown(); //Once not needed, it should be shut down } // end::snippet_3[] diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java index 409121d723a..dc12d2e4d16 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -47,36 +47,32 @@ public MyService get() { class MyServiceFactory implements Service.ServicesFactory { @Override public List> services() { - return List.of(Service.QualifiedInstance.create(new MyService())); + var named = Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name")); + var named2 = Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name2")); + return List.of(named, named2); } } // end::snippet_2[] // tag::snippet_3[] - @Service.Singleton - @Service.Named("name") - class MyServiceFactoryWithQualifier implements Service.ServicesFactory { - @Override - public List> services() { - return List.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); - } + @Service.Qualifier + public @interface Color { + String value(); } - // end::snippet_3[] - // tag::snippet_4[] @Service.Singleton - class QualifiedFactory implements Service.QualifiedFactory { + class QualifiedFactory implements Service.QualifiedFactory { @Override public Optional> first(Qualifier qualifier, Lookup lookup, GenericType genericType) { - return Optional.of(Service.QualifiedInstance.create(new MyService())); + return Optional.of(Service.QualifiedInstance.create(new MyService(), qualifier)); } } - // end::snippet_4[] + // end::snippet_3[] - // tag::snippet_5[] + // tag::snippet_4[] @Service.Singleton class InjectionPointFactory implements Service.InjectionPointFactory { @@ -85,9 +81,9 @@ public Optional> first(Lookup lookup) { return Optional.of(Service.QualifiedInstance.create(new MyService())); } } - // end::snippet_5[] + // end::snippet_4[] - // tag::snippet_6[] + // tag::snippet_5[] @Service.Singleton @Service.Named("name") class InjectionPointFactoryWithQualifier implements Service.InjectionPointFactory { @@ -97,5 +93,5 @@ public Optional> first(Lookup lookup) { return Optional.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); } } - // end::snippet_6[] + // end::snippet_5[] } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java index ab319152884..78e0d2e6d82 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorExample.java @@ -41,7 +41,7 @@ class InterceptorExample { // tag::snippet_2[] @Service.Singleton @Service.NamedByType(Traced.class) //<1> - static class MyServiceInterceptor implements Interception.Interceptor { + class MyServiceInterceptor implements Interception.Interceptor { @Override public V proceed(InterceptionContext ctx, Chain chain, Object... args) throws Exception { //Do something @@ -50,25 +50,4 @@ public V proceed(InterceptionContext ctx, Chain chain, Object... args) th } // end::snippet_2[] - // tag::snippet_3[] - @Service.Singleton - static class MyServiceProvider implements Supplier { - @Override - public MyService get() { - return new MyService(); - } - } - - @Service.Contract - @Interception.Delegate - static class MyService { - - @Traced - String sayHello(String name) { - return "Hello %s!".formatted(name); - } - - } - // end::snippet_3[] - } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java index c9f2a2ff899..17e48c24c7d 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/ServiceRegistryExample.java @@ -28,4 +28,15 @@ public void programmatic() { // end::snippet_1[] } + public void programmatic2() { + // tag::snippet_2[] + // create an instance of a registry manager - can be configured and shut down + var registryManager = ServiceRegistryManager.create(); + // Your desired logic with ServiceRegistry + + // Once ServiceRegistryManager is no longer needed, it needs to be closed + registryManager.shutdown(); + // end::snippet_2[] + } + } From 6381a01754b538a681fdf37976280590d4205281 Mon Sep 17 00:00:00 2001 From: David Kral Date: Mon, 24 Feb 2025 17:11:15 +0100 Subject: [PATCH 13/18] Minor factory example changed and javadoc links added Signed-off-by: David Kral --- .../main/asciidoc/includes/attributes.adoc | 1 + docs/src/main/asciidoc/se/injection.adoc | 179 +++++++++++------- .../docs/se/inject/FactoryExample.java | 37 ++-- 3 files changed, 137 insertions(+), 80 deletions(-) diff --git a/docs/src/main/asciidoc/includes/attributes.adoc b/docs/src/main/asciidoc/includes/attributes.adoc index 6aa77dd4e88..64cac76c7c0 100644 --- a/docs/src/main/asciidoc/includes/attributes.adoc +++ b/docs/src/main/asciidoc/includes/attributes.adoc @@ -229,6 +229,7 @@ endif::[] :scheduling-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.scheduling :security-integration-jersey-base-url: {javadoc-base-url}/io.helidon.security.integration.jersey :security-integration-webserver-base-url: {javadoc-base-url}/io.helidon.webserver.security +:service-registry-base-url: {javadoc-base-url}/io.helidon.service.registry :telemetry-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.telemetry :tracing-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing :tracing-otel-provider-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing.providers.opentelemetry diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 9edb4a9e2bc..5ebc9f9008d 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -85,7 +85,8 @@ This is called Dependency Injection (DI). In a declarative approach, you use annotations on classes, constructors, and constructor arguments to express your intent. For example, instead of manually managing dependencies, -you declare that a class should be injectable using annotations like `@Service.Singleton` +you declare that a class should be injectable using annotations like +link:{service-registry-base-url}/io/helidon/service/registry/Service.Singleton.html[`@Service.Singleton`] (More about that later). === Service registry @@ -129,6 +130,7 @@ which creates a new instances to be registered into the service registry. @Service.Singleton class MyGreetingService implements GreetingContract { + @Override public String greet(String name) { return "Hello %s!".formatted(name); } @@ -138,12 +140,18 @@ class MyGreetingService implements GreetingContract { === Contract vs. service Contract and service can be the same thing, but also separate entities. +It all depends on the design of your application and which approach you choose. == How are services defined Services are defined by: -1. Java classes annotated with one of the `Service.Scope` annotations (see <>) -2. Any class with `@Service.Inject` annotation even when it doesn’t have a scope annotation. In such a case, the scope of the service will be set as `@Service.PerLookup`. +1. Java classes annotated with one of the +link:{service-registry-base-url}/io/helidon/service/registry/Service.Scope.html[`@Service.Scope`] +annotations (see <>) +2. Any class with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Inject.html[`@Service.Inject`] +annotation even when it doesn’t have a scope annotation. In such a case, the scope of the service will be set as +link:{service-registry-base-url}/io/helidon/service/registry/Service.PerLookup.html[`@Service.PerLookup`]. Keep in mind that if you create any service instance directly, it will not get its injection points resolved! This works only when using service registry. @@ -153,8 +161,12 @@ Now, let's talk about an injection points. == Injection points In Helidon, dependency injection can be done into the injection point in the following ways: -1. 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 declared private final) -2. 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 can’t be declared as final +1. Through a constructor annotated with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Inject.html[`@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 declared private final) +2. Through field(s) annotated with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Inject.html[`@Service.Inject`] - +each field is considered an injection point; this is not recommended, as the fields must be accessible (at least package local), and can’t be declared as final Injected services are picked by the highest weight and implementing the requested contract. Only services can have Injection points. @@ -168,22 +180,30 @@ Dependencies can be injected in different formats, depending on the required beh - `List` - Retrieves all available instances of a given service. - `Supplier`, `Supplier>`, `Supplier>` - Similar to the above, but the value is only resolved when get() is called, allowing lazy evaluation for more control. -The same behavior can be achieved programmatically using the `io.helidon.service.registry.ServiceRegistry` instance. -To obtain a `ServiceRegistry`, you can retrieve it from a `ServiceRegistryManager`, +The same behavior can be achieved programmatically using the +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance. +To obtain a link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`], +you can retrieve it from a +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`], allowing manual lookup and management of services. See <> chapter. == Scopes Helidon Inject provides three built-in scopes: -- `@Service.Singleton` – A single instance exists in the service registry for the application's lifetime. -- `@Service.PerLookup` – A new instance is created each time a lookup occurs (including when injected into an injection point). -- `@Service.PerRequest` – A single instance per request exists in the service registry. The definition of a "request" is not enforced by the injection framework but aligns with concepts like an HTTP request-response cycle or message consumption in a messaging system. +- link:{service-registry-base-url}/io/helidon/service/registry/Service.Singleton.html[`@Service.Singleton`] – +A single instance exists in the service registry for the application's lifetime. +- link:{service-registry-base-url}/io/helidon/service/registry/Service.PerLookup.html[`@Service.PerLookup`] – +A new instance is created each time a lookup occurs (including when injected into an injection point). +- link:{service-registry-base-url}/io/helidon/service/registry/Service.PerRequest.html[`@Service.PerRequest`] – +A single instance per request exists in the service registry. +The definition of a "request" is not enforced by the injection framework +but aligns with concepts like an HTTP request-response cycle or message consumption in a messaging system. == Build time To ensure everything works correctly, you need to add the following annotation processors to your application's compilation process. -These processors generate the necessary metadata and wiring for dependency injection and service registration in Helidon Inject. +These processors generate the necessary metadata and wiring for dependency injection and service registration. For Maven: [source,xml] @@ -219,7 +239,7 @@ This descriptor is discovered at runtime and is used to instantiate the service == Basic injection example -To demonstrate how Helidon Inject works, let's create a simple working example where one service is injected into another. +To demonstrate how injection works, let's create a simple working example where one service is injected into another. [source,java] .Creating simple Greeter service. @@ -255,17 +275,24 @@ and ready to use service. The service registry manages the lifecycle of services. To ensure a method is invoked at a specific lifecycle phase, you can use the following annotations: -* `@Service.PostConstruct` – Invokes the annotated method after the instance has been created and fully injected. -* `@Service.PreDestroy` – Invokes the annotated method when the service is no longer in use by the registry. (Such as if the intended scope ends) -** `@Service.PerLookup` – PreDestroy annotated method is not invoked, since it is not managed by the service registry after the injection. +* link:{service-registry-base-url}/io/helidon/service/registry/Service.PostConstruct.html[`@Service.PostConstruct`] +– Invokes the annotated method after the instance has been created and fully injected. +* link:{service-registry-base-url}/io/helidon/service/registry/Service.PreDestroy.html[`@Service.PreDestroy`] +– Invokes the annotated method when the service is no longer in use by the registry. (Such as if the intended scope ends) +** link:{service-registry-base-url}/io/helidon/service/registry/Service.PerLookup.html[`@Service.PerLookup`] +– PreDestroy annotated method is not invoked, since it is not managed by the service registry after the injection. ** *Other scopes* – The pre-destroy method is invoked when the scope is deactivated (e.g. for singletons this happens during registry or JVM shutdown). == Qualifiers In dependency injection, a qualifier is a way to tell the framework which dependency to use when there are multiple options available. -Annotations are considered qualifier if they’re "meta-annotated" with `@Service.Qualifier`. +Annotations are considered qualifier if they’re "meta-annotated" with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. -Helidon Inject comes with one qualifier provided out-of-the-box - the `@Service.Named` (and `@Service.NamedByType` which does the same thing, but uses class instead of a `String` name) +Helidon Inject comes with one qualifier provided out-of-the-box - the +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] +(and link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] +which does the same thing, but uses class instead of a `String` name) === Named service injection @@ -299,7 +326,8 @@ Alternatively, instead of using string-based names for services, a specific clas include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_4, indent=0] ---- -The way it is used on the injection point, it is the same as it was in case of the `@Service.Named`. +The way it is used on the injection point, it is the same as it was in case of the +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`]. [source,java] .Named by type injection point @@ -308,7 +336,8 @@ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_5, indent=0] ---- === Custom qualifiers -To make custom qualifiers, it is necessary to "meta-annotated" it with `@Service.Qualifier`. +To make custom qualifiers, it is necessary to "meta-annotated" it with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. [source,java] .Create Blue and Green custom qualifiers @@ -316,7 +345,8 @@ To make custom qualifiers, it is necessary to "meta-annotated" it with `@Service include::{sourcedir}/se/inject/Qualifier2Example.java[tag=snippet_1, indent=0] ---- The `@Blue` and `@Green` annotations serve as our new qualifiers. -It can now be used to qualify services in the same way as `@Service.Named`. +It can now be used to qualify services in the same way as +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`]. [source,java] .Custom qualifier Blue and Green usage @@ -354,12 +384,12 @@ However, this approach only works if the contract is an interface and we’re im These challenges can be addressed by implementing one of the factory interfaces supported by the Helidon Service Registry: -- `java.util.function.Supplier` -- `io.helidon.service.registry.Service.ServicesFactory` -- `io.helidon.service.registry.Service.InjectionPointFactory` -- `io.helidon.service.registry.Service.QualifiedFactory` +- <> +- <> +- <> +- <> -=== java.util.function.Supplier +=== Supplier A factory that supplies a single instance (it can also return `Supplier>`) [source,java] .Supplier factory @@ -367,16 +397,20 @@ A factory that supplies a single instance (it can also return `Supplier Passing interceptor processing to another interceptor in the chain === Delegate annotation -The `@Interception.Delegate` annotation enables interception for classes -that aren’t created through the service registry -but are instead produced by a factory (More about factories can be found here - -<>). +The link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`] +annotation enables interception for classes that aren’t created through the service registry +but are instead produced by a factory (More about factories can be found here - <>). Let's make the same `@Traced` annotation and Interceptor as in the previous examples @@ -466,7 +503,9 @@ Now, let's create the factory of the service instance. include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_2, indent=0] ---- -Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the `@Interception.Delegate` annotation. However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. +Method calls on an instance created this way can’t be intercepted. To enable interception in such cases, we use the +link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`] annotation. +However, keep in mind that usage of this annotation doesn’t add the ability to intercept constructor calls. To enable interception, this annotation must be present on the class that the factory produces. While it is not required on interfaces, it will still work correctly if applied there. @@ -476,7 +515,8 @@ If you need to enable interception for classes using delegation, you should make - The constructor should have no side effects, as the instance will act only as a wrapper for the delegate - All invoked methods must be accessible (at least package local) -Therefore using `@Interception.Delegate` on the classes can be considered as acknowledgement of the rules above. +Therefore, using link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`] +on the classes can be considered as acknowledgement of the rules above. [source,java] .Delegate used on the class @@ -504,8 +544,10 @@ Key Terminology: - *Event Object* – Any object that is sent as an event. - *Event Emitter* – A service responsible for emitting events into the event system. - *Event Producer* – A service that triggers an event by calling an emitter. -- *Event Observer* – A service that listens for events, with a method annotated using `io.helidon.service.registry.Event.Observer`. -- *Qualified Event* – An event emitted with a qualifier, using an annotation marked with `Service.Qualifier`. +- *Event Observer* – A service that listens for events, with a method annotated using +link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. +- *Qualified Event* – An event emitted with a qualifier, using an annotation marked with +link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. === Event Object To begin emitting events, the first step is to define the event type itself. @@ -520,7 +562,7 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] Event emitters are code generated by Helidon. So we don't make them ourselves. === Event Producer -An event producer is a service which triggers the event by using the event emitter. +An event producer is a service that triggers the event by using the event emitter. To emit an event, inject the desired event emitter, construct the corresponding event object, and call the emit method on the emitter instance. @@ -529,7 +571,9 @@ To emit an event, inject the desired event emitter, construct the corresponding ---- include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_2, indent=0] ---- -The method returns only after all event observers have been notified. If any observer throws an exception, an `EventDispatchException` is thrown, with all caught exceptions added as suppressed. This ensures that all observers are invoked, even if an exception occurs. +The method returns only after all event observers have been notified. If any observer throws an exception, an +link:{service-registry-base-url}/io/helidon/service/registry/EventDispatchException.html[`EventDispatchException`] +is thrown, with all caught exceptions added as suppressed. This ensures that all observers are invoked, even if an exception occurs. === Event Observer An event observer is a service which processes fired event. @@ -537,7 +581,8 @@ An event observer is a service which processes fired event. 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` +- annotate the method with +link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`] [source,java] .Event observer example @@ -553,7 +598,9 @@ A Qualified Event is only delivered to Event Observers that use the same qualifi A qualified event can be produced with two options: -1. The injection point of `Event.Emitter` (the constructor parameter, or field) is annotated with a qualifier annotation +1. The injection point of +link:{service-registry-base-url}/io/helidon/service/registry/Event.Emitter.html[`@Event.Emitter`] +(the constructor parameter, or field) is annotated with a qualifier annotation 2. 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! In this example below, we create event producer, which fires event only to observers qualified with name "id". @@ -576,7 +623,9 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_7, indent=0] == Asynchronous Events Events can be emitted asynchronously, and event observers can also operate asynchronously. -An executor service for handling asynchronous events can be registered in the service registry by providing a service that implements the `java.util.concurrent.ExecutorService` contract and is named `io.helidon.service.registry.EventManager`. +An executor service for handling asynchronous events can be registered in the service registry by providing +a service that implements the `java.util.concurrent.ExecutorService` contract +and is named link:{service-registry-base-url}/io/helidon/service/registry/EventManager.html[`EventManager`]. If no custom executor service is provided, the system defaults to a per-task executor using Virtual Threads, with thread names prefixed as `inject-event-manager-`. @@ -592,9 +641,12 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_4, indent=0] ---- === Asynchronous Observer -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`. +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 link:{service-registry-base-url}/io/helidon/service/registry/EventManager.html[`EventManager`]. -To declare an asynchronous observer use annotation `Event.AsyncObserver` instead of `Event.Observer`. +To declare an asynchronous observer use annotation +link:{service-registry-base-url}/io/helidon/service/registry/Event.AsyncObserver.html[`@Event.AsyncObserver`] +instead of +link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. [source,java] .Asynchronous Event Observer Example @@ -609,7 +661,8 @@ There are two primary ways to access services from the service registry: - *Dependency Injection* – Services are injected automatically at defined injection points. - *Programmatic Lookup* – Services are retrieved manually by accessing the service registry directly. -If you want to use programmatic lookup, there are two main ways to access a `ServiceRegistry` instance: +If you want to use programmatic lookup, there are two main ways to access a +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance: - Create a new ServiceRegistry instance and search for the desired service. - Inject the existing ServiceRegistry instance currently used by Helidon and retrieve services from it. @@ -621,7 +674,9 @@ To create a registry instance: include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] ---- -Keep in mind, that ServiceRegistryManager needs to be shut down, once it is not needed +Keep in mind, that +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`] +needs to be shut down, once it is not needed. [source,java] ---- include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_2, indent=0] @@ -631,13 +686,6 @@ Note that all instances are created lazily, meaning the service registry does no If a service performs any actions during construction or post-construction, you must first retrieve an instance from the registry to trigger its initialization. -Special registry operations: - -- `List lookupServices(Lookup lookup)` - get all service descriptors that match the lookup -- `Optional 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 available @@ -660,7 +708,8 @@ Helidon provides a Maven plugin (`io.helidon.service:helidon-service-maven-plugi 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`: +Methods that accept the bindings are on +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`]: - `start(Binding)` - starts the service registry with the generated binding, initializing all singleton and per-lookup services annotated with a `@RunLevel` annotation (i.e. `start(ApplicationBinding.create())`) - `start(Binding, ServiceRegistryConfig)` - same as above, allows for customization of configuration, if used, do not forget to set discovery to `false` to prevent automated discovery from the classpath diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java index dc12d2e4d16..8b19f0be4c3 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -56,34 +56,41 @@ public List> services() { // tag::snippet_3[] @Service.Qualifier - public @interface Color { + @interface SystemProperty { String value(); } @Service.Singleton - class QualifiedFactory implements Service.QualifiedFactory { + class SystemProperties { - @Override - public Optional> first(Qualifier qualifier, - Lookup lookup, - GenericType genericType) { - return Optional.of(Service.QualifiedInstance.create(new MyService(), qualifier)); + private final String httpHost; + private final String httpPort; + + SystemProperties(@SystemProperty("http.host") String httpHost, + @SystemProperty("http.port") String httpPort) { + this.httpHost = httpHost; + this.httpPort = httpPort; } + } - // end::snippet_3[] - // tag::snippet_4[] @Service.Singleton - class InjectionPointFactory implements Service.InjectionPointFactory { + class SystemPropertyFactory implements Service.QualifiedFactory { @Override - public Optional> first(Lookup lookup) { - return Optional.of(Service.QualifiedInstance.create(new MyService())); + public Optional> first(Qualifier qualifier, + Lookup lookup, + GenericType genericType) { + String propertyValue = qualifier.stringValue() + .map(System::getProperty) + .orElse(""); + return Optional.of(Service.QualifiedInstance.create(propertyValue, qualifier)); } + } - // end::snippet_4[] + // end::snippet_3[] - // tag::snippet_5[] + // tag::snippet_4[] @Service.Singleton @Service.Named("name") class InjectionPointFactoryWithQualifier implements Service.InjectionPointFactory { @@ -93,5 +100,5 @@ public Optional> first(Lookup lookup) { return Optional.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); } } - // end::snippet_5[] + // end::snippet_4[] } From b27b204d68351d74a2a1b469d859a24cf2861895 Mon Sep 17 00:00:00 2001 From: David Kral Date: Wed, 26 Feb 2025 12:45:02 +0100 Subject: [PATCH 14/18] Suggestions implemented Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 230 ++++++++++++++---- .../helidon/docs/se/inject/EventsExample.java | 7 +- .../docs/se/inject/FactoryExample.java | 32 ++- .../se/inject/InterceptorDelegateExample.java | 7 +- .../docs/se/inject/RunLevelExample.java | 39 +++ 5 files changed, 249 insertions(+), 66 deletions(-) create mode 100644 docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 5ebc9f9008d..47d9fe2efcc 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -168,30 +168,34 @@ each parameter is considered an injection point; this is the recommended way of link:{service-registry-base-url}/io/helidon/service/registry/Service.Inject.html[`@Service.Inject`] - each field is considered an injection point; this is not recommended, as the fields must be accessible (at least package local), and can’t be declared as final -Injected services are picked by the highest weight and implementing the requested contract. -Only services can have Injection points. +Injection points are satisfied by services that match the required contract and qualifiers. +If more than one service satisfies an injection point, the service with the highest weight is chosen (see +link:{common-javadoc-base-url}/io/helidon/common/Weight.html[`@Weight`], +link:{common-javadoc-base-url}/io/helidon/common/Weighted.html[`Weighted`]). +If two services have the same weight, the order is undefined. +If the injection point accepts a `java.util.List` of contracts, +all available services are used to satisfy the injection point ordered by weight. + +It is also important to note, that only services can have injection points. === Injected dependency formats Dependencies can be injected in different formats, depending on the required behavior: - `Contract` - Retrieves an instance of another service. -- `Optional` - Retrieves an instance, but if the service is unavailable, an empty optional is provided. +- `Optional` - Retrieves an instance, but if there is no service providing the contract, an empty optional is provided. - `List` - Retrieves all available instances of a given service. -- `Supplier`, `Supplier>`, `Supplier>` - Similar to the above, but the value is only resolved when get() is called, allowing lazy evaluation for more control. - -The same behavior can be achieved programmatically using the -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance. -To obtain a link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`], -you can retrieve it from a -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`], -allowing manual lookup and management of services. See <> chapter. +- `Supplier`, `Supplier>`, `Supplier>` - Similar to the above, +but the value is only resolved when get() is called, +allowing lazy evaluation to resolve cyclic dependencies and +a "storm" of initializations when a service is looked up from the registry. +When suppliers aren’t used, all instances MUST be created when the service instance is created (during construction). == Scopes -Helidon Inject provides three built-in scopes: +There are three built-in scopes: - link:{service-registry-base-url}/io/helidon/service/registry/Service.Singleton.html[`@Service.Singleton`] – -A single instance exists in the service registry for the application's lifetime. +A single instance exists in the service registry for the registry lifetime. - link:{service-registry-base-url}/io/helidon/service/registry/Service.PerLookup.html[`@Service.PerLookup`] – A new instance is created each time a lookup occurs (including when injected into an injection point). - link:{service-registry-base-url}/io/helidon/service/registry/Service.PerRequest.html[`@Service.PerRequest`] – @@ -205,7 +209,6 @@ you need to add the following annotation processors to your application's compil These processors generate the necessary metadata and wiring for dependency injection and service registration. -For Maven: [source,xml] .Example annotation processor configuration in Maven ---- @@ -289,10 +292,13 @@ In dependency injection, a qualifier is a way to tell the framework which depend Annotations are considered qualifier if they’re "meta-annotated" with link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. -Helidon Inject comes with one qualifier provided out-of-the-box - the -link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] -(and link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] -which does the same thing, but uses class instead of a `String` name) +Helidon Inject provides two built-in qualifier: + +- link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] – +Uses a `String` name to qualify a service. +- link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] – +Works the same way as `@Service.Named` but uses a class type instead. +The name that would be used is the fully qualified name of the type. === Named service injection @@ -390,7 +396,7 @@ These challenges can be addressed by implementing one of the factory interfaces - <> === Supplier -A factory that supplies a single instance (it can also return `Supplier>`) +A factory that supplies a single instance (it can also return `Supplier>` or `Optional`) [source,java] .Supplier factory ---- @@ -411,11 +417,18 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_2, indent=0] === QualifiedFactory link:{service-registry-base-url}/io/helidon/service/registry/Service.QualifiedFactory.html[`QualifiedFactory`] is strictly bound to a specific qualifier type. -It will get executed only for injection points, -which are annotated with this selected qualifier and are intended for a selected contract. +It will get executed only for injection points +that are annotated with this selected qualifier and are intended for a selected contract. It will be ignored for any other injection points. -It receives a Lookup object as a parameter, which contains all necessary information about the injection point. +It receives: + +- link:{service-registry-base-url}/io/helidon/service/registry/Qualifier.html[`Qualifier`] +instance (metadata of the qualifier that is on the annotated injection point, with each annotation property available) +- link:{service-registry-base-url}/io/helidon/service/registry/Lookup.html[`Lookup`] instance +- link:{common-javadoc-base-url}/io/helidon/common/GenericType.html[`GenericType`] instance of the injection point +(contains information about the injection point type) + This allows the factory to create instances dynamically based on the injection point information. [source,java] @@ -431,6 +444,8 @@ but with one key difference—it is executed for each injection point and is not It receives a Lookup object as a parameter, which contains all necessary information about the injection point. This allows the factory to create instances dynamically based on the injection point information. +If the factory type `T` is `java.lang.Object`, the factory can handle ANY contract - +such as when injecting configuration properties, which may be of any type (boolean, int, String etc.) It is possible to restrict this factory to specific qualifiers by specifying them at the class level of the factory. @@ -442,7 +457,8 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_4, indent=0] ---- == Interceptors -Interception allows intercepting calls to constructors or methods, and even fields when used as injection points. +Interception allows adding behavior to constructors, methods, +and injected fields without the need to explicitly code the required functionality in place. By default, interception is enabled only for elements annotated with link:{service-registry-base-url}/io/helidon/service/registry/Interception.Intercepted.html[`Interception.Intercepted`]. @@ -469,9 +485,16 @@ include::{sourcedir}/se/inject/InterceptorExample.java[tag=snippet_1, indent=0] === Interceptor interface The link:{service-registry-base-url}/io/helidon/service/registry/Interception.Interceptor.html[`Interception.Interceptor`] interface defines an interceptor service that intercepts methods/constructors/fields annotated with the configured marker annotation. -This supported marker annotation is specified using -link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`]. -To properly handle the interception chain, the interceptor must always invoke the `proceed` method. +The interceptor service must be named by the fully qualified name of the +link:{service-registry-base-url}/io/helidon/service/registry/Interception.Intercepted.html[`Interception.Intercepted`] +annotation (either via +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] or +link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`]). + +To properly handle the interception chain, the interceptor must always invoke the `proceed` method +if the invocation should continue normally. +However, it is also possible to return a custom value directly from the interceptor and +effectively bypass the original method execution. [source,java] .Sample Interceptor interface implementation @@ -479,7 +502,7 @@ To properly handle the interception chain, the interceptor must always invoke th include::{sourcedir}/se/inject/InterceptorExample.java[tag=snippet_2, indent=0] ---- -<1> Binds this Interceptor to process annotated with `@Traced` +<1> Binds this Interceptor to process elements annotated with `@Traced` <2> Passing interceptor processing to another interceptor in the chain === Delegate annotation @@ -511,9 +534,9 @@ While it is not required on interfaces, it will still work correctly if applied If you need to enable interception for classes using delegation, you should make sure about the following: -- The class must have accessible no-arg constructor (at least package local) +- The class must have accessible no-arg constructor (at least package local) and mustn’t be final - The constructor should have no side effects, as the instance will act only as a wrapper for the delegate -- All invoked methods must be accessible (at least package local) +- All invoked methods must be accessible (at least package local) and non-final Therefore, using link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`] on the classes can be considered as acknowledgement of the rules above. @@ -524,15 +547,41 @@ on the classes can be considered as acknowledgement of the rules above. include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_3, indent=0] ---- -// === ExternalDelegate annotation -// TODO Uncomment this, once this issue is fixed https://github.com/helidon-io/helidon/issues/9726 -// The `@Interception.ExternalDelegate` annotation functions similarly to `@Interception.Delegate`. However, the key difference is that `@Interception.ExternalDelegate` is designed for classes that you don’t control. This means it allows you to apply the interception mechanism even to third-party classes. -// -// [source,java] -// .ExternalDelegate used for external class -// ---- -// include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_4, indent=0] -// ---- +=== ExternalDelegate annotation +The link:{service-registry-base-url}/io/helidon/service/registry/Interception.ExternalDelegate.html[`@Interception.ExternalDelegate`] +annotation works similarly to +link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`]. +However, the key difference is that +link:{service-registry-base-url}/io/helidon/service/registry/Interception.ExternalDelegate.html[`@Interception.ExternalDelegate`] +is designed for classes that you don’t have control over. +This means it allows you to apply the interception mechanism even to third-party classes. + +It is not required to apply this annotation on the interfaces, however, it needs to be present for classes. + +Let's make the same `@Traced` annotation and Interceptor as in the previous examples + +[source,java] +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_1, indent=0] +---- + +In this example, we can’t change the class we’re intercepting in any way. +Let's assume we have this external class, and we need to add an interception mechanism to it. + +[source,java] +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_4, indent=0] +---- + +Now we need to apply the +link:{service-registry-base-url}/io/helidon/service/registry/Interception.ExternalDelegate.html[`@Interception.ExternalDelegate`] +annotation on the factory class. Once this is done, interception will work as expected + +[source,java] +.ExternalDelegate annotation used on the factory +---- +include::{sourcedir}/se/inject/InterceptorDelegateExample.java[tag=snippet_5, indent=0] +---- == Events Events enable in-application communication between services by providing a mechanism to emit events and register consumers to handle them. @@ -542,7 +591,7 @@ A single event can be delivered to zero or more consumers. Key Terminology: - *Event Object* – Any object that is sent as an event. -- *Event Emitter* – A service responsible for emitting events into the event system. +- *Event Emitter* – A service responsible for emitting events into the event system. Emitters are always generated. - *Event Producer* – A service that triggers an event by calling an emitter. - *Event Observer* – A service that listens for events, with a method annotated using link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. @@ -559,12 +608,17 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] ---- === Event Emitter -Event emitters are code generated by Helidon. So we don't make them ourselves. +Event emitters are code generated by Helidon. The emitter is generated when an injection point is discovered that expects it. + +All the emitters are generated as implementations of the +link:{service-registry-base-url}/io/helidon/service/registry/Event.Emitter.html[`Event.Emitter`] interface. === Event Producer An event producer is a service that triggers the event by using the event emitter. -To emit an event, inject the desired event emitter, construct the corresponding event object, and call the emit method on the emitter instance. +To emit an event, inject the desired +link:{service-registry-base-url}/io/helidon/service/registry/Event.Emitter.html[`Event.Emitter`] +Event.Emitter instance, construct the corresponding event object, and call the emit method on the emitter instance. [source,java] .Event producer example @@ -603,7 +657,9 @@ link:{service-registry-base-url}/io/helidon/service/registry/Event.Emitter.html[ (the constructor parameter, or field) is annotated with a qualifier annotation 2. 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! -In this example below, we create event producer, which fires event only to observers qualified with name "id". +We are using qualifier created in the chapter <>, +to demonstrate how events work with qualifiers. +Now we need to create a new event producer, which fires event only to observers qualified with `@Blue`. [source,java] .Qualified event producer ---- @@ -613,6 +669,9 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_6, indent=0] === Qualified Event Observers To consume a qualified event, observer method must be annotated with the correct qualifier(s). +If we want to consume the same messages which are produced by the producer in the example above, +our observer needs to be annotated with the same qualifier. +In this case it is `@Blue`. [source,java] .Qualified event observer @@ -627,7 +686,8 @@ An executor service for handling asynchronous events can be registered in the se a service that implements the `java.util.concurrent.ExecutorService` contract and is named link:{service-registry-base-url}/io/helidon/service/registry/EventManager.html[`EventManager`]. -If no custom executor service is provided, the system defaults to a per-task executor using Virtual Threads, with thread names prefixed as `inject-event-manager-`. +If no custom executor service is provided, the system defaults to a thread-per-task executor using Virtual Threads, +with thread names prefixed as `inject-event-manager-`. === Asynchronous Event Producer All asynchronous event producers must use the `Event.Emitter.emitAsync(..)` method instead of the synchronous `Event.Emitter.emit(..)`. @@ -661,11 +721,20 @@ There are two primary ways to access services from the service registry: - *Dependency Injection* – Services are injected automatically at defined injection points. - *Programmatic Lookup* – Services are retrieved manually by accessing the service registry directly. -If you want to use programmatic lookup, there are two main ways to access a +If you want to use programmatic lookup, there are several ways how to get a link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance: -- Create a new ServiceRegistry instance and search for the desired service. -- Inject the existing ServiceRegistry instance currently used by Helidon and retrieve services from it. +* Get the global +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] via +link:{service-registry-base-url}/io/helidon/service/registry/GlobalServiceRegistry.html[`GlobalServiceRegistry`] +** This shouldn’t be used from the factories or services. +Intended use is in the `main` methods or when one needs static access. +* Create a new +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] +instance and search for the desired service. +* Inject the +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] +instance which is used to create the service instance. To create a registry instance: @@ -698,7 +767,7 @@ available Lookup parameter options: -- `Class` - the contract we are looking for +- `Class` - the contract we’re looking for - `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) - `Lookup` - a full search criteria for a registry lookup @@ -711,12 +780,71 @@ Default name is `ApplicationBinding` (customizable) Methods that accept the bindings are on link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`]: -- `start(Binding)` - starts the service registry with the generated binding, initializing all singleton and per-lookup services annotated with a `@RunLevel` annotation (i.e. `start(ApplicationBinding.create())`) -- `start(Binding, ServiceRegistryConfig)` - same as above, allows for customization of configuration, if used, do not forget to set discovery to `false` to prevent automated discovery from the classpath +- `start(Binding)` - starts the service registry with the generated binding, +initializing all singleton and per-lookup services annotated with a +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] +annotation (i.e. `start(ApplicationBinding.create())`) +- `start(Binding, ServiceRegistryConfig)` - same as above, allows for customization of configuration, +if used, remember to set discovery to `false` to 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, or `ServiceRegistryManager.create(...)` methods -- A generated `ApplicationMain` - optional feature of the Maven plugin, requires property `generateMain` to be set to `true` -- The Helidon startup class `io.helidon.Main`, which will start the registry manager and initialize all `RunLevel` services, though it uses service discover (which in turn must use reflection to get service descriptor instances) +- A generated `ApplicationMain` - optional feature of the Maven plugin, requires property `generateMain` to be set to `true`. +It uses link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] actively, +but via code generated classes -> See <> for more information. +This is the only approach that is fully reflection free and skips lookups for injection points. +- The Helidon startup class `io.helidon.Main`, which will start the registry manager and initialize all +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] +services, though it uses service discover (which in turn must use reflection to get service descriptor instances) + +=== ServiceRegistryManager +Manager is responsible for managing the state of a single +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance. + +When created programmatically, two possible methods can be chosen. + +- `create` - Creates a new +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance, +but does not populate it with any services. Service registry is only populated when asked for a specific contract. +- `start` - Creates a new +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance and +creates all the available services. +It also properly executes any available +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] annotations +present on the service. +See <> chapter. + +It is important to note, that once you don’t need your service registry, +method `shutdown` on the manager must be called to ensure proper termination of the service registry. + +=== RunLevel +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] +is the annotation used for specifying the order in which certain services should be created. +It always starts from the lowest number to the highest. + +To start, add the +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] +on the service. + +[source,java] +---- +include::{sourcedir}/se/inject/RunLevelExample.java[tag=snippet_1, indent=0] +---- +For better understanding, we can also add helpful method, which get executed upon service construction. + +The easiest way for us to use these annotations, is to use `start` method on the +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`]. + +[source,java] +---- +include::{sourcedir}/se/inject/RunLevelExample.java[tag=snippet_2, indent=0] +---- + +Once executed, we get the following output: +[source,text] +---- +level1 created +level2 created +---- diff --git a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java index f3c403e301e..5816a06a85d 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java @@ -17,6 +17,7 @@ import java.util.concurrent.CompletionStage; +import io.helidon.docs.se.inject.Qualifier2Example.Blue; import io.helidon.service.registry.Event; import io.helidon.service.registry.Service; @@ -78,7 +79,7 @@ void event(MyEvent event) { // tag::snippet_6[] @Service.Singleton - record MyIdProducer(@Service.Named("id") Event.Emitter emitter) { + record MyBlueProducer(@Blue Event.Emitter emitter) { void emit(String msg) { emitter.emit(msg); @@ -88,10 +89,10 @@ void emit(String msg) { // tag::snippet_7[] @Service.Singleton - class MyIdObserver { + class MyBlueObserver { @Event.Observer - @Service.Named("id") + @Blue void event(MyEvent event) { //Do something with the event } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java index 8b19f0be4c3..496e8b266b5 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/FactoryExample.java @@ -79,12 +79,11 @@ class SystemPropertyFactory implements Service.QualifiedFactory> first(Qualifier qualifier, - Lookup lookup, - GenericType genericType) { - String propertyValue = qualifier.stringValue() + Lookup lookup, + GenericType genericType) { + return qualifier.stringValue() .map(System::getProperty) - .orElse(""); - return Optional.of(Service.QualifiedInstance.create(propertyValue, qualifier)); + .map(propertyValue -> Service.QualifiedInstance.create(propertyValue, qualifier)); } } @@ -92,12 +91,27 @@ public Optional> first(Qualifier qualifier, // tag::snippet_4[] @Service.Singleton - @Service.Named("name") - class InjectionPointFactoryWithQualifier implements Service.InjectionPointFactory { + class TestClass { + + private final System.Logger logger; + + TestClass(System.Logger logger) { + this.logger = logger; + } + + } + + @Service.Singleton + class LoggerFactory implements Service.InjectionPointFactory { + private static final System.Logger DEFAULT_LOGGER = System.getLogger(LoggerFactory.class.getName()); @Override - public Optional> first(Lookup lookup) { - return Optional.of(Service.QualifiedInstance.create(new MyService(), Qualifier.createNamed("name"))); + public Optional> first(Lookup lookup) { + System.Logger logger = lookup.dependency() + .map(dep -> System.getLogger(dep.service().fqName())) + .orElse(DEFAULT_LOGGER); + + return Optional.of(Service.QualifiedInstance.create(logger)); } } // end::snippet_4[] diff --git a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java index 84acd9394d7..ce0ce96ee49 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/InterceptorDelegateExample.java @@ -69,18 +69,19 @@ String sayHello(String name) { } // end::snippet_3[] - - // tag::snippet_4[] /** * Assume this is the class we have no control over. */ class SomeExternalClass { + @Traced String sayHello(String name) { return "Hello %s!".formatted(name); } } + // end::snippet_4[] + // tag::snippet_5[] @Service.Singleton @Interception.ExternalDelegate(SomeExternalClass.class) class SomeExternalClassProvider implements Supplier { @@ -89,6 +90,6 @@ public SomeExternalClass get() { return new SomeExternalClass(); } } - // end::snippet_4[] + // end::snippet_5[] } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java b/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java new file mode 100644 index 00000000000..d0d25a41a03 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java @@ -0,0 +1,39 @@ +package io.helidon.docs.se.inject; + +import io.helidon.service.registry.Service; +import io.helidon.service.registry.ServiceRegistryManager; + +/** + * A example that illustrates {@link Service.RunLevel} + */ +class RunLevelExample { + + // tag::snippet_1[] + @Service.RunLevel(1) + @Service.Singleton + class Level1 { + + @Service.PostConstruct + void onCreate() { + System.out.println("level1 created"); + } + } + + @Service.RunLevel(2) + @Service.Singleton + class Level2 { + + @Service.PostConstruct + void onCreate() { + System.out.println("level2 created"); + } + } + // end::snippet_1[] + + // tag::snippet_2[] + public static void main(String[] args) { + ServiceRegistryManager registryManager = ServiceRegistryManager.start(); + registryManager.shutdown(); + } + // end::snippet_2[] +} From bc0562b16a218e5a7808509ff39da73237737ba5 Mon Sep 17 00:00:00 2001 From: David Kral Date: Wed, 26 Feb 2025 17:56:25 +0100 Subject: [PATCH 15/18] The first batch of changes added Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 29 ++++++++++++++++--- .../helidon/docs/se/inject/BasicExample.java | 7 ++--- .../docs/se/inject/QualifierExample.java | 6 ++++ .../docs/se/inject/RunLevelExample.java | 17 ++++++++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 47d9fe2efcc..8c5197ad616 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -261,7 +261,7 @@ include::{sourcedir}/se/inject/BasicExample.java[tag=snippet_2, indent=0] Now it just needs to be tested. The easiest way is to make a main method. The following piece of code initializes Service registry. After that we search for our `GreetingInjectionService` and execute it -to print out `Hello David!`. To find out more about this manual approach, please take a look into the <> chapter. +to print out `Hello David!`. To find out more about this manual approach, please take a look into the <> chapter. [source,java] .Lookup our created service and execute it manually ---- @@ -324,7 +324,8 @@ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_3, indent=0] ---- === Named by the type -Alternatively, instead of using string-based names for services, a specific class can be used to "name" them. For this purpose, we use the @Service.NamedByType annotation. +Alternatively, instead of using string-based names for services, a specific class can be used to "name" them. For this purpose, we use the +link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] annotation. [source,java] .Named by type usage example @@ -341,6 +342,21 @@ link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[ include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_5, indent=0] ---- +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] +and link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] +are even interchangeable. +So it is possible to use +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] +annotation with fully qualified class name of a class we used before. +Let's assume for this example, that our previously used class `Green` was in the `my.test` package. +Now we can specify it as any other `String` name. + +[source,java] +.Named injection point +---- +include::{sourcedir}/se/inject/QualifierExample.java[tag=snippet_6, indent=0] +---- + === Custom qualifiers To make custom qualifiers, it is necessary to "meta-annotated" it with link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. @@ -386,6 +402,7 @@ However, this approach only works if the contract is an interface and we’re im - The instance needs to be provided by an external source. - The contract is not an interface. +- The contract is a sealed interface. - The instance may not always be created (e.g., it is optional). These challenges can be addressed by implementing one of the factory interfaces supported by the Helidon Service Registry: @@ -405,7 +422,7 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_1, indent=0] === ServicesFactory The link:{service-registry-base-url}/io/helidon/service/registry/Service.ServicesFactory.html[`ServicesFactory`] -should be used only for its intended purpose — creating zero OR n instances at runtime, each assigned different qualifiers. +should be used only for its intended purpose — creating zero to n instances at runtime, each assigned different qualifiers. This allows for dynamic and flexible service creation while ensuring proper differentiation between instances based on their qualifiers. @@ -421,6 +438,9 @@ It will get executed only for injection points that are annotated with this selected qualifier and are intended for a selected contract. It will be ignored for any other injection points. +However, there is one special case. +If selected contract is `java.lang.Object`, the factory will be used for any contract, as long as the qualifier matches. + It receives: - link:{service-registry-base-url}/io/helidon/service/registry/Qualifier.html[`Qualifier`] @@ -534,7 +554,8 @@ While it is not required on interfaces, it will still work correctly if applied If you need to enable interception for classes using delegation, you should make sure about the following: -- The class must have accessible no-arg constructor (at least package local) and mustn’t be final +- The class must have accessible no-arg constructor (at least package local) +- The class must must be extensible (not final) - The constructor should have no side effects, as the instance will act only as a wrapper for the delegate - All invoked methods must be accessible (at least package local) and non-final diff --git a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java index e86735cef86..e23055567dd 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/BasicExample.java @@ -17,6 +17,7 @@ import io.helidon.service.registry.Service; import io.helidon.service.registry.ServiceRegistryManager; +import io.helidon.service.registry.Services; class BasicExample { @@ -50,12 +51,8 @@ void printGreeting(String name) { // tag::snippet_3[] public static void main(String[] args) { - var serviceRegistryManager = ServiceRegistryManager.create(); - var registry = serviceRegistryManager.registry(); - var greetings = registry.get(GreetingInjectionService.class); + var greetings = Services.get(GreetingInjectionService.class); greetings.printGreeting("David"); - - serviceRegistryManager.shutdown(); //Once not needed, it should be shut down } // end::snippet_3[] diff --git a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java index e012e4d6c19..cecd1418909 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/QualifierExample.java @@ -76,6 +76,12 @@ record GreenCircleType(@Service.NamedByType(Green.class) Color color) { } // end::snippet_5[] + // tag::snippet_6[] + @Service.Singleton + record GreenCircleStringType(@Service.Named("my.test.Green") Color color) { + } + // end::snippet_6[] + } diff --git a/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java b/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java index d0d25a41a03..0b078a7ab10 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/RunLevelExample.java @@ -1,10 +1,25 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.helidon.docs.se.inject; import io.helidon.service.registry.Service; import io.helidon.service.registry.ServiceRegistryManager; /** - * A example that illustrates {@link Service.RunLevel} + * An example that illustrates {@link Service.RunLevel} */ class RunLevelExample { From 9a491302b1a5527002afc2a4b0697fd08c687333 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 27 Feb 2025 11:00:08 +0100 Subject: [PATCH 16/18] Second batch of changes Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 30 +++++++++++-------- .../helidon/docs/se/inject/EventsExample.java | 8 ++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 8c5197ad616..9e200f6785e 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -555,12 +555,10 @@ While it is not required on interfaces, it will still work correctly if applied If you need to enable interception for classes using delegation, you should make sure about the following: - The class must have accessible no-arg constructor (at least package local) -- The class must must be extensible (not final) +- The class must be extensible (not final) - The constructor should have no side effects, as the instance will act only as a wrapper for the delegate - All invoked methods must be accessible (at least package local) and non-final -Therefore, using link:{service-registry-base-url}/io/helidon/service/registry/Interception.Delegate.html[`@Interception.Delegate`] -on the classes can be considered as acknowledgement of the rules above. [source,java] .Delegate used on the class @@ -611,12 +609,12 @@ A single event can be delivered to zero or more consumers. Key Terminology: -- *Event Object* – Any object that is sent as an event. -- *Event Emitter* – A service responsible for emitting events into the event system. Emitters are always generated. -- *Event Producer* – A service that triggers an event by calling an emitter. -- *Event Observer* – A service that listens for events, with a method annotated using +- *<>* – Any object that is sent as an event. +- *<>* – A service responsible for emitting events into the event system. Emitters are always generated. +- *<>* – A service that triggers an event by calling an emitter. +- *<>* – A service that listens for events, with a method annotated using link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. -- *Qualified Event* – An event emitted with a qualifier, using an annotation marked with +- *<>* – An event emitted with a qualifier, using an annotation marked with link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. === Event Object @@ -750,6 +748,11 @@ link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.htm link:{service-registry-base-url}/io/helidon/service/registry/GlobalServiceRegistry.html[`GlobalServiceRegistry`] ** This shouldn’t be used from the factories or services. Intended use is in the `main` methods or when one needs static access. +** It is also possible to access global service registry via +link:{service-registry-base-url}/io/helidon/service/registry/Services.html[`Services`] +static methods. This is the shortcut for accessing the services from the global service registry. +** In most of the cases, the global service registry is enough (and expected to be used) for a single +applications. A custom service registry should only be created for specific use cases. * Create a new link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance and search for the desired service. @@ -757,14 +760,14 @@ instance and search for the desired service. link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance which is used to create the service instance. -To create a registry instance: +To create a custom registry and manager instance: [source,java] ---- include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] ---- -Keep in mind, that +Keep in mind, that custom link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`] needs to be shut down, once it is not needed. [source,java] @@ -841,8 +844,11 @@ method `shutdown` on the manager must be called to ensure proper termination of === RunLevel link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] -is the annotation used for specifying the order in which certain services should be created. +is the annotation used for specifying the steps in which certain services should be started. It always starts from the lowest number to the highest. +It is possible to zero to n services in each run level. +When more than one service is defined to a specific run level, the order of creation is defined by +link:{common-javadoc-base-url}/io/helidon/common/Weight.html[`@Weight`]. To start, add the link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] @@ -853,7 +859,7 @@ on the service. include::{sourcedir}/se/inject/RunLevelExample.java[tag=snippet_1, indent=0] ---- -For better understanding, we can also add helpful method, which get executed upon service construction. +For better understanding, we can also add helpful method, which get executed post service construction. The easiest way for us to use these annotations, is to use `start` method on the link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`]. diff --git a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java index 5816a06a85d..e00ca74de7e 100644 --- a/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java +++ b/docs/src/main/java/io/helidon/docs/se/inject/EventsExample.java @@ -36,7 +36,7 @@ record MyEvent(String msg) { @Service.Singleton record MyEventProducer(Event.Emitter emitter) { - void emit(String msg) { + void produce(String msg) { emitter.emit(new MyEvent(msg)); } } @@ -57,9 +57,9 @@ void event(MyEvent event) { // tag::snippet_4[] @Service.Singleton - record MyAsyncEmitter(Event.Emitter emitter) { + record MyAsyncProducer(Event.Emitter emitter) { - void emit(String msg) { + void produce(String msg) { CompletionStage completionStage = emitter.emitAsync(new MyEvent(msg)); //Do something with the completion stage } @@ -81,7 +81,7 @@ void event(MyEvent event) { @Service.Singleton record MyBlueProducer(@Blue Event.Emitter emitter) { - void emit(String msg) { + void produce(String msg) { emitter.emit(msg); } } From d53cb2a3a2cf32bf1b9071fc7b8eeb7c90ae7888 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 27 Feb 2025 16:10:14 +0100 Subject: [PATCH 17/18] another set of changes Signed-off-by: David Kral --- docs/src/main/asciidoc/se/injection.adoc | 45 ++++++++++-------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index 9e200f6785e..cfefc0f45f0 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -407,13 +407,13 @@ However, this approach only works if the contract is an interface and we’re im These challenges can be addressed by implementing one of the factory interfaces supported by the Helidon Service Registry: -- <> -- <> -- <> -- <> +- <> +- <> +- <> +- <> === Supplier -A factory that supplies a single instance (it can also return `Supplier>` or `Optional`) +A factory that supplies a single instance (it can also return `Optional`) [source,java] .Supplier factory ---- @@ -422,7 +422,7 @@ include::{sourcedir}/se/inject/FactoryExample.java[tag=snippet_1, indent=0] === ServicesFactory The link:{service-registry-base-url}/io/helidon/service/registry/Service.ServicesFactory.html[`ServicesFactory`] -should be used only for its intended purpose — creating zero to n instances at runtime, each assigned different qualifiers. +should be used to create zero to n instances at runtime, each assigned different qualifiers if needed. This allows for dynamic and flexible service creation while ensuring proper differentiation between instances based on their qualifiers. @@ -439,15 +439,14 @@ that are annotated with this selected qualifier and are intended for a selected It will be ignored for any other injection points. However, there is one special case. -If selected contract is `java.lang.Object`, the factory will be used for any contract, as long as the qualifier matches. +If the selected contract is `java.lang.Object`, the factory will be used for any contract, as long as the qualifier matches. It receives: -- link:{service-registry-base-url}/io/helidon/service/registry/Qualifier.html[`Qualifier`] -instance (metadata of the qualifier that is on the annotated injection point, with each annotation property available) -- link:{service-registry-base-url}/io/helidon/service/registry/Lookup.html[`Lookup`] instance -- link:{common-javadoc-base-url}/io/helidon/common/GenericType.html[`GenericType`] instance of the injection point -(contains information about the injection point type) +- link:{service-registry-base-url}/io/helidon/service/registry/Qualifier.html[`Qualifier`] - +metadata of the qualifier that is on the annotated injection point, with each annotation property available +- link:{service-registry-base-url}/io/helidon/service/registry/Lookup.html[`Lookup`] - information about injection request +- link:{common-javadoc-base-url}/io/helidon/common/GenericType.html[`GenericType`] - injection point type This allows the factory to create instances dynamically based on the injection point information. @@ -610,7 +609,7 @@ A single event can be delivered to zero or more consumers. Key Terminology: - *<>* – Any object that is sent as an event. -- *<>* – A service responsible for emitting events into the event system. Emitters are always generated. +- *<>* – Helidon generated service responsible for emitting events into the event system. - *<>* – A service that triggers an event by calling an emitter. - *<>* – A service that listens for events, with a method annotated using link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. @@ -627,7 +626,7 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_1, indent=0] ---- === Event Emitter -Event emitters are code generated by Helidon. The emitter is generated when an injection point is discovered that expects it. +Event emitters are code generated by Helidon when an injection point is discovered that expects it. All the emitters are generated as implementations of the link:{service-registry-base-url}/io/helidon/service/registry/Event.Emitter.html[`Event.Emitter`] interface. @@ -734,12 +733,6 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_5, indent=0] ---- == Programmatic Lookup - -There are two primary ways to access services from the service registry: - -- *Dependency Injection* – Services are injected automatically at defined injection points. -- *Programmatic Lookup* – Services are retrieved manually by accessing the service registry directly. - If you want to use programmatic lookup, there are several ways how to get a link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance: @@ -769,7 +762,7 @@ include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent Keep in mind, that custom link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`] -needs to be shut down, once it is not needed. +must be shut down, once it is not needed. [source,java] ---- include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_2, indent=0] @@ -830,13 +823,11 @@ When created programmatically, two possible methods can be chosen. - `create` - Creates a new link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance, -but does not populate it with any services. Service registry is only populated when asked for a specific contract. +but does not create any service instance. Service instances are created only when needed. - `start` - Creates a new link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance and -creates all the available services. -It also properly executes any available -link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] annotations -present on the service. +creates all services annotated with +link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`]. See <> chapter. It is important to note, that once you don’t need your service registry, @@ -846,7 +837,7 @@ method `shutdown` on the manager must be called to ensure proper termination of link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] is the annotation used for specifying the steps in which certain services should be started. It always starts from the lowest number to the highest. -It is possible to zero to n services in each run level. +It is possible to have one or more services in each run level. When more than one service is defined to a specific run level, the order of creation is defined by link:{common-javadoc-base-url}/io/helidon/common/Weight.html[`@Weight`]. From 9a8ad01f12473ba886e25e4777f4fcc0b38c439c Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 27 Feb 2025 18:40:29 +0100 Subject: [PATCH 18/18] another set of changes Signed-off-by: David Kral --- .../main/asciidoc/includes/attributes.adoc | 1 + docs/src/main/asciidoc/se/injection.adoc | 119 +++++++++++------- docs/src/main/asciidoc/se/introduction.adoc | 2 +- docs/src/main/asciidoc/sitegen.yaml | 6 + 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/docs/src/main/asciidoc/includes/attributes.adoc b/docs/src/main/asciidoc/includes/attributes.adoc index 64cac76c7c0..39f8009a142 100644 --- a/docs/src/main/asciidoc/includes/attributes.adoc +++ b/docs/src/main/asciidoc/includes/attributes.adoc @@ -233,6 +233,7 @@ endif::[] :telemetry-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.telemetry :tracing-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing :tracing-otel-provider-javadoc-base-url: {javadoc-base-url}/io.helidon.tracing.providers.opentelemetry +:types-javadoc-base-url: {javadoc-base-url}/io.helidon.common.types :webclient-javadoc-base-url: {javadoc-base-url}/io.helidon.webclient :webserver-javadoc-base-url: {javadoc-base-url}/io.helidon.webserver diff --git a/docs/src/main/asciidoc/se/injection.adoc b/docs/src/main/asciidoc/se/injection.adoc index cfefc0f45f0..9f83a68fc6d 100644 --- a/docs/src/main/asciidoc/se/injection.adoc +++ b/docs/src/main/asciidoc/se/injection.adoc @@ -204,6 +204,13 @@ The definition of a "request" is not enforced by the injection framework but aligns with concepts like an HTTP request-response cycle or message consumption in a messaging system. == Build time +Helidon injection is compile/build time based injection. +This provides a significant performance boost since it eliminates the need for reflection or +dynamic proxying at runtime, resulting in faster startup. +Additionally, it integrates well with Native Image, making it an efficient choice for high-performance applications. + +Because of that it needs to generate all the needed classes at compile time, so it minimizes the need +of runtime processing. To ensure everything works correctly, you need to add the following annotation processors to your application's compilation process. @@ -300,10 +307,18 @@ Uses a `String` name to qualify a service. Works the same way as `@Service.Named` but uses a class type instead. The name that would be used is the fully qualified name of the type. +Both +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] +and +link:{service-registry-base-url}/io/helidon/service/registry/Service.NamedByType.html[`@Service.NamedByType`] +are interchangeable, so one can combine them. +To see an example of this see <> chapter. + === Named service injection Services can be assigned names, allowing them to be specified by name during injection. -This ensures that the correct service is injected. To achieve this, we use the @Service.Named annotation. +This ensures that the correct service is injected. To achieve this, we use the +link:{service-registry-base-url}/io/helidon/service/registry/Service.Named.html[`@Service.Named`] annotation. [source,java] .Create named services @@ -407,10 +422,10 @@ However, this approach only works if the contract is an interface and we’re im These challenges can be addressed by implementing one of the factory interfaces supported by the Helidon Service Registry: -- <> -- <> -- <> -- <> +- <> +- <> +- <> +- <> === Supplier A factory that supplies a single instance (it can also return `Optional`) @@ -608,10 +623,10 @@ A single event can be delivered to zero or more consumers. Key Terminology: -- *<>* – Any object that is sent as an event. -- *<>* – Helidon generated service responsible for emitting events into the event system. -- *<>* – A service that triggers an event by calling an emitter. -- *<>* – A service that listens for events, with a method annotated using +- *<>* – Any object that is sent as an event. +- *<>* – Helidon generated service responsible for emitting events into the event system. +- *<>* – A service that triggers an event by calling an emitter. +- *<>* – A service that listens for events, with a method annotated using link:{service-registry-base-url}/io/helidon/service/registry/Event.Observer.html[`@Event.Observer`]. - *<>* – An event emitted with a qualifier, using an annotation marked with link:{service-registry-base-url}/io/helidon/service/registry/Service.Qualifier.html[`@Service.Qualifier`]. @@ -734,41 +749,28 @@ include::{sourcedir}/se/inject/EventsExample.java[tag=snippet_5, indent=0] == Programmatic Lookup If you want to use programmatic lookup, there are several ways how to get a -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance: - -* Get the global -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] via -link:{service-registry-base-url}/io/helidon/service/registry/GlobalServiceRegistry.html[`GlobalServiceRegistry`] -** This shouldn’t be used from the factories or services. -Intended use is in the `main` methods or when one needs static access. -** It is also possible to access global service registry via -link:{service-registry-base-url}/io/helidon/service/registry/Services.html[`Services`] -static methods. This is the shortcut for accessing the services from the global service registry. -** In most of the cases, the global service registry is enough (and expected to be used) for a single -applications. A custom service registry should only be created for specific use cases. -* Create a new -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] -instance and search for the desired service. -* Inject the -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] -instance which is used to create the service instance. +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] instance. -To create a custom registry and manager instance: +We can either get the global one or create a custom one (more advanced use case). +In most of the cases, the global service registry is enough (and expected to be used) for a single +applications. +A custom service registry should only be created for specific use cases. -[source,java] ----- -include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] ----- +To get the global +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] +we need to use +link:{service-registry-base-url}/io/helidon/service/registry/GlobalServiceRegistry.html[`GlobalServiceRegistry`] +and select `registry` method. -Keep in mind, that custom -link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`] -must be shut down, once it is not needed. -[source,java] ----- -include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_2, indent=0] ----- +However, it is also possible to access global service registry via +link:{service-registry-base-url}/io/helidon/service/registry/Services.html[`Services`] +static methods. +This is the shortcut for accessing the services from the global service registry. +Global service registry shouldn’t be used from the factories or services. +Intended use is in the `main` methods or when one needs static access. -Note that all instances are created lazily, meaning the service registry does nothing by default. +Note that all instances in the registry are created lazily, +meaning the service registry does nothing by default. If a service performs any actions during construction or post-construction, you must first retrieve an instance from the registry to trigger its initialization. @@ -785,8 +787,37 @@ available Lookup parameter options: - `Class` - the contract we’re looking for -- `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments) -- `Lookup` - a full search criteria for a registry lookup +- link:{types-javadoc-base-url}/io/helidon/common/types/TypeName.html[`TypeName`] - +the same, but using Helidon abstraction of type names (may have type arguments) +- link:{service-registry-base-url}/io/helidon/service/registry/Lookup.html[`Lookup`] - +a full search criteria for a registry lookup + +=== Service registry injection +It is also possible to inject +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`]. +Keep in mind that when injected, the provided instance will be the same one, +which is used to create the service instance. + +=== Custom service registry +It is possible to create custom +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistry.html[`ServiceRegistry`] +instance. +Keep in mind, this whole process is advanced and should be used only when you know what you are doing. + +To create a custom registry and manager instance: + +[source,java] +---- +include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_1, indent=0] +---- + +Keep in mind, that custom +link:{service-registry-base-url}/io/helidon/service/registry/ServiceRegistryManager.html[`ServiceRegistryManager`] +must be shut down, once it is not needed. +[source,java] +---- +include::{sourcedir}/se/inject/ServiceRegistryExample.java[tag=snippet_2, indent=0] +---- == Startup @@ -805,8 +836,8 @@ annotation (i.e. `start(ApplicationBinding.create())`) if used, remember to set discovery to `false` to 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, or `ServiceRegistryManager.create(...)` methods +// Can be commented out once the PR https://github.com/helidon-io/helidon/pull/9840 is merged +// - A custom Main method using `ServiceRegistryManager.start(...)` methods, or `ServiceRegistryManager.create(...)` methods - A generated `ApplicationMain` - optional feature of the Maven plugin, requires property `generateMain` to be set to `true`. It uses link:{service-registry-base-url}/io/helidon/service/registry/Service.RunLevel.html[`@Service.RunLevel`] actively, but via code generated classes -> See <> for more information. diff --git a/docs/src/main/asciidoc/se/introduction.adoc b/docs/src/main/asciidoc/se/introduction.adoc index 87bc6ec0932..fe3c52a10ce 100644 --- a/docs/src/main/asciidoc/se/introduction.adoc +++ b/docs/src/main/asciidoc/se/introduction.adoc @@ -81,7 +81,7 @@ Expose health statuses of your applications. //Injection [CARD] .Injection -[icon=message,link=injection.adoc] +[icon=colorize,link=injection.adoc] -- Use of the Helidon injection in your applications. -- diff --git a/docs/src/main/asciidoc/sitegen.yaml b/docs/src/main/asciidoc/sitegen.yaml index ec70219ba58..0bd62b703c1 100644 --- a/docs/src/main/asciidoc/sitegen.yaml +++ b/docs/src/main/asciidoc/sitegen.yaml @@ -405,6 +405,12 @@ backend: - "oci.adoc" - "hcv.adoc" - "neo4j.adoc" + - type: "PAGE" + title: "Injection" + source: "injection.adoc" + glyph: + type: "icon" + value: "colorize" - type: "MENU" title: "Metrics" dir: "metrics"