diff --git a/README.markdown b/README.markdown index ede22446..7cc8c440 100644 --- a/README.markdown +++ b/README.markdown @@ -1,404 +1,535 @@ -Dirk: Dynamic Dependency Injection Framework -============================================ +# Dirk DI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.hjohn.ddif/ddif-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.hjohn.ddif/ddif-core) [![Build Status](https://github.com/hjohn/hs.ddif/actions/workflows/maven.yml/badge.svg?branch=master)](https://github.com/hjohn/hs.ddif/actions) [![Coverage](https://codecov.io/gh/hjohn/hs.ddif/branch/master/graph/badge.svg?token=QCNNRFYF98)](https://codecov.io/gh/hjohn/hs.ddif) [![License](https://img.shields.io/badge/License-BSD_2--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause) +[![javadoc](https://javadoc.io/badge2/com.github.hjohn.ddif/ddif-core/javadoc.svg)](https://javadoc.io/doc/com.github.hjohn.ddif/ddif-core) -A light-weight framework that allows you to use standard JSR-330 javax.inject -Annotations to create instances of objects, even when they're dynamically loaded at -runtime. This framework will allow you to package your classes in separate JAR's, -load them at runtime, and have them injected with dependencies or serve as dependencies -for other classes. +Dirk is a small, highly customizable, dynamic dependency injection framework. -For example given a class implementing the `PaymentProcessor` interface: +## Quick Start - public class CreditCardPaymentProcessor implements PaymentProcessor { - } +Add a dependency to Dirk in your POM: -The below class would get its `paymentProvider` field injected with an instance of -the above class: + org.int4.dirk + dirk-di + 1.0.0 - public class BookShop { - @Inject - private PaymentProcessor paymentProcessor; +Assume there is a small class which depends on a `String` which Dirk should provide: + + public class Greeter { + @Inject private String greeting; + + public void greet() { + System.out.println(greeting); + } } -This framework differs from most standard DI frameworks in that dependencies can be -added and removed at runtime. The used Injector will make sure that dependencies can -be satisfied and will refuse to add or remove classes when it would result in broken -dependencies. +Create an injector: + + Injector injector = Injectors.autoDiscovering(); + +Register a `String` and the `Greeter` class: + + injector.registerInstance("Hello World"); + injector.register(Greeter.class); -For example, if a class registered with an Injector instance needs a `PaymentProvider`, -then attempting to remove the only matching `PaymentProvider` from the Injector will -fail. Likewise, registering a class that needs a `PaymentProvider` would fail when no -class is registered that could provide one. +Let the injector create an instance of `Greeter`, then call its `greet` method and observe that the +injected greeting is printed to the console: -Other than those restrictions, the Injector is free to be modified at runtime in any -way that is desired. + Greeter greeter = injector.getInstance(Greeter.class); -You may ask if dependencies are enforced in such a way, how would it ever be possible -to depend on something that a dynamically loaded class offers? This can be achieved by -using optional dependencies and by using Collection dependencies. + greeter.greet(); // prints "Hello World" -An example of a Collection dependency: +## Features - public class BookShop { +- Dependency Injection + - Constructor, Method and Field injection + - Supports qualifiers, scopes, generics and lifecycle callbacks +- Dynamic + - Register and unregister types at any time + - Ensures all dependencies are always resolvable and unambiguous +- Highly Customizable + - Choose what annotations and extensions Dirk should use + - Built-in styles for Jakarta (CDI), JSR-330 and Dirk DI or create a new one +- Extendable + - Fully documented API and SPI + - Common DI features are just extensions in Dirk +- Small + - Core jar and its dependencies are around 200 kB + +Several well known features of DI systems are implemented as standard extensions to Dirk's core system. Included +are extensions to support: + +- Producer methods and fields +- Delayed lookup of dependencies (providers) +- Assisted Injection +- Proxy creation and injection +- List, Set and Optional injection + +# Available Dependency Injection Styles + +| |Dirk|CDI|Jakarta|JSR-330| +|---|---|---|---|---| +|Artifact|dirk-di|dirk-cdi|dirk-jakarta|dirk-jsr330| +|Standard Annotations|`jakarta.inject`|`jakarta.inject`|`jakarta.inject`|`javax.inject`| +|Additional Annotations|`org.int4.dirk.annotations`|`jakarta.enterprise.inject`|`org.int4.dirk.annotations`|`org.int4.dirk.annotations`| +|Default Annotation|`@Default`|`@Default`|-|-| +|Any Annotation|`@Any`|`@Any`|-|-| +|Optional Injection|`@Opt`|-|`@Opt`|`@Opt`| +|Producer Support|`@Produces`|`@Produces`|`@Produces`|`@Produces`| +|Assisted Injection|`@Assisted` & `@Argument`1|-|`@Assisted` & `@Argument`1|`@Assisted` & `@Argument`1| +|Indirection Injection|`Provider`|`Provider` & `Instance`|`Provider`|`Provider`| +|Collection Injection|`List` & `Set`|-|`List` & `Set`|`List` & `Set`| +|Proxy Support|Yes2|Yes2|Yes2|Yes2| + +1 When detected on classpath by including `org.int4.dirk.extensions:extensions-assisted` +2 When detected on classpath by including `org.int4.dirk.extensions:extensions-proxy` + +# Documentation + +## Terminology + +|Term|Explanation| +|---|---| +|Candidate|A qualified type that could be used to satisfy a dependency| +|Dependency|A qualified type required by an inject annotated constructor, method or field| + +## Dependency Injection + +Dependencies are other classes or types that are required for the correct functioning of another class. A +dependency can be a class, an interface, a generic type or a primitive type. Dependency injection supplies +these required values automatically. Dependencies can be supplied through constructor or method parameters or +by setting fields directly. + +Constructor injection: + + public class Greeter { @Inject - private Set booksOnShelf; + public Greeter(String greeting) { ... } } -If there are no books registered with the Injector, it will simply inject an -empty collection. However, if at creation time of the BookShop instance there are -books registered those will all get injected. - -To make this dependency even more dynamic, it can be wrapped in a `Provider` (see -JSR-330 docs), like this: +Method injection: - public class BookShop { + public class Greeter { @Inject - private Provider> booksOnShelfProvider; + void setGreeting(String greeting) { ... } } -In this instance, each time the `get` method of the Provider is called, the Injector -will look for all registered books, even ones that were added or removed after the -`BookShop` instance was created, and return them. +Field injection: -## Features + public class Greeter { + @Inject + private String greeting; + } -* Supports all JSR-330 annotations - * `@Inject`, `@Qualifier`, `@Named`, `@Scope`, `@Singleton` -* Light-weight, very few dependencies -* Inject classes with dependencies, even if loaded at runtime - * Constructor, Field and Setter injection - * Assisted injection using any qualified SAM class or interface - * Optional injection with the `@Opt` annotation or any `@Nullable` annotation - * Scoping of injectables with the `ScopeResolver` interface - * Injection of generic types (for example `List`) - * Injection of collections containing all matching dependencies, if any - * Backed by `TypeExtension` interface, easily extended for special handling of other types -* Injection candidates can be supplied by: - * Direct registration of types and instances - * Scanning packages or jars for annotated classes using `ComponentScanner` - * Auto discovery of dependencies not explicitly registered - * Producer methods or fields annotated with the `@Produces` annotation - * JSR-330 providers -* Highly extendable: - * `TypeExtension`s allow for handling special types at injection points: - * Support is available for `Provider`, `List` and `Set` - * Easily create support for, for example, `Optional`, `Supplier` or `Map` - * Coming soon: support for CDI's `Instance` - * `InjectableExtension`s allow for discovering additional injectables based on interfaces or annotations: - * Support for `@Produces` via `ProducesInjectableExtension` - * Support for classes extending `Provider` is available via `ProviderInjectableExtension` -* Very configurable: - * See `Injectors` class for examples of how to configure a custom `Injector` - * Can be configured to work like CDI or Spring (work in progress) - * Select which annotations trigger injection - * Add additional qualifiers automatically (like `@Default` or `@Any`) - -## Requirements -* Java Runtime Environment 8+ installed - -# Getting started -A basic example will be used to illustrate how this framework works. We start with -a class that needs some dependencies injected: - - public class BookShop { +Any of the above forms can have a `String` dependency injected. Note that when using method or field injection +the values are set *after* the constructor is called. Referring to these values in the constructor therefore could +result in an error, instead consider implementing this logic in an initializer method that can be called after +injection completes. See Lifecycle Callbacks for more information. - @Inject - private Set booksOnShelf; +### Type Resolution - @Inject - public BookShop(CreditCardPaymentProcessor paymentProcessor) { - ... - } - } +When considering the type to inject for a dependency, the system follows standard Java rules when doing +type conversions. Any type conversion which does not require a cast is allowed, except primitive widening +conversions. This includes boxing and unboxing conversions and compatible generic conversions. -To have this class injected, we'll need to create an Injector and configure it -properly: +When a class is registered with an injector it can satisfy one or more types. The types that can +be satisfied are all implemented interfaces, its super classes and any interfaces and super classes these +implement or extend in turn. The `Greeter` class for example could be a candidate for dependencies of type +`Greeter` and `Object`. - Injector injector = Injectors.manual(); +The framework also does automatic boxing and unboxing conversion. For primitive types and their boxed types +this adds another possible type they can supply. An `Integer` can be used to inject an `int` or vice versa. +Other types the `Integer` class could satisfy are `Number` (its super class), `Object` (`Number`'s +super class), `Comparable` (implemented interface) and `Serializable` (interface implemented by +`Number`). - injector.register(CreditCardPaymentProcessor.class); - injector.register(BookShop.class); +Example: -Now we can ask the Injector to create us an instance of `BookShop`: + injector.registerInstance(42); // register an int with value 42 - BookShop bookShop = injector.getInstance(BookShop.class); +Would satisfy all the following dependencies: -This will create a `BookShop` instance by calling the constructor annotated with `@Inject` and providing it with -an instance of `CreditCardPaymentProcessor`. The `booksOnShelf` field will be injected (after the Constructor -has completed) with an empty Set as no `Book` injection candidates are currently known to the Injector. + @Inject int i; + @Inject Integer integer; + @Inject Number number; + @Inject Object object; + @Inject Comparable integerComparable; + @Inject Comparable numberComparable; -By registering some `Book` objects, and asking for a new Instance of `BookShop` we can get the `booksOnShelf` field -populated with some books: +... but would not satisfy: - injector.registerInstance(new Book("Dune")); + @Inject long l; // Primitive widening conversion not allowed + @Inject Long longValue; // Integer cannot be cast to Long + @Inject Comparable comparable; // Incompatible generic type + +### Qualifiers - BookShop anotherBookShop = injector.getInstance(BookShop.class); +Types registered with the injector can be annotated with qualifier annotations. These annotations provide +another way to distinguish candidates besides their types. This makes it possible to distinguish between multiple +candidates that may all match a dependency where exactly one dependency is required. Qualifiers can be placed on +candidates and on dependencies. In order for a candidate to match, it must have all the qualifiers specified on +the dependency. -The second `BookShop` instance will have a `Set` of `Book` objects matching those known by the Injector. If you -wanted to have all `BookShop` instances to have access to the latest known books, you can wrap the collection -with a `Provider`. The `BookShop` instances can then query for the latest books by calling the `get` method of -the `Provider` any time they want: +A dependency with an `@English` qualifier annotation: - public class BookShop { + @Inject @English private String greeting; - @Inject - private Provider> booksOnShelf; +Or as a constructor or method parameter: - public Set getLatestBooks() { - return booksOnShelf.get(); - } + public Greeter(@English String greeting) { ... } + public void setGreeting(@English String greeting) { ... } - ... - } +Candidates can be annotated directly with qualifiers, or they can be specified during +registration (for instances). -## Auto discovery of dependencies +A `Greeter` candidate with an `@English` qualifier annotation: -Registering every dependency manually (and in the correct order) can quickly become tedious. In our above -example, the `BookShop` class needed to be registered as well as its `CreditCardPaymentProcessor` dependency. -We can however have the Injector discover these automatically, as long as the dependencies themselves are -concrete classes that have a public default constructor or have exactly one constructor with annotated with `@Inject`. + @English + public class Greeter { ... } -In order to discover dependencies automatically, we have to create the Injector slightly differently: +Registering `String` candidate instances with different qualifiers: - Injector injector = Injectors.autoDiscovering(); + injector.registerInstance("Hello World", English.class); + injector.registerInstance("Hallo Wereld", Dutch.class); -Now we can get an instance of `BookShop` without any further dependencies needing to be registered: +As an example, given two `String` candidates, one annotated with `@Greeting` and `@English`, the other +annotated with `@Greeting` and `@Dutch`: - BookShop bookShop = injector.getInstance(BookShop.class); + injector.registerInstance("Hello World", English.class, Greeting.class); + injector.registerInstance("Hallo Wereld", Dutch.class, Greeting.class); -Under the hood, the Injector will notice there is no `BookShop` injection candidate registered. However, -by analyzing the `BookShop` class it sees that it can be instantiated with a Constructor that requires a -`CreditCardPaymentProcessor` -- unfortunately, there is also no `CreditCardPaymentProcessor` registered. The -Injector then recursively analyzes the `CreditCardPaymentProcessor` class, and registers this class with -itself as it noticed that it can be simply instantiated with a default constructor. +Then the following dependencies could be satisfied: -Now that the dependencies for `BookShop` can also be satisfied, the `BookShop` class is registered with the -Injector, and an instance is returned. + @Greeting @English String s; // an English greeting + @Greeting @Dutch String s; // a Dutch greeting + @English String s; // any English String + @Dutch String s; // any Dutch String + +The following dependencies will not be satisfied: -## Assisted Injection + @Greeting String s; // ambiguous, English or Dutch greeting? + String s; // ambiguous, there are two String candidates + @Greeting @French String s; // unsatisfiable, no French greeting was registered + @English int englishNumber; // unsatisfiable, no int was registered -Assisted injection makes it possible to automatically create a factory for a class that will inject known -dependencies automatically while allowing you to supply additional arguments of your own. An abstract class -or interface needs to be supplied annotated with the `@Assisted` annotation. It also must have a single -abstract method (SAM type) that returns the correct type. This type will serve as the factory that can be -injected. Secondly the product of the factory should have its supplied arguments annotated with `@Argument` -while its dependencies will be injected as usual. +### Scopes -An example Product with a dependency on `Engine` and a user supplied argument `wheelCount`: +Scopes are used to control the lifecycle of candidates, and which instance of a candidate is used to satisfy a +dependency. The injector supports two types of scopes, pseudo-scopes and normal scopes. Candidates which have a +pseudo-scope are never wrapped in a proxy, and do not need to use indirection to resolve scope conflicts. Candidates +with a normal scope will require a proxy (or indirection via a provider) when injected into other candidates with a +different scope. - public class Car { - @Inject - public Car(Engine engine, @Argument int wheelCount) { ... } - } +Which scopes are considered pseudo-scopes and which scopes provide the mandatory singleton and unscoped scopes is +determined by the used `ScopeStrategy`. The actual implementation of each scope is provided by a corresponding +`ScopeResolver` when creating the injector. + +### Lifecycle + +When the injector is configured to do lifecycle callbacks (for example, calling `@PostConstruct` or `@PreDestroy` +annotated methods), the injector will call these, respectively, after injection completes and just before the candidate +is removed. + +## Dependency validation during registration + +When adding or removing candidates from the injector, the injector ensures that all (remaining) registered +candidates can have their dependencies satisfied. If the addition or removal would result in unsatisfied or +ambiguous dependencies then an exception is thrown explaining the problem and the addition or removal is rolled back +to the previous consistent state. -Next create an interface with a single method with only the parameters you wish to supply. The injector -will automatically implement it. Note that the name of the parameter `wheelCount` must match the name in -the Product `Car`. Classes should be compiled with parameter name information in order to discover these -names at runtime; alternatively the `@Argument` annotation can be used to supply these manually. +The injector can throw the following exceptions to indicate a problem during addition or removal: - @Assisted - interface CarFactory { - Car createCar(int wheelCount); +- `AmbiguousDependencyException` when a new candidate was added which requires a single candidate for a dependency +but multiple candidates are available +- `UnsatisfiedDependencyException` when a new candidate was added which requires a single candidate for a dependency +but no candidates were available +- `AmbiguousRequiredDependencyException` when the addition or removal of a candidate would cause another dependency in +another candidate to become ambiguous, for example when a new candidate supplies another option for a dependency that +was already satisfied +- `UnsatisfiedRequiredDependencyException` when the addition or removal of a candidate would cause another dependency +in another candidate to become unsatisfied, for example when a removed candidate was the only supplier of a dependency +- `CyclicDependencyException` when a dependency cycle was detected amongst two or more candidates that was not broken +by making use of a provider +- `ScopeConflictException` when a scoped dependency depends directly on another scoped dependency, and the conflict +could not be resolved automatically by means of a proxy + +## Registration of Candidates at runtime + +Dirk allows addition and removal of new candidates at any time, assuming the change won't leave the injector in an +inconsistent state where it can't satisfy all its existing dependencies. + +Let's assume there is a class which requires a list of books: + + class BookShop { + @Inject List availableBooks; + + List getBooks() { return availableBooks; } } -The factory interface can now be injected anywhere you wish to create a `Car` instance. Only `wheelCount` -needs to be supplied, while `engine` gets injected as usual: +The class is registered, and an instance is obtained: - public class Garage { - @Inject - public Garage(CarFactory factory) { - Car car = factory.createCar(5); - } + injector.register(BookShop.class); + + BookShop bookShop = injector.getInstance(BookShop.class); + +When calling `getBooks` the list will be empty as no candidates were registered that supply a type `Book`: + + assertThat(bookShop.getBooks()).isEmpty(); // passes + +This can be resolved by registering a `Book` and obtaining a new `BookShop` instance: + + injector.registerInstance(new Book("Dune")); + + BookShop bookShop2 = injector.getInstance(BookShop.class); + + assertThat(bookShop2.getBooks()).hasSize(1); // passes + +Creating a new `BookShop` every time the list of available books changes is a bit cumbersome, so it is possible to +use indirection to obtain the latest available books by means of a provider. Redefine the `BookShop` class: + + class ModifiedBookShop { + @Inject Provider> availableBooks; + + List getBooks() { return availableBooks.get(); } } -## Producers -Producers are fields or methods that can supply a dependency that can be used for injection. They are useful -for objects that are too complex for the injector to construct itself or for example make use of object pooling. +An instance of this class will now immediately respond to a change in the books available: + + injector.register(ModifiedBookShop.class); + + ModifiedBookShop modifiedBookShop = injector.getInstance(ModifiedBookShop.class); + + assertThat(modifiedBookShop.getBooks()).isEmpty(); // passes + + injector.registerInstance(new Book("Dune")); + + assertThat(modifiedBookShop.getBooks()).hasSize(1); // passes -For example: +In more complex scenario's it is sometimes necessary to register multiple candidates simultaneously to avoid a chicken/egg +type problem. Given two classes: - @Producer - public static Connection createConnection() { - return new Connection( ... ); + class A { + @Inject B b; // depends on B } -When registering a class containing this method, the injector will start providing instances of `Connection` -by calling the `createConnection` method every time a `Connection` is needed. + class B { + @Inject Provider a; // indirectly depends on A + } + +Registering either of these two candidates separately is not allowed as not all dependencies could be satisfied: -It is also possible to annotate a field in this way: + injector.register(A.class); // --> UnsatisfiedDependencyException, requires B + injector.register(B.class); // --> UnsatisfiedDependencyException, requires A - @Producer - private static Connection connection = new Connection(); +However, registering both at the same time works as expected: -Just like the method, the field will be read every time a new `Connection` is required. + injector.register(List.of(A.class, B.class)); // works! -### Scoping +## Auto Discovery -By default, an instance produced by a producer has no scope applied to it. This means it will be called or -read every time an instance of the type it supplies is required. By annotating the producer method or field -with a scope annotation the same instance will be returned by the injector within an active scope. For example: +When auto discovery is enabled, Dirk will attempt to automatically register candidates referred to in dependencies +that are not yet registered, assuming it can find a way to construct them. Given two classes: - @Producer - @Singleton - public static Connection createConnection() { - return new Connection( ... ); + class BookShop { + @Inject CreditCardPaymentProcessor paymentProcessor; } -The above producer will only be called once, and the same `Connection` instance will be injected everywhere. + public class CreditCardPaymentProcessor { + } -### Qualifiers +In order to create a `BookShop` instance, normally a `CreditCardPaymentProcessor` must be registered first (or simultaneously): -Producers may be annotated with qualifiers to distinguish instances of the same type: + Injector injector = Injectors.manual(); - @Producer @Red Color color = Color.RED; - @Producer @Green Color createGreen() { return Color.GREEN; } - @Producer @Named("username") String name = "John"; + injector.register(CreditCardPaymentProcessor.class); + injector.register(BookShop.class); -At injection sites this qualifier can then be used to get a specific instance: - - @Inject @Red Color color; // will be Color.RED - @Inject @Green Color color; // will be Color.GREEN - @Inject @Named("username") String name; // will be "John" + BookShop bookShop = injector.getInstance(BookShop.class); + +Since `CreditCardPaymentProcessor` has an empty public constructor, the candidate could be automatically discovered when +Dirk encounters the missing dependency in `BookShop`: + + Injector injector = Injectors.autoDiscovering(); -Or to get all `Color` instances: + injector.register(BookShop.class); // registers BookShop and the discovered type CreditCardPaymentProcessor - @Inject List colors; // will be List.of(Color.RED, Color.GREEN) + BookShop bookShop = injector.getInstance(BookShop.class); + +This automatic discovery works recursively and so large graphs of dependent candidates can all be discovered by +registering only a few root candidates. -### Dependencies +## Optional Injection -A producer can also have dependencies of its own. This can be achieved by either adding parameters to the -method declaration or by making it a non-static method of a class that has dependencies. Here are two ways to make -the `createConnection` method depend on a `Credentials` instance: +Dirk allows dependencies to be optional if configured as such. When a dependency is optional, and no suitable candidate +is available, Dirk will either inject `null` (for constructor and method parameters) or skip injection completely (for +field injection). For example: - @Producer - public static Connection createConnection(Credentials credentials) { - return new Connection(credentials, ... ); + class Greeter { + @Inject @Opt private String greeting = "Hello World"; } -Or: +When no `String` candidate is available, this version of the `Greeter` class will fall back to a default greeting. +Using constructor injection the same can be achieved as follows: - public class DatabaseSetup { - @Inject Credentials credentials; + class Greeter { + final String greeting; - @Producer - public Connection createConnection() { - return new Connection(credentials, ... ); + @Inject + Greeter(@Opt String greeting) { + this.greeting = greeting == null ? "Hello World" : greeting; } } -Note that for non-static producer methods or fields, an instance of the declaring class must be available or -can be created on demand. It is not possible for the declaring class to be directly or indirectly dependent on -something one of its fields or methods produces as this would be a circular dependency. Declare the relevant -producer `static` or use the `Provider` interface to avoid this. +This allows a convenient way to have default values for, for example, configuration settings: -## Optional dependencies + class TimeProvider { + @Inject @Opt @Named("server.ntp.url") String url = "pool.ntp.org"; + @Inject @Opt @Named("server.sync.interval") int interval = 3600; // every hour + } -Dependencies can be marked optional by either using a `Nullable` annotation (any annotation named `Nullable` -is supported) or the `Opt` annotation provided by this project. When such a dependency is encountered, -it will not be injected if the required dependency is not available. For example: +## Customizing Injectors - @Inject @Nullable @Named("database.user") private String user; - @Inject @Opt @Named("database.timeout") private int timeout = 1000; +The injector can be customized in several ways: -In the above example, `user` will be left `null` and `timeout` will be left `1000` if the corresponding -dependency is unavailable. +- The `InjectorStrategy` allows for detailed customization of how the injector works: + - `AnnotationStrategy` is responsible for detecting injectable members and their qualifiers + - `ScopeStrategy` is responsible for detecting scope annotations, and configuring default scope annotations + - `ProxyStrategy` is responsible for creating proxies + - `LifeCycleCallbacksFactory` controls which and in what order lifecycle methods are to be called +- Using `InjectionTargetExtension`s + - Allows customizing injection targets of a specific generic interface type + - Inject lists of instances using the `ListInjectionTargetExtension` + - Inject sets of instances using the `SetInjectionTargetExtension` + - Inject providers to allow runtime creation of instances using the `ProviderInjectionTargetExtension` + - Inject CDI `Instance` types using the `InstanceInjectionTargetExtension` +- Using `TypeRegistrationExtension`s + - Allows detecting additional candidates when a type is registered + - Add additional candidates for each `@Produces` annotation using the `ProducesTypeRegistrationExtension` + - Add a candidate when types implement the `Provider` interface + - Assisted injection: When registering a SAM type (a class or interface with a single abstract method) automatically generate + an implementation of this method which produces a new candidate, see `AssistedTypeRegistrationExtension` +- Using `ScopeResolver`s + - Allows creating resolvers for custom scope annotations +- The injector can be configured to only provide candidates explicitly registered with it or also allow auto + discovery of additional candidates through their dependencies -## Scoping +## Extensions -By default, all dependencies are created by the injector each time they are needed, except for dependencies -supplied by providers or producers (which gives the user control over when to create a new instance or when -to return an existing one) or dependencies which were registered as an instance. +### Type Registration Extensions -To prevent the injector creating a new instance every time, classes and producers can be annotated with -`@Singleton`. +A `TypeRegistrationExtension` is called whenever a new type is encountered by the injector during registration. +The extension is given the opportunity to register additional types that it can derive from the new type. Usually, +additional types are derived by inspecting the given type for annotations or interfaces it implements. -### Scope Resolvers +### Injection Target Extension -An injector can be configured with instances of the `ScopeResolver` interface. This interface makes it -possible to introduce a custom scope allowing the injector to track a different instance of a dependency -depending on the currently active scope it is accessed within. The `AbstractScopeResolver` subclass -simplifies this process further by only requiring the name of the annotation that marks the custom -scope, and a method which identifies which scope is currently active. +An `InjectionTargetExtension` allows creating instances of a specific generic interface prior to injection. +The interface must have at least one type variable which the extension must expose to the injector as its element +type. The element type, together with any qualifiers on the injection target, determines which candidates can be +used by the extension to provide the interface implementation. The extension is allowed to delay the lookup or +perform it immediately. -For example, given a `ScopeResolver`: +Interfaces which are extended by an injection target extension can no longer be directly injected, even if a +candidate implements the interface. - public class TestScopeResolver extends AbstractScopeResolver { - private static String currentScope = "default"; +Examples are the `ListInjectionTargetExtension` and the `ProviderInjectionTargetExtension`, which provide support for +injecting lists of candidates or delayed lookup of candidates (through the `List` and `Provider` interfaces) +respectively. - @Override - public Class getScopeAnnotationClass() { - return TestScoped.class; - } +Extensions which do delayed lookup are allowed to wrap any other extended type, but in all other cases extensions +cannot be nested. - @Override - public String getCurrentScope() { - return currentScope; - } +# The Core Library - public void changeScope(String scope) { - this.currentScope = scope; - } - } +Dirk's library and extension modules provide several standard extensions, strategies and scope resolvers that are +normally integrated directly in most dependency injection frameworks. These are maintained as part of the project +and can be used as desired, or serve as a starting point for 3rd party customizations. -.. and a `@Scope` annotation: +## Injection Target Extensions - @Scope - @Documented - @Retention(RUNTIME) - public @interface TestScoped { } +- `ListInjectionTargetExtension` supports injecting multiple matching candidates as instances in a `List`. +- `SetInjectionTargetExtension` supports injecting multiple matching candidates as instances in a `Set`. +- `ProviderInjectionTargetExtension` supports indirect access to candidate instances using a + configurable interface (often the `Provider` interface) + +## Type Registration Extensions -... and a `@TestScoped` annotated class registered with the injector: +### `ProducesTypeRegistrationExtension` - @TestScoped - public class Car { } +Allows fields and methods to act as factories for additional candidates using a configurable producer annotation. +Producer methods with parameters are considered to offer candidates with dependencies which are to be provided when the +method is called. -... and an injector constructed with the above `ScopeResolver`: +The following examples assume the annotation `@Produces` is configured to mark producer fields and methods: - Injector injector = Injectors.manual(new TestScopeResolver()); +A field which produces a `String`: - injector.register(Car.class); + @Produces String greeting = "Hello World"; -When asking the injector for a `Car` instance, it will first determine the current scope. This is achieved -by finding the `ScopeResolver` responsible for resolving `TestScoped` annotations. The resolver is queried -for an instance of `Car`, which the `AbstractScopeResolver` will attempt to find in the currently active scope -supplied by `getCurrentScope`. +A method which produces a `Connection` given a URI: -If no `Car` instance is available in the current scope, the injector will create one and store it in the -resolver. If one was available, it is simply returned. For example: + @Produces Connection createConnection(URI uri) { ... } - Car car = injector.getInstance(Car.class); +Producers can be static or non-static. Static producers can be called at any time and have no access to any +dependencies injected in their owner instance. Non-static producers can either have dependencies provided as parameters +(in the case of a producer method) or by accessing fields in their owner instance. -The above will return a new `Car` and is registered with our `TestScopeResolver`. When the injector is -queried again, the same instance is returned: +### `ProviderTypeRegistrationExtension` - injector.getInstance(Car.class) == injector.getInstance(Car.class) +Allows a type to act as a factory for an additional candidate when it implements a configurable interface (typically, +the `Provider` interface). -Depending on the currently active scope, the injector will return different instances. Let `resolver` be -an instance of our `TestScopeResolver`: +Below an example of a class that implements the `Provider` interface and provides an additional candidate `Connection`: - Car car = injector.getInstance(Car.class); - Car sameCar = injector.getInstance(Car.class); + class ConnectionProvider implements Provider { + @Inject URI uri; - assert car == sameCar; + @Override + public Connection get() { ... } + } - resolver.changeScope("local"); +### `AssistedTypeRegistationExtension` - Car otherCar = injector.getInstance(Car.class); +Allows a type which has a single abstract method (a SAM type) with a non-void return type to act as a factory for the +returned type; any dependencies the produced type may have are injected by the injector, including the arguments +supplied to the factory method; this is otherwise known as assisted injection. - assert car != otherCar; +Assume we have a candidate that requires an additional parameter at runtime before it can be constructed, but also +has a required dependency: -Scopes can also be inactive. The `AbstractScopeResolver` allows this by returning a `null` as current scope. -When an attempt is made to get an instance of a scoped object when no scope is active, an `OutOfScopeException` -will be thrown: + class Greeter { + @Inject String greeting; // a normal dependency + @Inject @Argument LocalTime timeOfDay; - resolver.changeScope(null); + void greet() { ... } + } + +With this extension we can have Dirk generate a factory that can be injected, and which takes the additional argument +as a parameter: + + @Inject Function greeterFactory; + +The factory can be called as a normal `Function`: + + Greeter greeter = greeterFactory.apply(LocalTime.now()); + + greeter.greet(); - Car car = injector.getInstance(Car.class); // throws exception +Any SAM type can be used, as long as the arguments its method accepts can be matched up with the arguments in the +produced type. The following two examples would also allow constructing the `Greeter` type: + + interface GreeterFactory { + Greeter createGreeter(LocalTime timeOfDay); + } + +... and: + + abstract class GreeterFactory { + Greeter createGreeter(LocalTime timeOfDay); + } # BSD License @@ -421,7 +552,7 @@ https://github.com/ronmamo/reflections Linking: Maven Dependency -Usage: +Used for: - Scanning classpath for annotations ### Geantyref @@ -431,7 +562,7 @@ https://github.com/leangen/geantyref Linking: Maven Dependency -Usage: +Used for: - Implementation of Annotation interface ### Apache Commons Lang @@ -441,7 +572,7 @@ https://commons.apache.org/proper/commons-lang/ Linking: Embedded (minimal required code directly included) -Usage: +Used for: - `TypeUtils` for dealing with generic types and type variables ### Byte Buddy @@ -451,5 +582,6 @@ https://bytebuddy.net/ Linking: Maven Dependency (optional) -Usage: +Used for: - Generating factories for Assisted Injection +- Generating proxies