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

Merged
merged 59 commits into from
Oct 31, 2024

Conversation

tomas-langer
Copy link
Member

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

Related to #9158 (Step 1)

Follow up steps:

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

Updated description (matches current PR state):

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 PerRequest scope and possible custom scopes
  • Interceptors - intercept method invocation

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
NamedByType An equivalent of Named, that uses the fully qualified class name of the configured class as name
Scope Marker for annotations that are scopes
PerLookup Service instance is created per lookup (either for injection point, or via registry lookup)
Singleton Singleton scope - a service registry will create zero or one instances of this service (instantiation is lazy)
PerRequest Request scope - a service registry will create zero or one instance of this service per request scope instance
RunLevel A "layer" in which this service should be instantiated; not executed by injection, will be used when starting application
PerInstance Create a service instance for each instance of the configured contract available in registry (usually for named)
InstanceName Parameter or field that will be injected with the name this service instance is created for (see PerInstance)
Describe Create a descriptor for a type that is not a service itself, but an instance would be provided at scope creation time

Interfaces:

Interface class Description
ServicesProvider A service provider that creates zero or more qualified service instances at runtime
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>>

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 Interception.Intercepted.
Annotation processor configuration allows for creating interception "plumbing" for any annotation, or to disable it altogether.

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

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

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

Annotations:

Annotation class Description
Intercepted Marker for annotations that should trigger interception
Delegate Marks a class as supporting interception delegation. Classes are not good candidates for delegation, as you need to create an instance that delegates to another instance, opening space for side-effects. To use a class, it must have an accessible no-arg constructor, and it should be designed not to have side-effects from construction
ExternalDelegate Add this to a service provider that provides a class that requires delegation, if the class is not part of your current project (i.e. you cannot annotate it with Delegate

Interfaces:

Interface class Description
Interceptor A service implementing this interface, and named with the annotation type (maybe using NamedByType) will be used as interceptor of methods annotated with that annotation. Interceptor must call proceed method to handle the interception chain

@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.

@tomas-langer
Copy link
Member Author

Added support for lookup tracing. Add the following to logging.properties:

io.helidon.service.inject.LookupTrace.level=ALL

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
…e for core registry)

Introduction of ProviderType in descriptor and lookup, to distinguish contracts from provider handling.
InterceptionMetadata moved to top level class.
Ensure binding at startup (to discover unsatisfied injection points).
- introduce inject codegen module
- analysis/codegen separation

Renaming of provider to Factory
Fix of inject packaging test - using the correct annotation processor.
@tomas-langer
Copy link
Member Author

Rebased on latest main

@tomas-langer tomas-langer merged commit b38a304 into helidon-io:main Oct 31, 2024
46 checks passed
@tomas-langer tomas-langer deleted the 9158-inject-1 branch October 31, 2024 16:56
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