Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helidon Inject documentation #9742

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

Verdent
Copy link
Member

@Verdent Verdent commented Feb 6, 2025

Description

Fixes #9741

Documentation

Adds user documentation for Helidon Inject

@Verdent Verdent self-assigned this Feb 6, 2025
@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Feb 6, 2025
Signed-off-by: David Kral <[email protected]>
Signed-off-by: David Kral <[email protected]>
Signed-off-by: David Kral <[email protected]>
Signed-off-by: David Kral <[email protected]>
Signed-off-by: David Kral <[email protected]>
== Overview
Injection is the basic building stone for inversion of control. Dependency injection provides a mechanism to get
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend we start with the basics of Injection and IoC. SE developers, prior to 4.2 would be focused only on procedural coding thus a very brief introduction to what is Injection, what IoC is, why use Injection; why consider adopting IoC will help users. Include why we didn't just adopt CDI (light-weight, smaller, faster, easier to understand, ...) I also think we should describe the goals achieved with Helidon Inject (How/Why is Helidon Inject different from other Injection frameworks that a reader might have used/heard about. Some potential differentiators: -- compile-time generation (immutable images, more secure)-- source-code, not byte-code (easier debugging/stack traces) ... (add more beneficial facets as you think is appropriate).
Give the reader a bit of a primer on Inject, IoC, and set up reason to read further.

@tomas-langer tomas-langer self-requested a review February 18, 2025 10:09
Copy link
Member

@tomas-langer tomas-langer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of examples (injection.adoc will come later).


// tag::snippet_6[]
@Service.Singleton
record MyIdProducer(@Service.Named("id") Event.Emitter<String> emitter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use custom qualifiers instead of a named qualifier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I want this to make sense, I would need to paste custom qualifier creation there as well. What I am trying to do, is to examplain the emitter with qualifier. Since we do provider named one out of the box, it does not need to be made and this is te simplest example from my point of view.

class InjectionPointFactory implements Service.InjectionPointFactory<MyService> {

@Override
public Optional<Service.QualifiedInstance<MyService>> first(Lookup lookup) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is just showing that you can implement a method of an interface, not how to query the lookup, which is the reason this factory exists.


// tag::snippet_6[]
@Service.Singleton
@Service.Named("name")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, using a name qualifier does not work nicely in this example.

----

== Usage
To start using Helidon Inject, you need to create both:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not really called Helidon Inject anywhere right now. Not sure if that is what we want to use

@Service.Singleton
class MyGreetingService implements GreetingContract {

public String greet(String name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @Override

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message seems to be missing something. Weight is suddenly introduced without a link to explanation (or inlined explanation).
Also picked is not very deterministic here.
This is the definition:

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 `@Weight`, `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.

Weight, Weighted - this should have proper links

Dependencies can be injected in different formats, depending on the required behavior:

- `Contract` - Retrieves an instance of another service.
- `Optional<Contract>` - Retrieves an instance, but if the service is unavailable, an empty optional is provided.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"but if the service is unavailable" - if there is no service providing the contract

The injection point asks for a contract, not for a service.

- `Contract` - Retrieves an instance of another service.
- `Optional<Contract>` - Retrieves an instance, but if the service is unavailable, an empty optional is provided.
- `List<Contract>` - Retrieves all available instances of a given service.
- `Supplier<Contract>`, `Supplier<Optional<Contract>>`, `Supplier<List<Contract>>` - Similar to the above, but the value is only resolved when get() is called, allowing lazy evaluation for more control.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not "for more control", but to resolve:

  • cyclic dependencies
  • a "storm" of initializations when a service is looked up from the registry

When suppliers are not used, all instances MUST be created when the service instance is created (during construction).

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only be used for advanced use cases, as it bypasses a lot of optimizations of the service registry (i.e. build time binding of services to injection points when using the Maven plugin)

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are advanced methods, which we should not even mention in documentation. Just document the methods that operate on contracts.

- `TypeName` - the same, but using Helidon abstraction of type names (may have type arguments)
- `Lookup` - a full search criteria for a registry lookup

== Startup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a bit unfinished, as we need explanation of RunLevel and how it is used in generated main class, and in io.helidon.Main.

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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also mention it uses RunLevel actively, but via code generated class.
This is the only approach that is fully reflection free and skips lookups for injection points.

== 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplicationBinding provides build time bindings of service providers to injection points - this is to avoid lookup at runtime.
It does NOT elimiminate classpath discovery and reflection. To avoid that you need to use the generated ApplicationMain class.

Also classpath discovery does not mean classpath scanning (it sounds a bit bad in here) - we lookup all service.registry and service.loader resources from the classpath - we are not scanning for them (i.e. using only Java features that do not depend on how the classpath is created, so it works in native image as well)

Not sure how much of this should be in the docs, but it seems a bit misleading right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Helidon Inject documentation
3 participants