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

Improve annotation providers and add lazy service initialization #14

Merged
merged 9 commits into from
Aug 10, 2024
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
```
```xml
<dependency>
<groupId>com.github.qibergames</groupId>
<artifactId>di-vine</artifactId>
<version>VERSION</version>
</dependency>
```

### Gradle
```gradle
repositories {
maven { url 'https://jitpack.io' }
}
```
```gradle
dependencies {
implementation 'com.github.qibergames:di-vine:VERSION'
}
```
6 changes: 4 additions & 2 deletions src/main/java/com/atlas/divine/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down
111 changes: 98 additions & 13 deletions src/main/java/com/atlas/divine/impl/DefaultContainerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -83,6 +83,9 @@ public class DefaultContainerImpl implements ContainerRegistry {

/**
* The map of fields to be lazily injected by the container.
* <p>
* 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);

Expand All @@ -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.
* <p>
* 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.
*/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -296,7 +312,7 @@ public void insert(@NotNull @ServiceLike Class<?> @NotNull ... services) {
@Override
public <TServices> @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);
}
Expand All @@ -315,7 +331,7 @@ public void insert(@NotNull @ServiceLike Class<?> @NotNull ... services) {
@Override
public <T> @NotNull T get(@NotNull Class<T> 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);
}
Expand Down Expand Up @@ -353,7 +369,7 @@ public void insert(@NotNull @ServiceLike Class<?> @NotNull ... services) {
@NotNull Class<TService> 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);
}
Expand Down Expand Up @@ -474,6 +490,7 @@ public <TDependency, TResult> TResult resolve(
finally {
stack.pop();
injectLazyFields();
invokeLazyMethods();
}
}

Expand Down Expand Up @@ -805,12 +822,19 @@ private <TService> 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);
Expand Down Expand Up @@ -1108,9 +1132,6 @@ private void injectLazyFields() {
if (!resolvingStack.get().isEmpty())
return;

// resolve the lazy fields to be injected
Map<Field, LazyFieldAccess> 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()) {
Expand All @@ -1121,6 +1142,9 @@ private void injectLazyFields() {
// begin injecting the lazy fields
injectingLazyFields.set(true);

// resolve the lazy fields to be injected
Map<Field, LazyFieldAccess> fields = lazyFields.get();

// iterate over the lazy fields to be injected
for (Map.Entry<Field, LazyFieldAccess> entry : fields.entrySet()) {
// resolve the field and the access to the field
Expand Down Expand Up @@ -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<Method, Object> methods = lazyMethods.get();

// iterate over the lazy methods to be invoked
for (Map.Entry<Method, Object> 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.
*
Expand Down Expand Up @@ -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 " +
Expand Down Expand Up @@ -1503,7 +1588,7 @@ else if (
@Override
public <T> void set(@NotNull Class<T> 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);
}
Expand Down
18 changes: 14 additions & 4 deletions src/main/java/com/atlas/divine/provider/AnnotationProvider.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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 <TImplementation>
* @param <TImplementation> the type of the service that this class provides an implementation for
* @param <TAnnotation> the type of the annotation that is present on the field or constructor parameter
*/
@FunctionalInterface
public interface AnnotationProvider<TImplementation> {
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
);
}
3 changes: 2 additions & 1 deletion src/main/java/com/atlas/divine/provider/Ref.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,5 +25,5 @@ public interface Ref<T> {
*
* @return the service implementation
*/
T get();
@NotNull T get();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;

/**
* Represents a utility class that resolves call context classes.
* <p>
* 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()}.
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/atlas/divine/tree/ContainerInstance.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading