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

4.x: Introduction of Helidon Service Inject. #9249

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

Conversation

tomas-langer
Copy link
Member

@tomas-langer tomas-langer commented Sep 12, 2024

Related to #9158 (Step 1)

Follow up steps:

  • maven plugin to generate binding and main class
  • support for Jakarta inject (both javax and jakarta packages)

Inject Service Registry

An extension to the core service registry, Helidon Service Inject adds a few concepts:

  • Injection - injection into constructor
  • Scoped service instances - Singleton scope, optional RequestScope
  • Interceptors - intercept method invocation
  • Config beans - create instances from config

The main entry point to get service registry is
io.helidon.service.inject.InjectRegistryManager (part of implementation, not API), which can be used to obtain
io.helidon.service.inject.api.InjectRegistry.
The registry can then be used to lookup services (it extends the existing ServiceRegistry).

Injection and scopes

Annotation type: io.helidon.service.inject.api.Injection

Annotations:

Annotation class Description
Inject Marks element as an injection point; although we prefer constructor injection, field and method injection works as well
Qualifier Marker for annotations that are qualifiers
Named A qualifier that provides a name
NamedByClass An equivalent of Named, that uses the fully qualified class name of the configured class as name
Scope Marker for annotations that are scopes
Instance A service that does not have a scope, yet supports injection, and can be looked up in registry
Singleton Singleton scope - a service registry will create zero or one instances of this service (instantiation is lazy)
RequestScope Request scope - a service registry will create zero or one instance of this service per request scope instance
RunLevel A "layer" in which this service should be instantiated; not executed by injection, will be used when starting application
CreateFor Create a service instance for each instance of the configured contract available in registry (usually for named)
CreateForName Parameter or field that will be injected with the name this service instance is created for (see CreateFor)
Main Marker for a custom main class, so appropriate stub can be generated during annotation processing, later replaced by Maven plugin
Describe Create a descriptor for a type that is not a service itself, but an instance would be provided at scope creation time

Interfaces:

Interface class Description
ServicesProvider A service provider that creates zero or more qualified service instances at runtime (used for example by ConfigBean
InjectionPointProvider A service provider that creates values for specific injection points
QualifiedProvider A service provider to resolve qualified injection points of any type (used for example by config value injection
QualifiedInstance Used as a return type of some of the interfaces above, not to be implemented by users
ScopeHandler Extension point to support additional scopes

Injection into services

A service can have injection points, usually through constructor.

Example:

@Injection.Inject
MyType(Contract1 contract, Supplier<Contract2> contract2, Optional<Contract3> contract3) {
    // ...
}

A dependency (such as Contract1 above) may have the following forms (Contract stands for a contract interface, or class):

Instance based:

  1. Contract - injects an instance of the contract with the highest weight from the registry
  2. Optional<Contract> - same as previous, the contract may not have an implementation available in registry
  3. List<Contract> - a list of all available instances in the registry

Supplier based (to break cyclic dependency, and to create instances as late as possible):

  1. Supplier<Contract>
  2. Supplier<Optional<Contract>>
  3. Supplier<List<Contract>>

Service instance based (to obtain registry metadata in addition to the instance):

  1. ServiceInstance<Contract>
  2. Optional<ServiceInstance<Contract>>
  3. List<ServiceInstance<Contract>>

As can be seen above, we can have more than one producer of a single contract type within the registry. The ordering is always by qualifiers first, weight second (unqualified instances are first).

Interceptors

Interception provides capability to intercept call to a constructor or a method (even to fields when used as injection points).

Interception is (by default) only enabled for elements annotated with an annotation that is a Trigger. Annotation processor
configuration allows for creating interception "plumbing" for any annotation, or to disable it altogether.

Interception works "around" the invocation, so it can:

  • do something before actual invocation
  • modify invocation parameters
  • do something after actual invocation
  • modify response
  • handle exceptions

Annotation type: io.helidon.service.inject.api.Interception

Annotations:

Annotation class Description
Trigger Marker for annotations that should trigger interception

Interfaces:

Interface class Description
Interceptor A service implementing this interface, and named with the annotation type (maybe using NamedByClass) will be used as interceptor of methods annotated with that annotation. Interceptor must call proceed method to handle the interception chain
Factory Used to create intercepted instances using delegation, rather than extension (used for example to intercept ConfigBean methods

Config beans

Config beans are types that are driven by a configuration key. They must use root configuration key (such as server, security).
Config beans may be repeatable, or single instance (see details below in Annotations section).

Annotation type: io.helidon.service.inject.api.ConfigDriven

Annotations:

Annotation class Description
ConfigBean Marks a type as a config bean. In Helidon, we expect this to be used on @Prototype.Blueprint types
AtLeastOne At least one instance must be configured in configuration
Repeatable Zero or more instances may be configured in configuration
AddDefault If @default (or unnamed) instance is not in configuration, create it using defaults
OrDefault Either there is a configured instance, or a default is created (can only be combined with ConfigBean

In case OrDefault is used, the config bean is a singleton.
In all other cases, each config bean created is a named instance, where the name is either @default, or name key provided
in its configuration.

Config beans work in two steps (this is only true for @Prototype.Blueprint beans):

  1. A Prototype.Builder instance is created, which can be updated by other services
  2. A Prototype instance is created that injects the Prototype.Builder

Startup

Helidon Service Inject adds a new startup provider - so if you use io.helidon.Main as your main class, service registry
would be automatically started as long as it is on your classpath, and services in all run levels that are present will be
looked up (triggering @PostConstruct calls, which can be used to start the "container").

@tomas-langer tomas-langer added 4.x Version 4.x declarative Helidon Declarative labels Sep 12, 2024
@tomas-langer tomas-langer added this to the 4.2.0 milestone Sep 12, 2024
@tomas-langer tomas-langer self-assigned this Sep 12, 2024
@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Sep 12, 2024
@tomas-langer
Copy link
Member Author

I have done a small update to include inject as a Preview feature.
This required a change in our annotation processor for codegen, as it always returned true (meaning it consumed all annotations), which prevented other annotation processors to run.
Now this works as it should, so we do not need to change order of processors.

spericas
spericas previously approved these changes Sep 23, 2024
Copy link
Member

@spericas spericas left a comment

Choose a reason for hiding this comment

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

LVGTM

@ljnelson
Copy link
Member

Dmitry asked me to review. I mostly just had some questions and things to think about. I see there's an (enthusiastic) approval already so feel free to merge whenever you like; I don't want to get in the way.

  1. Is there a specification? Or is the sketch above what there is?
  2. (Related, I guess.) Suppose I have a service (an implementation of one or more contracts, I guess?) and now I inject an instance of it somewhere. Can the injection point specify a supertype such that the (subtype) service instance will be "found" and injected (if I call for a CharSequence and all there is is String, will that work?)? Or must the type matching be exact? (If the type matching can be anything other than exact, where can the rules for such matching be found?) Do wildcards work?
  3. Given a service with multiple constructors but no @Injection.Inject annotations, what happens? Presumably failure?
  4. There are a lot of concepts here (services, service instances, service providers, beans (implied), config beans (a special kind of bean, maybe? are there other kinds of beans?), contracts, interceptors, and so on). To ask one question in this general area: what makes a "bean" different from a service? To my naïve eyes it looks like many of these concepts can be reduced into one another.
  5. Is there something special about an object sourced from configuration that makes it its own special thing (I mean, other than the configuration itself of course)? Maybe they're just services, or whatever your preferred "primordial" concept is?
  6. Is constructor interception supported?
  7. DI systems including CDI, Spring, Avaje inject, HK2 and so on tend to fail early on purpose when two or more potential injectees can satisfy an injection point, normally taking the deliberate stance that the user must "opt-in" and indicate (via @Alternative, @Secondary and so on, together with the usual priority mechanisms) that she knows there are two or more, ordered or not, and she explicitly wants a specific one (HK2 is a little more weird here, with its notion of just-in-time resolvers and general downplaying of immutability of the service registry). From the textual outline above, it looks like Helidon Inject just grabs the "heaviest" (assuming it's the right qualified type of course), perhaps warning that many choices existed at the point of injection and one was picked. Was that a deliberate choice?
  8. Are parameterized types supported? On both the injection point and the services? If so, is there a description of how they work? As you know this can be very tricky, especially when scopes get involved.
  9. Harvesting/cleaning up injections is tricky. Does Helidon Inject do anything about it? Or does it punt to the class hosting the injection (via @PreDestroy or similar)?
  10. Are events supported, as in CDI, HK2, Spring and Avaje inject? Or is run level it?
  11. One of the holy wars that was fought when JSR-330 was being dragged unwillingly into its miserable existence was whether the "default" scope should be singleton-flavored, or no-scope-at-all-or-dependent-or-per-lookup-flavored. There's not a "right" answer. Which one did you pick?

Clearly a lot of work went into all this; hope this is useful.

@tomas-langer
Copy link
Member Author

@ljnelson:

  1. There is no formal specification, just readmes and javadoc
  2. Each service implements contracts, that are discovered; the default is to only discover contracts annotated by @Service.Contract or @Service.ExternalContracts (if we do not have the source code); the service then satisfies injection points of any of such contract; in addition you can control the annotation processor to add contracts for all implemented interfaces
  3. This should fail the build
  4. All are services; config beans is just a fancy name, because it has its own code generator and qualifier
  5. See previous answer...
  6. Yes, see test in service/tests/inject/interception/src/main/java/io/helidon/service/tests/inject/interception/TheService.java
  7. There was a deliberate choice to support multiple provides of the same contract using the heaviest one (or all if a List is injected). We are missing a tool for testing that would replace all others (enhancement?)
  8. Parameterized types are not supported, except for the ones described in the docs - List, Optional, Supplier etc., and they have prescribed meaning (i.e. a service cannot implement a supplier of list of something)
  9. We support @Service.PreDestroy, but only for scoped services (Singleton, RequestScope)
  10. We have events during code generation, not at runtime. RunLevel is the only vehicle right now (enhancement?)
  11. There is no default scope in Inject - you must annotate a type with one of the annotations that imply scope. For core service registry (Service.Provider) - default is singleton behavior, in case the service implements supplier, the supplier is called for each lookup

I will create follow up PRs that support javax and jakarta inject, passing the JSR-330 TCK...

Copy link
Contributor

@romain-grecourt romain-grecourt left a comment

Choose a reason for hiding this comment

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

Simple comments, aside from the offline review.

doTestExitOnStarted();
}

// @Test
Copy link
Contributor

Choose a reason for hiding this comment

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

Test is commented-out here but not in other variants.

<groupId>io.helidon.build-tools</groupId>
<artifactId>helidon-maven-plugin</artifactId>
<configuration>
<defaultJvmOptions>-Dapp.static.path=../../web</defaultJvmOptions>
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not used.

Signed-off-by: Tomas Langer <[email protected]>
Proper qualifier for config beans even when `OrDefault` is used
Added test for `OrDefault`

Signed-off-by: Tomas Langer <[email protected]>
…annotations.

Added feature processor and metadata codegen to Helidon Service Registry and Inject.

Signed-off-by: Tomas Langer <[email protected]>
Add support for injection of InterceptionMetadata.
Rework delegate interception to be usable by users.
…f possible.

Changed default of Injection.Described to be singleton, as that feels more natural.
…ltiple constructor injection

A few bugfixes for problems discovered by tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
4.x Version 4.x declarative Helidon Declarative OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants