2021SC@SDUSC
1, Inject overview
ActiveJ Inject is a lightweight and powerful dependency injection repository with strong performance and no third-party dependency. It is multi-threaded friendly and has rich functions. It can boast that its startup time and running time are very fast, which is much faster than Spring DI or Guice. ActiveJ Inject is one of ActiveJ technologies, but it has little dependence on third parties and can be used as an independent component.
ActiveJ Inject features:
- Annotation based configuration and manual binding are supported to avoid reflection overhead.
- Bindings are divided into modules that can be reused in other applications.
- Optimized injection for single threaded and multi-threaded usage
- Ability to combine, cover and transform bindings
- Single child, nested scope and transient binding are supported
- The dependency graph is processed once at startup
- Provides a way to reflect on dependency diagrams
- No third party dependency
Dependency injection: redefining
Enjoy development with a variety of powerful tools. ActiveJ Inject simplifies the development, debugging, refactoring and reuse of your code without restrictions and overhead.
Annotation processing is separated into a standard plug-in, which is used by default and allows the generation of missing dependencies. However, if you need to implement complex business logic, you can use ActiveJ Inject DSL (Domain Specific Language), or even create your own annotation processing plug-in.
DSL provides support for programmatic binding generation, reflection and transformation of dependency graph, automatic generation of missing bindings, and modification of existing bindings. In this way, you can use all the functions of Java to create complex binding and dependency diagrams through algorithms directly according to runtime information and settings at runtime.
Module cookbook = new AbstractModule() { @Provides Sugar sugar() { return new Sugar("Sugar", 10.f); } @Provides Butter butter() { return new Butter("Butter", 20.0f); } @Provides Flour flour() { return new Flour("Flour", 100.0f); } @Provides Pastry pastry(Sugar sugar, Butter butter, Flour flour) { return new Pastry(sugar, butter, flour); } @Provides Cookie cookie(Pastry pastry) { return new Cookie(pastry); } }; Injector.of(cookbook).getInstance(Cookie.class);
2, Core inject structure
The core inject structure in the source code is as follows:
As shown in the figure above, io.activej.inject consists of five packages: annotation, binding, impl, module and util, as well as eight separate classes: Inject, InstanceInjector, InstanceProvider, Key, KeyPattern, Qualifiers, ResourceLocator and Scope. This blog is to read the functions of these 8 separate classes.
3, Code interpretation
3.1 Injector
The Injector is the main working part of ActiveJ Injector.
It stores the trie of the binding graph and the generated singleton cache.
Each injector is associated with exactly zero or one instance of each Key.
The Injector uses the binding diagram under the trie root directory to recursively create and store object instances associated with some keys. The branch of trie is used for enter scopes.
The Injector is a component. It recursively traverses the dependency graph in a sequential manner, creates them first, and provides all the required dependencies (injection).
Binding is the default list - if an instance is created once, it will not be created from scratch. If it is needed by other bindings, the Injector will get it from the cache. You don't need to apply any additional comments to it.
In order to provide the requested key, the Injector recursively creates all its dependencies. If no binding is found in its scope, it returns to the Injector of its parent scope.
Let's take a look at the methods in the Injector class:
Construction method:
private Injector(@Nullable Injector parent, Trie<Scope, ScopeLocalData> scopeDataTree) { this.parent = parent; this.scopeDataTree = scopeDataTree; ScopeLocalData data = scopeDataTree.get(); this.localSlotMapping = data.slotMapping; this.localCompiledBindings = data.compiledBindings; AtomicReferenceArray[] scopeCaches = parent == null ? new AtomicReferenceArray[1] : Arrays.copyOf(parent.scopeCaches, parent.scopeCaches.length + 1); AtomicReferenceArray localCache = new AtomicReferenceArray(data.slots); localCache.set(0, this); scopeCaches[scopeCaches.length - 1] = localCache; this.scopeCaches = scopeCaches; }
Usespecialization method:
This method enables specialization of compilation bindings. Depends on the activej specialization module.
public static void useSpecializer() { try { Class<?> aClass = Class.forName("io.activej.specializer.Utils$InjectorSpecializer"); Constructor<?> constructor = aClass.getConstructor(); constructor.setAccessible(true); Object specializerInstance = constructor.newInstance(); Function<CompiledBinding<?>, CompiledBinding<?>> specializer = (Function<CompiledBinding<?>, CompiledBinding<?>>) specializerInstance; Injector.bytecodePostprocessorFactory = () -> specializer; } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InstantiationException e) { throw new IllegalStateException("Can not access ActiveJ Specializer", e); } }
Several methods:
This constructor combines the given modules (and DefaultModule), and then compiles them with compile (Injector, Module).
public static Injector of(Module... modules) { return compile(null, Modules.combine(Modules.combine(modules), new DefaultModule())); } public static Injector of(@Nullable Injector parent, Module... modules) { return compile(parent, Modules.combine(Modules.combine(modules), new DefaultModule())); } public static Injector of(@NotNull Trie<Scope, Map<Key<?>, Binding<?>>> bindings) { return compile(null, UNSCOPED, bindings.map(map -> map.entrySet().stream().collect(toMap(Entry::getKey, entry -> singleton(entry.getValue())))), errorOnDuplicate(), identity(), refusing()); }
compile method:
I think this compilation method is still more important. It is the most mature compilation method and allows you to create any configured Injector. It should be noted that any Injector will set an Injector key binding to provide itself. These keys are described below:
- @param parent parent Injector, which is called when the Injector cannot satisfy the request
- @The scope of param scope Injector can be described as the "root prefix" of binding trie, which is used when {enterScope enters the scope}
- @param bindingsMultimap is the trie of the binding set graph. Each key has multiple binding that may conflict. These bindings are resolved as part of the compilation.
- @param multibinder the multibinder that is called every time a binding conflict occurs (see {@ link Multibinders#combinedMultibinder})
- @param transformer is a converter that is called once per binding (see {@ link BindingTransformers#combinedTransformer})
- @param generator the generator called for each missing binding (see {@ link BindingGenerators#combinedGenerator})
public static Injector compile(@Nullable Injector parent, Scope[] scope, @NotNull Trie<Scope, Map<Key<?>, Set<Binding<?>>>> bindingsMultimap, @NotNull Multibinder<?> multibinder, @NotNull BindingTransformer<?> transformer, @NotNull BindingGenerator<?> generator) { Trie<Scope, Map<Key<?>, Binding<?>>> bindings = Preprocessor.reduce(bindingsMultimap, multibinder, transformer, generator); Set<Key<?>> known = new HashSet<>(); known.add(Key.of(Injector.class)); // injector is hardcoded in and will always be present if (parent != null) { known.addAll(parent.localCompiledBindings.keySet()); } Preprocessor.check(known, bindings); Trie<Scope, ScopeLocalData> scopeDataTree = compileBindingsTrie( parent != null ? parent.scopeCaches.length : 0, scope, bindings, parent != null ? parent.localCompiledBindings : emptyMap() ); return new Injector(parent, scopeDataTree); }
There are some other methods in the Injector. The above are the more important methods I selected.
3.2 InstanceInjector interface
This is a function that can inject instances into {@ link io.activej.inject.annotation.inject}
Fields and methods for some existing objects.
This is called "post injections" because this injection is not part of object creation.
It has an io.activej.inject.module.DefaultModule default generator, which can only be obtained by relying on it and then requesting it from the Injector.
The source code is as follows:
public interface InstanceInjector<T> { Key<T> key(); void injectInto(T existingInstance); }
It can Inject instances into fields and methods of some existing objects in Inject. Take a look at this simple example.
@Inject String message; @Provides String message() { return "Hello, world!"; } @Override protected void run() { System.out.println(message); } public static void main(String[] args) throws Exception { Launcher launcher = new InstanceInjectorExample(); launcher.launch(args); }
The question that may be a little confusing is, how does the launcher actually know that the message variable contains the "Hello, world!" string so that it can be displayed in the run() method? Here, in the internal work of DI, InstanceInjector actually provides help to the launcher.
3.3 InstanceProvider interface
Unlike other DI frameworks, the provider is just a version of {Injector.getInstance} with a baking key.
If you need a function that returns one new object at a time, you need to bind {@ link Transient}.
The main reason for its existence is that its binding has a {io.activej.inject.module.DefaultModule default generator}, so {io.activej.inject.annotation.provider methods} can request it smoothly.
In addition, it can also be used to delay dependent loop resolution.
public interface InstanceProvider<T> { Key<T> key(); T get(); }
3.4 ResourceLocator interface
As resource locators, some of these methods are also rewrites of the getInstance method.
public interface ResourceLocator { <T> @NotNull T getInstance(@NotNull Key<T> key); <T> @NotNull T getInstance(@NotNull Class<T> type); <T> @Nullable T getInstanceOrNull(@NotNull Key<T> key); <T> @Nullable T getInstanceOrNull(@NotNull Class<T> type); <T> T getInstanceOr(@NotNull Key<T> key, T defaultValue); <T> T getInstanceOr(@NotNull Class<T> type, T defaultValue); }
3.5 Qualifiers
This class contains utility methods for validating and creating objects used as qualifiers.
Qualifiers are used as additional tags to distinguish different keys with the same type.
public final class Qualifiers { public static Object uniqueQualifier() { return new UniqueQualifierImpl(); } public static Object uniqueQualifier(@Nullable Object qualifier) { return qualifier instanceof UniqueQualifierImpl ? qualifier : new UniqueQualifierImpl(qualifier); } public static boolean isUnique(@Nullable Object qualifier) { return qualifier instanceof UniqueQualifierImpl; } }
3.6 Key
Key defines the ID of the binding. In any DI, key is usually an object type and some optional tags to distinguish between bindings that make objects have the same type.
In ActiveJ Inject, Key is also a special abstract class of type tag, which can store type information with the shortest syntax in Java.
For example, to create a key type: Map < string, list < integer > >, you can use this syntax: new key < map < string, list < integer > > () {}.
If your type is unknown at compile time, you can use io.activej.types.types#parameteredType to generate a parameterized type and give it to the ofType Key.ofType constructor.
The construction method is as follows:
public Key() { this.type = getTypeParameter(); this.qualifier = null; } public Key(@Nullable Object qualifier) { this.type = getTypeParameter(); this.qualifier = qualifier; } Key(@NotNull Type type, @Nullable Object qualifier) { this.type = type; this.qualifier = qualifier; }
qualified method:
Returns a new key of the same type, but the qualifier has been replaced with the given key
public Key<T> qualified(Object qualifier) { return new KeyImpl<>(type, qualifier); }
getRawType() method:
Shortcut to {Types#getRawType(Type)}(key.getType()).
The result is also cast to a properly parameterized class.
public @NotNull Class<T> getRawType() { return (Class<T>) Types.getRawType(type); }
getDisplayString method:
If this key has a qualifier, the base type with display string format (package name stripped) and pre qualifier display string is returned.
public String getDisplayString() { return (qualifier != null ? Utils.getDisplayString(qualifier) + " " : "") + ReflectionUtils.getDisplayName(type); }
3.7 KeyPattern
Match the pattern of dependency injection Key
If the type of Key#getType()Key can be assigned to the type of this pattern, and the qualifier of this pattern is null or matches the qualifier of Key#getQualifier()Key, the Key matches.
Construction method:
public KeyPattern() { this.type = getTypeParameter(); this.qualifier = null; } public KeyPattern(Object qualifier) { this.type = getTypeParameter(); this.qualifier = predicateOf(qualifier); } public KeyPattern(Predicate<?> qualifier) { this.type = getTypeParameter(); this.qualifier = qualifier; } KeyPattern(@NotNull Type type, Predicate<?> qualifier) { this.type = type; this.qualifier = qualifier; }
3.8 Scope
Scope provides us with "local singletons", which is as long as the scope itself. The scope of ActiveJ Inject is somewhat different from other DI libraries.
- The internal structure of the Injector is a prefix tree, and the prefix is a range.
- The identifier (or prefix) of the tree is a simple annotation.
- The Injector can enter the scope. This means that you create an Injector and its scope will be set to the scope it enters.
- This can be done many times, so you can have N injectors in some range.
Construction method:
private Scope(@NotNull Class<? extends Annotation> annotationType, boolean threadsafe) { this.annotationType = annotationType; this.threadsafe = threadsafe; }
of method:
Create a scope from a tag (or stateless) comment, identified only by its class.
public static Scope of(Class<? extends Annotation> annotationType) { checkArgument(isMarker(annotationType), "Scope by annotation type only accepts marker annotations with no arguments"); ScopeAnnotation scopeAnnotation = annotationType.getAnnotation(ScopeAnnotation.class); checkArgument(scopeAnnotation != null, "Only annotations annotated with @ScopeAnnotation meta-annotation are allowed"); return new Scope(annotationType, scopeAnnotation.threadsafe()); }
Create a scope from a real (or its custom proxy impl) annotation instance.
public static Scope of(Annotation annotation) { return of(annotation.annotationType()); }
4, Summary
When interpreting the code this time, the classes or interfaces in the core inject that are independent of several packages, some are dependencies of other classes, and some are interfaces that need to be implemented in other classes, which are more important.