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 extends Annotation> annotation, @NotNull AnnotationProvider> provider) {
+ public void addProvider(
+ @NotNull Class extends Annotation> 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 extends Annotation>, @NotNull AnnotationProvider>> providers = new ConcurrentHashMap<>();
+ private final @NotNull Map<@NotNull Class extends Annotation>, @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 extends Annotation> annotation, @NotNull AnnotationProvider> provider
+ @NotNull Class extends Annotation> 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 extends Annotation> annotation, @NotNull AnnotationProvider> provider);
+ void addProvider(@NotNull Class extends Annotation> 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);
+ }
}