diff --git a/README.md b/README.md index 34a57a0..8d04ea7 100644 --- a/README.md +++ b/README.md @@ -543,3 +543,34 @@ class ImageProcessor { } } ``` + +## Installation +You may use the following code to use DiVine in your project. +Check out our [jitpack](https://jitpack.io/#qibergames/di-vine) page for the latest version. + +### Maven +```xml + + jitpack.io + https://jitpack.io + +``` +```xml + + com.github.qibergames + di-vine + VERSION + +``` + +### Gradle +```gradle +repositories { + maven { url 'https://jitpack.io' } +} +``` +```gradle +dependencies { + implementation 'com.github.qibergames:di-vine:VERSION' +} +``` diff --git a/src/main/java/com/atlas/divine/Container.java b/src/main/java/com/atlas/divine/Container.java index ba59a69..0a235d7 100644 --- a/src/main/java/com/atlas/divine/Container.java +++ b/src/main/java/com/atlas/divine/Container.java @@ -128,7 +128,9 @@ public void removeHook(@NotNull String id) { * * @throws InvalidServiceException if the annotation does not have a RUNTIME retention */ - public void addProvider(@NotNull Class annotation, @NotNull AnnotationProvider provider) { + public void addProvider( + @NotNull Class annotation, @NotNull AnnotationProvider provider + ) { CallContext context = getContextContainer(); context.getContainer().addProvider(annotation, provider); } @@ -403,7 +405,7 @@ public void reset() { * * @return the container tree exported as json */ - public JsonObject export() { + public @NotNull JsonObject export() { JsonObject json = new JsonObject(); ContainerRegistry global = ofGlobal(); diff --git a/src/main/java/com/atlas/divine/impl/DefaultContainerImpl.java b/src/main/java/com/atlas/divine/impl/DefaultContainerImpl.java index 63e71b2..9504e75 100644 --- a/src/main/java/com/atlas/divine/impl/DefaultContainerImpl.java +++ b/src/main/java/com/atlas/divine/impl/DefaultContainerImpl.java @@ -11,7 +11,7 @@ import com.atlas.divine.runtime.lifecycle.AfterInitialized; import com.atlas.divine.runtime.lifecycle.BeforeTerminate; import com.atlas.divine.descriptor.property.PropertyProvider; -import com.atlas.divine.runtime.context.Security; +import com.atlas.divine.runtime.context.Contexts; import com.atlas.divine.descriptor.factory.Factory; import com.atlas.divine.descriptor.factory.NoFactory; import com.atlas.divine.descriptor.implementation.NoImplementation; @@ -69,7 +69,7 @@ public class DefaultContainerImpl implements ContainerRegistry { /** * The map of the registered implementation providers for custom annotations. */ - private final @NotNull Map<@NotNull Class, @NotNull AnnotationProvider> providers = new ConcurrentHashMap<>(); + private final @NotNull Map<@NotNull Class, @NotNull AnnotationProvider> providers = new ConcurrentHashMap<>(); /** * The map of registered services that are grouped by their unique identifier. @@ -83,6 +83,9 @@ public class DefaultContainerImpl implements ContainerRegistry { /** * The map of fields to be lazily injected by the container. + *

+ * All fields annotated with {@link Inject} that specify {@code lazy = true} will be injected by the container, + * after the whole dependency tree for a service is resolved. */ private final @NotNull ThreadLocal<@NotNull Map<@NotNull Field, @NotNull LazyFieldAccess>> lazyFields = ThreadLocal.withInitial(LinkedHashMap::new); @@ -91,6 +94,19 @@ public class DefaultContainerImpl implements ContainerRegistry { */ private final @NotNull ThreadLocal<@NotNull Boolean> injectingLazyFields = ThreadLocal.withInitial(() -> false); + /** + * The map of initialization methods to be lazily invoked by the container. + *

+ * All methods annotated with {@link AfterInitialized} that specify {@code lazy = true} will be invoked by the + * container, after the whole dependency tree for a service is resolved. + */ + private final @NotNull ThreadLocal<@NotNull Map<@NotNull Method, @NotNull Object>> lazyMethods = ThreadLocal.withInitial(LinkedHashMap::new); + + /** + * The indication, whether the container is currently invoking lazy methods. + */ + private final @NotNull ThreadLocal<@NotNull Boolean> invokingLazyMethods = ThreadLocal.withInitial(() -> false); + /** * The root container of the container hierarchy. It is {@code null} if {@code this} container is the root. */ @@ -202,7 +218,7 @@ public void removeHook(@NotNull String id) { */ @Override public void addProvider( - @NotNull Class annotation, @NotNull AnnotationProvider provider + @NotNull Class annotation, @NotNull AnnotationProvider provider ) { Retention retention = annotation.getAnnotation(Retention.class); if (retention == null || retention.value() != RetentionPolicy.RUNTIME) @@ -296,7 +312,7 @@ public void insert(@NotNull @ServiceLike Class @NotNull ... services) { @Override public @NotNull List<@NotNull TServices> getMany(@NotNull String id) { try { - return getMany(id, Security.getCallerClass(Thread.currentThread().getStackTrace())); + return getMany(id, Contexts.getCallerClass(Thread.currentThread().getStackTrace())); } catch (ClassNotFoundException e) { return getMany(id, Container.class); } @@ -315,7 +331,7 @@ public void insert(@NotNull @ServiceLike Class @NotNull ... services) { @Override public @NotNull T get(@NotNull Class type) { try { - return get(type, Security.getCallerClass(Thread.currentThread().getStackTrace())); + return get(type, Contexts.getCallerClass(Thread.currentThread().getStackTrace())); } catch (ClassNotFoundException e) { return get(type, Container.class); } @@ -353,7 +369,7 @@ public void insert(@NotNull @ServiceLike Class @NotNull ... services) { @NotNull Class type, @Nullable TProperties properties ) { try { - return get(type, Security.getCallerClass(Thread.currentThread().getStackTrace()), properties); + return get(type, Contexts.getCallerClass(Thread.currentThread().getStackTrace()), properties); } catch (ClassNotFoundException e) { return get(type, Container.class, properties); } @@ -474,6 +490,7 @@ public TResult resolve( finally { stack.pop(); injectLazyFields(); + invokeLazyMethods(); } } @@ -805,12 +822,19 @@ private void handleServiceInit( // iterate over each method of the service class for (Method method : type.getDeclaredMethods()) { // skip methods that are not annotated with @AfterInitialized - if (!method.isAnnotationPresent(AfterInitialized.class)) + AfterInitialized init = method.getDeclaredAnnotation(AfterInitialized.class); + if (init == null) continue; // make the method accessible for the dependency injector method.setAccessible(true); + // register the method to be lazily invoked by the container + if (init.lazy()) { + lazyMethods.get().put(method, service); + continue; + } + // invoke the method on the service instance try { method.invoke(service); @@ -1108,9 +1132,6 @@ private void injectLazyFields() { if (!resolvingStack.get().isEmpty()) return; - // resolve the lazy fields to be injected - Map fields = lazyFields.get(); - // return and clean up if the lazy fields had been already injected // this prevents infinite loops when resolving circular dependencies if (injectingLazyFields.get()) { @@ -1121,6 +1142,9 @@ private void injectLazyFields() { // begin injecting the lazy fields injectingLazyFields.set(true); + // resolve the lazy fields to be injected + Map fields = lazyFields.get(); + // iterate over the lazy fields to be injected for (Map.Entry entry : fields.entrySet()) { // resolve the field and the access to the field @@ -1151,6 +1175,66 @@ private void clearLazyFields() { lazyFields.remove(); } + /** + * Invoke the lazy methods that are stored for the current dependency tree. + */ + private void invokeLazyMethods() { + // return if the dependency resolving tree is still being resolved + if (!resolvingStack.get().isEmpty()) + return; + + // return if currently injecting lazy fields + // this prevents the lazy methods from being invoked before the lazy fields are injected + if (injectingLazyFields.get()) + return; + + // return if the lazy methods have already been invoked + // this prevents infinite loops when resolving circular dependencies + if (invokingLazyMethods.get()) { + clearLazyMethods(); + return; + } + + // begin invoking the lazy methods + invokingLazyMethods.set(true); + + // resolve the lazy methods to be invoked + Map methods = lazyMethods.get(); + + // iterate over the lazy methods to be invoked + for (Map.Entry entry : methods.entrySet()) { + // resolve the method and the instance to invoke the method on + Method method = entry.getKey(); + Object instance = entry.getValue(); + + // invoke the method on the instance + try { + method.invoke(instance); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new ServiceInitializationException( + "Error whilst invoking initialization method `" + method.getName() + "` of service " + + instance.getClass().getName(), e + ); + } + } + + // clear the lazy methods after invoking them + clearLazyMethods(); + + // end invoking the lazy methods + invokingLazyMethods.set(false); + } + + /** + * Clear the lazy methods that are stored for the current dependency tree. + */ + private void clearLazyMethods() { + // clear any pending lazy methods that are stored in the thread-local map + lazyMethods.get().clear(); + // remove the thread-local map from the thread + lazyMethods.remove(); + } + /** * Inject the fields of the specified instance. * @@ -1229,17 +1313,18 @@ private void injectField( * @param target the class that requested the dependency * @return the implementation of the annotation */ + @SuppressWarnings({"unchecked", "rawtypes"}) private @Nullable Object provideAnnotation(@NotNull Annotation @NotNull [] annotations, @NotNull Class target) { // iterate over the annotations of the field for (Annotation annotation : annotations) { // skip annotations that are not registered in the container - AnnotationProvider provider = providers.get(annotation.annotationType()); + AnnotationProvider provider = providers.get(annotation.annotationType()); if (provider == null) continue; // provide the implementation of the annotation try { - return provider.provide(target, this); + return provider.provide(target, annotation, this); } catch (Exception e) { throw new ServiceInitializationException( "Error while providing annotation " + annotation.annotationType().getName() + " for " + @@ -1503,7 +1588,7 @@ else if ( @Override public void set(@NotNull Class type, @NotNull T dependency) { try { - set(type, dependency, Security.getCallerClass(Thread.currentThread().getStackTrace())); + set(type, dependency, Contexts.getCallerClass(Thread.currentThread().getStackTrace())); } catch (ClassNotFoundException e) { set(type, dependency, Container.class); } diff --git a/src/main/java/com/atlas/divine/provider/AnnotationProvider.java b/src/main/java/com/atlas/divine/provider/AnnotationProvider.java index a69a43a..f95ffbe 100644 --- a/src/main/java/com/atlas/divine/provider/AnnotationProvider.java +++ b/src/main/java/com/atlas/divine/provider/AnnotationProvider.java @@ -1,24 +1,34 @@ package com.atlas.divine.provider; +import com.atlas.divine.descriptor.generic.ServiceLike; import com.atlas.divine.tree.ContainerInstance; import org.jetbrains.annotations.NotNull; +import java.lang.annotation.Annotation; + /** * Represents a functional interface that provides an implementation for the specified interface. *

- * The {@link #provide(Class, ContainerInstance)} method is called, to resolve the implementation of a service, + * The {@link #provide(Class, Annotation, ContainerInstance)} method is called, to resolve the implementation of a service, * whenever its corresponding custom annotation is present on a field or constructor parameter. * - * @param + * @param the type of the service that this class provides an implementation for + * @param the type of the annotation that is present on the field or constructor parameter */ @FunctionalInterface -public interface AnnotationProvider { +public interface AnnotationProvider<@ServiceLike TImplementation, TAnnotation extends Annotation> { /** * Provide an implementation for the specified interface. * * @param target the target interface that the implementation is being provided for + * @param annotation the annotation that is present on the field or constructor parameter * @param container the container instance that the implementation is being provided from + * * @return the implementation of the specified interface */ - @NotNull TImplementation provide(@NotNull Class target, @NotNull ContainerInstance container); + @NotNull TImplementation provide( + @NotNull Class target, + @NotNull TAnnotation annotation, + @NotNull ContainerInstance container + ); } diff --git a/src/main/java/com/atlas/divine/provider/Ref.java b/src/main/java/com/atlas/divine/provider/Ref.java index ba4ea88..2c9c911 100644 --- a/src/main/java/com/atlas/divine/provider/Ref.java +++ b/src/main/java/com/atlas/divine/provider/Ref.java @@ -2,6 +2,7 @@ import com.atlas.divine.descriptor.generic.Inject; import com.atlas.divine.tree.ContainerInstance; +import org.jetbrains.annotations.NotNull; /** * Represents a lazy accessor of a dependency value. @@ -24,5 +25,5 @@ public interface Ref { * * @return the service implementation */ - T get(); + @NotNull T get(); } diff --git a/src/main/java/com/atlas/divine/runtime/context/Security.java b/src/main/java/com/atlas/divine/runtime/context/Contexts.java similarity index 89% rename from src/main/java/com/atlas/divine/runtime/context/Security.java rename to src/main/java/com/atlas/divine/runtime/context/Contexts.java index 773298f..c077605 100644 --- a/src/main/java/com/atlas/divine/runtime/context/Security.java +++ b/src/main/java/com/atlas/divine/runtime/context/Contexts.java @@ -3,8 +3,13 @@ import lombok.experimental.UtilityClass; import org.jetbrains.annotations.NotNull; +/** + * Represents a utility class that resolves call context classes. + *

+ * The call contexts are used to separate services based on scopes. + */ @UtilityClass -public class Security { +public class Contexts { /** * Get the caller class of the method that called {@link Thread#getStackTrace()}. * diff --git a/src/main/java/com/atlas/divine/tree/ContainerInstance.java b/src/main/java/com/atlas/divine/tree/ContainerInstance.java index f0d4cc5..a5df630 100644 --- a/src/main/java/com/atlas/divine/tree/ContainerInstance.java +++ b/src/main/java/com/atlas/divine/tree/ContainerInstance.java @@ -45,7 +45,7 @@ public interface ContainerInstance { * * @throws InvalidServiceException if the annotation does not have a RUNTIME retention */ - void addProvider(@NotNull Class annotation, @NotNull AnnotationProvider provider); + void addProvider(@NotNull Class annotation, @NotNull AnnotationProvider provider); /** * Remove a custom annotation from the container instance. diff --git a/src/test/java/com/atlas/divine/ContainerTest.java b/src/test/java/com/atlas/divine/ContainerTest.java index ffeec75..3bbdc82 100644 --- a/src/test/java/com/atlas/divine/ContainerTest.java +++ b/src/test/java/com/atlas/divine/ContainerTest.java @@ -15,6 +15,7 @@ import com.atlas.divine.descriptor.generic.ServiceScope; import com.atlas.divine.descriptor.property.NoProperties; import com.atlas.divine.descriptor.property.PropertyProvider; +import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -295,33 +296,39 @@ interface MyCustomService { int get(); } + @RequiredArgsConstructor static class MyCustomServiceImpl implements MyCustomService { + private final int val; + @Override public int get() { - return 123; + return val; } } - static class MyProvider implements AnnotationProvider { + static class MyAnnotationProvider implements AnnotationProvider { @Override - public @NotNull MyCustomServiceImpl provide(@NotNull Class target, @NotNull ContainerInstance container) { - return new MyCustomServiceImpl(); + public @NotNull MyCustomServiceImpl provide( + @NotNull Class target, @NotNull MyCustomAnnotation annotation, @NotNull ContainerInstance container + ) { + return new MyCustomServiceImpl(annotation.val()); } } @Retention(RetentionPolicy.RUNTIME) @interface MyCustomAnnotation { + int val(); } @Service static class MyProvidedService { - @MyCustomAnnotation + @MyCustomAnnotation(val = 123) public MyCustomService service; } @Test public void test_custom_annotation_injection() { - Container.addProvider(MyCustomAnnotation.class, new MyProvider()); + Container.addProvider(MyCustomAnnotation.class, new MyAnnotationProvider()); MyProvidedService service = Container.get(MyProvidedService.class); assertEquals(123, service.service.get()); } @@ -504,4 +511,36 @@ public void test_container_token_resolve() { int value = Container.resolve("SECRET", Integer::parseInt); assertEquals(1337, value); } + + @Test + public void test_lazy_service_init() { + @Service + class OtherService { + int get() { + return 123; + } + } + + @Service + class MyService { + @Inject(lazy = true) + OtherService otherService; + + int val; + + @AfterInitialized + public void init() { + assertNull(otherService); + } + + @AfterInitialized(lazy = true) + public void lazyInit() { + assertNotNull(otherService); + val = otherService.get(); + } + } + + MyService service = Container.get(MyService.class); + assertEquals(123, service.val); + } }